Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 128 additions & 7 deletions src/gamescene/components/Ball.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,100 @@ export type ShootFn = (power: number) => boolean;

const PORTAL_TELEPORT_COOLDOWN_MS = 350;
const PORTAL_WARP_SOUND_URL = "/portal_se.mp3";
const CUSHION_COLLISION_SOUND_URL = "/maou_se_sound_footstep02.mp3";
const BALL_COLLISION_SOUND_URL = "/collision_with_balls.mp3";
const CUSHION_COLLISION_VOLUME = 1;
const BALL_COLLISION_VOLUME = 0.2;
const COLLISION_AUDIO_POOL_SIZE = 6;

let collisionPoolsInitialized = false;
let cushionCollisionPool: HTMLAudioElement[] = [];
let ballCollisionPool: HTMLAudioElement[] = [];
let cushionCollisionCursor = 0;
let ballCollisionCursor = 0;
let collisionPrimed = false;
let collisionGestureListenerAttached = false;

function initCollisionAudioPools() {
if (collisionPoolsInitialized) return;
const createAudioPool = (url: string, volume: number, size: number) => {
const pool: HTMLAudioElement[] = [];
for (let i = 0; i < size; i += 1) {
const audio = new Audio(url);
audio.preload = "auto";
audio.volume = volume;
audio.load();
pool.push(audio);
}
return pool;
};

cushionCollisionPool = createAudioPool(
CUSHION_COLLISION_SOUND_URL,
CUSHION_COLLISION_VOLUME,
COLLISION_AUDIO_POOL_SIZE,
);
ballCollisionPool = createAudioPool(
BALL_COLLISION_SOUND_URL,
BALL_COLLISION_VOLUME,
COLLISION_AUDIO_POOL_SIZE,
);
collisionPoolsInitialized = true;
}

function primeCollisionAudioPools() {
if (collisionPrimed) return;
if (cushionCollisionPool.length === 0 || ballCollisionPool.length === 0)
return;
collisionPrimed = true;

const primeAudio = (audio: HTMLAudioElement | undefined, volume: number) => {
if (!audio) return;
const desiredVolume = volume;
audio.volume = 0;
const playPromise = audio.play();
if (playPromise) {
playPromise
.then(() => {
audio.pause();
audio.currentTime = 0;
audio.volume = desiredVolume;
})
.catch(() => {
audio.volume = desiredVolume;
});
} else {
audio.volume = desiredVolume;
}
};

primeAudio(cushionCollisionPool[0], CUSHION_COLLISION_VOLUME);
primeAudio(ballCollisionPool[0], BALL_COLLISION_VOLUME);
}

function attachCollisionPrimeOnFirstGesture() {
if (collisionGestureListenerAttached) return;
collisionGestureListenerAttached = true;
window.addEventListener(
"pointerdown",
() => {
primeCollisionAudioPools();
},
{ once: true },
);
}

function playCollisionFromPool(pool: HTMLAudioElement[], cursor: number) {
if (pool.length === 0) return cursor;
const index = cursor % pool.length;
const audio = pool[index];
audio.currentTime = 0;
const playPromise = audio.play();
if (playPromise) {
playPromise.catch(() => {});
}
return index + 1;
}

type BallProps = {
id: string;
Expand Down Expand Up @@ -59,6 +153,20 @@ export function Ball({
}: BallProps) {
const texture = useTexture(textureUrl);

const playCushionCollision = useCallback(() => {
cushionCollisionCursor = playCollisionFromPool(
cushionCollisionPool,
cushionCollisionCursor,
);
}, []);

const playBallCollision = useCallback(() => {
ballCollisionCursor = playCollisionFromPool(
ballCollisionPool,
ballCollisionCursor,
);
}, []);

const [ref, api] = useSphere(() => ({
mass: 1, // ボールに質量を設定
position, // 初期位置を設定 (プレイエリアの上)
Expand All @@ -68,15 +176,19 @@ export function Ball({
material: { friction: 0.1, restitution: 1 }, // 摩擦を0.1から0.5に増加
linearDamping: 0.4, // 移動の減衰を追加
angularDamping: 0.4, // 回転の減衰を追加
userData: { type: "ball" },
userData: { type: "ball", id },
onCollide: (e) => {
console.log(e);
const audio1 = new Audio("/maou_se_sound_footstep02.mp3");
const audio2 = new Audio("/collision_with_balls.mp3");
audio1.volume = 1;
audio2.volume = 0.2;
if (e.body.userData.type === "cushion") audio1.play();
if (e.body.userData.type === "ball") audio2.play();
const type = e.body?.userData?.type;
if (type === "cushion") {
playCushionCollision();
}
if (type === "ball") {
const otherId = e.body?.userData?.id;
if (typeof otherId === "string" && id < otherId) {
playBallCollision();
}
}
},
}));

Expand Down Expand Up @@ -160,12 +272,21 @@ export function Ball({
});
}, [accelerationFloors]);

useEffect(() => {
initCollisionAudioPools();
attachCollisionPrimeOnFirstGesture();
}, []);

useEffect(() => {
portalWarpAudioRef.current = new Audio(PORTAL_WARP_SOUND_URL);
portalWarpAudioRef.current.volume = 0.35;
portalWarpAudioRef.current.preload = "auto";
portalWarpAudioRef.current.load();

dashAudioRef.current = new Audio("/acceleration_floor_se.mp3");
dashAudioRef.current.volume = 0.5;
dashAudioRef.current.preload = "auto";
dashAudioRef.current.load();

return () => {
portalWarpAudioRef.current = null;
Expand Down
107 changes: 105 additions & 2 deletions src/gamescene/constants/levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,102 @@ const DIVIDER_HEIGHT = PLAY_HEIGHT * 1.2;
const DIVIDER_THICKNESS = PLAY_HEIGHT * 0.6;
const DIVIDER_Y = (PLAY_HEIGHT + DIVIDER_HEIGHT) / 2 - OFFSET_Y;

const EX_STAGE_POCKETS: [number, number, number][] = [
[PLAY_WIDTH / 2, 0, PLAY_LENGTH / 2],
[-PLAY_WIDTH / 2, 0, PLAY_LENGTH / 2],
[PLAY_WIDTH / 2, 0, -PLAY_LENGTH / 2],
[-PLAY_WIDTH / 2, 0, -PLAY_LENGTH / 2],
[PLAY_WIDTH / 2, 0, 0],
[-PLAY_WIDTH / 2, 0, 0],
];

function directionToNearestPocket(
position: [number, number, number],
): [number, number, number] {
let best = EX_STAGE_POCKETS[0];
let bestDist = Number.POSITIVE_INFINITY;
for (const pocket of EX_STAGE_POCKETS) {
const dx = pocket[0] - position[0];
const dz = pocket[2] - position[2];
const dist = dx * dx + dz * dz;
if (dist < bestDist) {
bestDist = dist;
best = pocket;
}
}
return [best[0] - position[0], 0, best[2] - position[2]];
}

const EX_STAGE_BALLS: BallSpawnConfig[] = (() => {
const gridX = [-0.6, -0.36, -0.12, 0.12, 0.36, 0.6];
const gridZ = [-0.8, -0.4, 0, 0.4, 0.8];
const textures = [
poolballs1,
poolballs2,
poolballs3,
poolballs4,
poolballs5,
poolballs6,
];
const balls: BallSpawnConfig[] = [];
let idx = 0;
for (const z of gridZ) {
for (const x of gridX) {
if (idx === 0) {
balls.push({
id: "poolballs0",
textureUrl: poolballs0,
position: [x, 0.2, z],
shootable: true,
});
} else {
const textureUrl = textures[(idx - 1) % textures.length];
balls.push({
id: `exball-${idx}`,
textureUrl,
position: [x, 0.2, z],
});
}
idx += 1;
}
}
return balls;
})();

const EX_STAGE_ACCEL_FLOORS: AccelerationFloorConfig[] = (() => {
const positions: [number, number, number][] = [
[-0.9, 0, -1.6],
[0, 0, -1.6],
[0.9, 0, -1.6],
[-0.9, 0, -1.2],
[0, 0, -1.2],
[0.9, 0, -1.2],
[-0.9, 0, -0.6],
[0, 0, -0.6],
[0.9, 0, -0.6],
[-0.9, 0, 0.6],
[0, 0, 0.6],
[0.9, 0, 0.6],
[-0.9, 0, 1.2],
[0, 0, 1.2],
[0.9, 0, 1.2],
[-0.9, 0, 1.6],
[0, 0, 1.6],
[0.9, 0, 1.6],
];
return positions.map((position, index) => ({
id: `accel-ex-${index}`,
position,
size: [0.45, 0.3],
direction: directionToNearestPocket(position),
strength: 3.5,
}));
})();

export const LEVELS: LevelConfig[] = [
{
id: "level1",
name: "Level 1",
name: "Level 1 - Normal stage 1",
description: "2球を5打以内に落とす",
shotLimit: 5,
cueBallId: "poolballs0",
Expand All @@ -105,7 +197,7 @@ export const LEVELS: LevelConfig[] = [
},
{
id: "level2",
name: "Level 2",
name: "Level 2 - Normal stage 2",
description: "5球を15打以内に落とす",
shotLimit: 15,
cueBallId: "poolballs0",
Expand Down Expand Up @@ -555,6 +647,17 @@ export const LEVELS: LevelConfig[] = [
},
],
},
{
id: "level-ex-30",
name: "EX Stage - 30 Balls",
description: "30球を制限打数内に落とす。",
gimmic:
"大量のボールが配置されているステージです。うまくボールを加速床に乗せて落とそう!",
shotLimit: 10,
cueBallId: "poolballs0",
accelerationFloors: EX_STAGE_ACCEL_FLOORS,
balls: EX_STAGE_BALLS,
},
];

export function getLevelConfig(levelId?: string) {
Expand Down