From aa1dafa1a1b5a7a9ee7692a9baa28f958ca0200c Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Thu, 30 Apr 2026 18:18:26 +0900 Subject: [PATCH 01/24] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E5=BA=8A=E3=81=AE?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E7=BE=A9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index e63c297..ace5f58 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -30,6 +30,13 @@ export type GateConfig = { gatePos: [number, number, number][]; }; +export type AccelerationFloorConfig = { + position: [number, number, number]; + size: [number, number]; + direction: [number, number, number]; + strength: number; +}; + export type LevelConfig = { id: string; name: string; @@ -43,6 +50,7 @@ export type LevelConfig = { planeColor?: string; }; gate?: GateConfig; + accelerationFloors?: AccelerationFloorConfig[]; balls: BallSpawnConfig[]; }; From 872ff23a4a5082486eaedeb1c56465944a4c840e Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Thu, 30 Apr 2026 18:18:48 +0900 Subject: [PATCH 02/24] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E5=BA=8A=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AccelerationFloor.tsx | 82 +++++++++++++++++++ src/gamescene/index.tsx | 2 + 2 files changed, 84 insertions(+) create mode 100644 src/gamescene/components/AccelerationFloor.tsx diff --git a/src/gamescene/components/AccelerationFloor.tsx b/src/gamescene/components/AccelerationFloor.tsx new file mode 100644 index 0000000..0a1bfda --- /dev/null +++ b/src/gamescene/components/AccelerationFloor.tsx @@ -0,0 +1,82 @@ +import { useFrame } from "@react-three/fiber"; +import { useMemo, useRef } from "react"; +import * as THREE from "three"; +import type { AccelerationFloorConfig } from "../constants/levels"; + +type AccelerationFloorProps = { + config: AccelerationFloorConfig; +}; + +export function AccelerationFloor({ config }: AccelerationFloorProps) { + // 矢印の流れるテクスチャを Canvas API で生成 + const texture = useMemo(() => { + const canvas = document.createElement("canvas"); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext("2d"); + if (ctx) { + // 背景を少し暗く + ctx.fillStyle = "rgba(0, 0, 0, 0.4)"; + ctx.fillRect(0, 0, 256, 256); + + // 矢印の描画設定 + ctx.strokeStyle = "#ffaa00"; + ctx.lineWidth = 20; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + // 矢印を描画 + const drawArrow = (yOffset: number) => { + ctx.beginPath(); + ctx.moveTo(16, yOffset + 112); + ctx.lineTo(128, yOffset); + ctx.lineTo(240, yOffset + 112); + ctx.stroke(); + }; + + // y軸マイナス方向が進行方向になるように描画 + // 切れ目なくループするように配置 (16と144でちょうど128px間隔) + drawArrow(16); + drawArrow(144); + } + + const tex = new THREE.CanvasTexture(canvas); + tex.wrapS = THREE.RepeatWrapping; + tex.wrapT = THREE.RepeatWrapping; + // 幅に対して1回、高さはアスペクト比を維持してリピート + tex.repeat.set(1, config.size[1] / config.size[0]); + return tex; + }, [config.size]); + + const materialRef = useRef(null); + + // 毎フレーム、テクスチャをずらして流れるアニメーションを実現 + useFrame((_, delta) => { + if (materialRef.current?.map) { + // テクスチャを上方向(V軸マイナス方向)へスクロールして、矢印の向きに流れるようにする + materialRef.current.map.offset.y -= delta * 2.5; + } + }); + + // direction ベクトルからY軸の回転角度を計算 + // Z軸(0, 0, 1) を基準とした角度 + const angle = Math.atan2(config.direction[0], config.direction[2]); + // 矢印(テクスチャの上部)が指定された方向を向くように補正 (+ Math.PI) + const visualAngle = angle + Math.PI; + + return ( + + + + + + + ); +} diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index a52153b..8e88f98 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -11,6 +11,7 @@ import { } from "react"; import { useNavigate, useParams } from "react-router-dom"; import billiardHallHdr from "../assets/backgroundHDR/billiard_hall_1k.hdr"; +import { AccelerationFloor } from "./components/AccelerationFloor"; import { Ball, type ShootFn } from "./components/Ball"; import { BilliardTable } from "./components/billiardTable"; import { CameraController } from "./components/CameraController"; @@ -324,6 +325,7 @@ export default function GameScene() { position={ballPositionsRef.current[ball.id]} velocity={isRespawnedCueBall ? [0, 0, 0] : ball.velocity} portal={level.portal} + accelerationFloors={level.accelerationFloors} respawnPosition={ ball.id === cueBallId ? state?.respawnPosition : undefined } From 9634b0cf4bcb02b96939694a392c06d735325de1 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Thu, 30 Apr 2026 18:19:03 +0900 Subject: [PATCH 03/24] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E5=BA=8A=E3=81=AE?= =?UTF-8?q?=E7=89=A9=E7=90=86=E6=BC=94=E7=AE=97=E3=83=AD=E3=82=B8=E3=83=83?= =?UTF-8?q?=E3=82=AF=EF=BC=88=E9=80=9F=E5=BA=A6=E5=BC=B7=E5=88=B6=E4=B8=8A?= =?UTF-8?q?=E6=9B=B8=E3=81=8D=EF=BC=89=E3=82=92Ball=E3=81=AB=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/components/Ball.tsx | 71 ++++++++++++++++++++++++++++++- src/gamescene/index.tsx | 6 +++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/gamescene/components/Ball.tsx b/src/gamescene/components/Ball.tsx index 998c532..1c0ead3 100644 --- a/src/gamescene/components/Ball.tsx +++ b/src/gamescene/components/Ball.tsx @@ -3,7 +3,10 @@ import { useTexture } from "@react-three/drei"; import { useThree } from "@react-three/fiber"; import { useCallback, useEffect, useRef } from "react"; import * as THREE from "three"; -import type { PortalConfig } from "../constants/levels"; +import type { + AccelerationFloorConfig, + PortalConfig, +} from "../constants/levels"; import { BALL_RADIUS } from "../constants/physics"; import { POCKET_Y_THRESHOLD } from "./billiardTable"; export type ShootFn = (power: number) => boolean; @@ -23,6 +26,7 @@ type BallProps = { onPocket?: (id: string) => void; onPositionChange?: (id: string, position: [number, number, number]) => void; portal?: PortalConfig; + accelerationFloors?: AccelerationFloorConfig[]; }; function isInsidePortal( @@ -47,6 +51,7 @@ export function Ball({ onPocket, onPositionChange, portal, + accelerationFloors, }: BallProps) { const texture = useTexture(textureUrl); @@ -75,14 +80,21 @@ export function Ball({ const hasPocketed = useRef(false); const lastVelocityRef = useRef<[number, number, number]>([0, 0, 0]); const lastTeleportAtRef = useRef(0); + const floorsOnRef = useRef>(new Set()); const portalWarpAudioRef = useRef(null); + const dashAudioRef = useRef(null); useEffect(() => { portalWarpAudioRef.current = new Audio(PORTAL_WARP_SOUND_URL); portalWarpAudioRef.current.volume = 0.35; + // ダッシュ時の効果音として既存の衝突音などを流用するか、専用の音声をロードする + dashAudioRef.current = new Audio("/collision_with_balls.mp3"); + dashAudioRef.current.volume = 0.5; + return () => { portalWarpAudioRef.current = null; + dashAudioRef.current = null; }; }, []); @@ -142,6 +154,52 @@ export function Ball({ } } + if (accelerationFloors) { + accelerationFloors.forEach((floor, idx) => { + const dx = p[0] - floor.position[0]; + const dz = p[2] - floor.position[2]; + + // directionベクトルの角度を計算 + const angle = Math.atan2(floor.direction[0], floor.direction[2]); + + // 逆回転させてローカル座標に変換 + const localX = dx * Math.cos(-angle) - dz * Math.sin(-angle); + const localZ = dx * Math.sin(-angle) + dz * Math.cos(-angle); + + const halfWidth = floor.size[0] / 2; + const halfLength = floor.size[1] / 2; + + const isInside = + Math.abs(localX) <= halfWidth && Math.abs(localZ) <= halfLength; + const wasInside = floorsOnRef.current.has(idx); + + if (isInside && !wasInside) { + // 床に入った瞬間、速度とスピンを完全に上書きする + // directionは正規化されている前提 + const dir = new THREE.Vector3( + floor.direction[0], + floor.direction[1], + floor.direction[2], + ).normalize(); + + // 速度(velocity)を強制上書き。strength を直接のスピードとして扱う + api.velocity.set(dir.x * floor.strength, 0, dir.z * floor.strength); + + // 直進後に変なカーブを描かないように、ボールの回転(スピン)をリセット + api.angularVelocity.set(0, 0, 0); + floorsOnRef.current.add(idx); + + if (dashAudioRef.current) { + dashAudioRef.current.currentTime = 0; + void dashAudioRef.current.play(); + } + } else if (!isInside && wasInside) { + // 床から出た + floorsOnRef.current.delete(idx); + } + }); + } + if (hasPocketed.current) return; if (p[1] <= POCKET_Y_THRESHOLD) { @@ -153,7 +211,16 @@ export function Ball({ }); return () => unsubscribe(); - }, [api.position, api.velocity, id, onPocket, onPositionChange, portal]); + }, [ + api.position, + api.velocity, + id, + onPocket, + onPositionChange, + portal, + accelerationFloors, + api.angularVelocity, + ]); useEffect(() => { if (!respawnPosition) return; diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index 8e88f98..9da421b 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -345,6 +345,12 @@ export default function GameScene() { ); })} {level.portal && } + {level.accelerationFloors?.map((floor) => ( + + ))} From fdce50acc0fadf911cc9ea3e01281f4b39d63bf1 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Thu, 30 Apr 2026 18:19:12 +0900 Subject: [PATCH 04/24] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E5=BA=8A=E3=81=AE?= =?UTF-8?q?=E9=80=A3=E9=8E=96=E3=82=92=E5=88=A9=E7=94=A8=E3=81=97=E3=81=9F?= =?UTF-8?q?Level=205=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index ace5f58..09bef0e 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -192,6 +192,46 @@ export const LEVELS: LevelConfig[] = [ }, ], }, + { + id: "level5", + name: "Level 5 - Dash Panel Chain", + description: "3つの加速床を連鎖させて、サイドポケットに落とそう", + shotLimit: 5, + cueBallId: "poolballs0", + accelerationFloors: [ + { + position: [-0.5, 0, 0.5], + size: [0.3, 0.3], + direction: [1, 0, 0], // +Xへ + strength: 7, + }, + { + position: [0.6, 0, 0.5], + size: [0.3, 0.3], + direction: [0, 0, 1], // +Zへ + strength: 7, + }, + { + position: [0.6, 0, 1.87], + size: [0.3, 0.3], + direction: [1, 0, 1], // 斜め45度 (右上コーナーポケットへ) + strength: 9, // 最後は少し強めに + }, + ], + balls: [ + { + id: "poolballs0", + textureUrl: poolballs0, + position: [-0.5, 0.2, -2.0], + shootable: true, + }, + { + id: "poolballs1", + textureUrl: poolballs1, + position: [-0.5, 0.2, -1.0], + }, + ], + }, ]; export function getLevelConfig(levelId?: string) { From 2b4460490d2ce80efcc9bd4f2569ecc78c50dcb2 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Thu, 30 Apr 2026 18:54:28 +0900 Subject: [PATCH 05/24] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=81=B8=E3=81=AE=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AccelerationFloor.tsx | 15 ++++-- src/gamescene/components/Ball.tsx | 54 ++++++++++++------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/gamescene/components/AccelerationFloor.tsx b/src/gamescene/components/AccelerationFloor.tsx index 0a1bfda..7a5d8dd 100644 --- a/src/gamescene/components/AccelerationFloor.tsx +++ b/src/gamescene/components/AccelerationFloor.tsx @@ -1,5 +1,5 @@ import { useFrame } from "@react-three/fiber"; -import { useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import * as THREE from "three"; import type { AccelerationFloorConfig } from "../constants/levels"; @@ -48,13 +48,22 @@ export function AccelerationFloor({ config }: AccelerationFloorProps) { return tex; }, [config.size]); + // メモリリークを防ぐため、コンポーネントのアンマウント時またはテクスチャ再生成時に古いテクスチャを破棄 + useEffect(() => { + return () => { + texture.dispose(); + }; + }, [texture]); + const materialRef = useRef(null); // 毎フレーム、テクスチャをずらして流れるアニメーションを実現 useFrame((_, delta) => { - if (materialRef.current?.map) { + const map = materialRef.current?.map; + if (map) { // テクスチャを上方向(V軸マイナス方向)へスクロールして、矢印の向きに流れるようにする - materialRef.current.map.offset.y -= delta * 2.5; + // RepeatWrapping を使っているため、オフセットは 0..1 の範囲に正規化して精度低下を防ぐ + map.offset.y = (((map.offset.y - delta * 2.5) % 1) + 1) % 1; } }); diff --git a/src/gamescene/components/Ball.tsx b/src/gamescene/components/Ball.tsx index 1c0ead3..8e0bd50 100644 --- a/src/gamescene/components/Ball.tsx +++ b/src/gamescene/components/Ball.tsx @@ -1,7 +1,7 @@ import { useSphere } from "@react-three/cannon"; import { useTexture } from "@react-three/drei"; import { useThree } from "@react-three/fiber"; -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import * as THREE from "three"; import type { AccelerationFloorConfig, @@ -84,6 +84,26 @@ export function Ball({ const portalWarpAudioRef = useRef(null); const dashAudioRef = useRef(null); + // 加速床の判定に必要な計算(角度、三角関数、半サイズ、正規化ベクトル)を事前計算 + const processedFloors = useMemo(() => { + if (!accelerationFloors) return null; + return accelerationFloors.map((floor) => { + const angle = Math.atan2(floor.direction[0], floor.direction[2]); + return { + ...floor, + sinAngle: Math.sin(-angle), + cosAngle: Math.cos(-angle), + halfWidth: floor.size[0] / 2, + halfLength: floor.size[1] / 2, + normalizedDir: new THREE.Vector3( + floor.direction[0], + floor.direction[1], + floor.direction[2], + ).normalize(), + }; + }); + }, [accelerationFloors]); + useEffect(() => { portalWarpAudioRef.current = new Audio(PORTAL_WARP_SOUND_URL); portalWarpAudioRef.current.volume = 0.35; @@ -129,6 +149,10 @@ export function Ball({ }, [api.velocity, api.angularVelocity, id, onMovingChange]); useEffect(() => { + // 物理サブスクリプションが再生成される(床などの環境が更新された)タイミングで + // 過去に乗っていた床の履歴をクリアする + floorsOnRef.current.clear(); + const unsubscribe = api.position.subscribe((p) => { onPositionChange?.(id, [p[0], p[1], p[2]]); @@ -154,33 +178,23 @@ export function Ball({ } } - if (accelerationFloors) { - accelerationFloors.forEach((floor, idx) => { + if (processedFloors) { + processedFloors.forEach((floor, idx) => { const dx = p[0] - floor.position[0]; const dz = p[2] - floor.position[2]; - // directionベクトルの角度を計算 - const angle = Math.atan2(floor.direction[0], floor.direction[2]); - - // 逆回転させてローカル座標に変換 - const localX = dx * Math.cos(-angle) - dz * Math.sin(-angle); - const localZ = dx * Math.sin(-angle) + dz * Math.cos(-angle); - - const halfWidth = floor.size[0] / 2; - const halfLength = floor.size[1] / 2; + // 事前計算済みの値を使ってローカル座標に変換 + const localX = dx * floor.cosAngle - dz * floor.sinAngle; + const localZ = dx * floor.sinAngle + dz * floor.cosAngle; const isInside = - Math.abs(localX) <= halfWidth && Math.abs(localZ) <= halfLength; + Math.abs(localX) <= floor.halfWidth && + Math.abs(localZ) <= floor.halfLength; const wasInside = floorsOnRef.current.has(idx); if (isInside && !wasInside) { // 床に入った瞬間、速度とスピンを完全に上書きする - // directionは正規化されている前提 - const dir = new THREE.Vector3( - floor.direction[0], - floor.direction[1], - floor.direction[2], - ).normalize(); + const dir = floor.normalizedDir; // 速度(velocity)を強制上書き。strength を直接のスピードとして扱う api.velocity.set(dir.x * floor.strength, 0, dir.z * floor.strength); @@ -218,7 +232,7 @@ export function Ball({ onPocket, onPositionChange, portal, - accelerationFloors, + processedFloors, api.angularVelocity, ]); From cf6093c00edbe50b47dced29a9968684773621c2 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Wed, 6 May 2026 15:13:50 +0900 Subject: [PATCH 06/24] =?UTF-8?q?=E8=A9=A6=E3=81=97=E3=81=AB=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=81=97=E3=81=9Flevel6=E3=82=92=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=81=97=E3=81=A6level8(=E7=88=86=E5=BC=BE+=E5=8A=A0=E9=80=9F?= =?UTF-8?q?=E5=BA=8A=E3=81=AE=E7=BD=A0)=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 71 +++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index 4b8ac84..19d7c3c 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -225,42 +225,77 @@ export const LEVELS: LevelConfig[] = [ ], }, { - id: "level6", - name: "Level 6 - Dash Panel Chain", - description: "3つの加速床を連鎖させて、サイドポケットに落とそう", - shotLimit: 5, + id: "level8", + name: "Level 8 - Bomb Trap", + description: "加速床の罠を避けて3球を20打以内に落とす", + shotLimit: 20, cueBallId: "poolballs0", + bombs: [ + { id: "bomb0", position: [0, 0.2, -1.1] }, + { id: "bomb1", position: [0, 0.2, 1.1] }, + ], accelerationFloors: [ + // 罠1: 爆弾(0, -1.1)を重心とする正三角形の各頂点に配置 + // 頂点1: テーブル中央側 (0, -0.6) → 爆弾へ [0, 0, -1] + { + position: [0, 0, -0.6], + size: [0.35, 0.3], + direction: [0, 0, -1], + strength: 8, + }, + // 頂点2: 左奥 (-0.43, -1.35) → 爆弾へ [0.87, 0, 0.5] + { + position: [-0.43, 0, -1.35], + size: [0.35, 0.3], + direction: [0.87, 0, 0.5], + strength: 8, + }, + // 頂点3: 右奥 (0.43, -1.35) → 爆弾へ [-0.87, 0, 0.5] { - position: [-0.5, 0, 0.5], - size: [0.3, 0.3], - direction: [1, 0, 0], // +Xへ - strength: 7, + position: [0.43, 0, -1.35], + size: [0.35, 0.3], + direction: [-0.87, 0, 0.5], + strength: 8, }, + // 罠2: 爆弾(0, 1.1)を重心とする正三角形の各頂点に配置 + // 頂点1: テーブル中央側 (0, 0.6) → 爆弾へ [0, 0, 1] { - position: [0.6, 0, 0.5], - size: [0.3, 0.3], - direction: [0, 0, 1], // +Zへ - strength: 7, + position: [0, 0, 0.6], + size: [0.35, 0.3], + direction: [0, 0, 1], + strength: 8, }, + // 頂点2: 右奥 (0.43, 1.35) → 爆弾へ [-0.87, 0, -0.5] { - position: [0.6, 0, 1.87], - size: [0.3, 0.3], - direction: [1, 0, 1], // 斜め45度 (右上コーナーポケットへ) - strength: 9, // 最後は少し強めに + position: [0.43, 0, 1.35], + size: [0.35, 0.3], + direction: [-0.87, 0, -0.5], + strength: 8, + }, + // 頂点3: 左奥 (-0.43, 1.35) → 爆弾へ [0.87, 0, -0.5] + { + position: [-0.43, 0, 1.35], + size: [0.35, 0.3], + direction: [0.87, 0, -0.5], + strength: 8, }, ], balls: [ { id: "poolballs0", textureUrl: poolballs0, - position: [-0.5, 0.2, -2.0], + position: [-0.9, 0.2, 0], shootable: true, }, { id: "poolballs1", textureUrl: poolballs1, - position: [-0.5, 0.2, -1.0], + position: [0, 0.2, -2.0], + }, + { + id: "poolballs2", + textureUrl: poolballs2, + position: [0, 0.2, 2.0], }, ], }, From 06fbb5049cb776a009b81c68e6ddbc361b6dc353 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Wed, 6 May 2026 15:46:18 +0900 Subject: [PATCH 07/24] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E5=BA=8A=E3=81=AESE?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/acceleration_floor_se.mp3 | Bin 0 -> 31346 bytes src/gamescene/components/Ball.tsx | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 public/acceleration_floor_se.mp3 diff --git a/public/acceleration_floor_se.mp3 b/public/acceleration_floor_se.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..1caea8765d9fca8b0dd5302630f49edbc6e148a7 GIT binary patch literal 31346 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ĩq~fFDbYe3dxx& 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 literal 0 HcmV?d00001 diff --git a/src/gamescene/components/Ball.tsx b/src/gamescene/components/Ball.tsx index 7d0b09a..ebf99b3 100644 --- a/src/gamescene/components/Ball.tsx +++ b/src/gamescene/components/Ball.tsx @@ -168,8 +168,7 @@ export function Ball({ portalWarpAudioRef.current = new Audio(PORTAL_WARP_SOUND_URL); portalWarpAudioRef.current.volume = 0.35; - // ダッシュ時の効果音として既存の衝突音などを流用するか、専用の音声をロードする - dashAudioRef.current = new Audio("/collision_with_balls.mp3"); + dashAudioRef.current = new Audio("/acceleration_floor_se.mp3"); dashAudioRef.current.volume = 0.5; return () => { From e0598fe4b499f50479979aa2d3c783274a5e5a73 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Wed, 6 May 2026 15:46:38 +0900 Subject: [PATCH 08/24] =?UTF-8?q?=E5=8A=A0=E9=80=9F=E5=BA=8A=E3=81=AE?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E7=BE=A9=E3=82=92=E8=A6=8B=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/components/Ball.tsx | 10 +++++----- src/gamescene/constants/levels.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/gamescene/components/Ball.tsx b/src/gamescene/components/Ball.tsx index ebf99b3..7421385 100644 --- a/src/gamescene/components/Ball.tsx +++ b/src/gamescene/components/Ball.tsx @@ -82,7 +82,7 @@ export function Ball({ const hasPocketed = useRef(false); const lastVelocityRef = useRef<[number, number, number]>([0, 0, 0]); const lastTeleportAtRef = useRef(0); - const floorsOnRef = useRef>(new Set()); + const floorsOnRef = useRef>(new Set()); const portalWarpAudioRef = useRef(null); const keys = useRef>({}); @@ -238,7 +238,7 @@ export function Ball({ } if (processedFloors) { - processedFloors.forEach((floor, idx) => { + processedFloors.forEach((floor) => { const dx = p[0] - floor.position[0]; const dz = p[2] - floor.position[2]; @@ -249,7 +249,7 @@ export function Ball({ const isInside = Math.abs(localX) <= floor.halfWidth && Math.abs(localZ) <= floor.halfLength; - const wasInside = floorsOnRef.current.has(idx); + const wasInside = floorsOnRef.current.has(floor.id); if (isInside && !wasInside) { // 床に入った瞬間、速度とスピンを完全に上書きする @@ -260,7 +260,7 @@ export function Ball({ // 直進後に変なカーブを描かないように、ボールの回転(スピン)をリセット api.angularVelocity.set(0, 0, 0); - floorsOnRef.current.add(idx); + floorsOnRef.current.add(floor.id); if (dashAudioRef.current) { dashAudioRef.current.currentTime = 0; @@ -268,7 +268,7 @@ export function Ball({ } } else if (!isInside && wasInside) { // 床から出た - floorsOnRef.current.delete(idx); + floorsOnRef.current.delete(floor.id); } }); } diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index 19d7c3c..a1f6c2d 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -36,9 +36,10 @@ export type GateConfig = { }; export type AccelerationFloorConfig = { + id: string; position: [number, number, number]; size: [number, number]; - direction: [number, number, number]; + direction: [number, number, number]; // Y成分は無視され、XZ平面上の方向のみ有効 strength: number; }; @@ -238,6 +239,7 @@ export const LEVELS: LevelConfig[] = [ // 罠1: 爆弾(0, -1.1)を重心とする正三角形の各頂点に配置 // 頂点1: テーブル中央側 (0, -0.6) → 爆弾へ [0, 0, -1] { + id: "accel-l8-0", position: [0, 0, -0.6], size: [0.35, 0.3], direction: [0, 0, -1], @@ -245,6 +247,7 @@ export const LEVELS: LevelConfig[] = [ }, // 頂点2: 左奥 (-0.43, -1.35) → 爆弾へ [0.87, 0, 0.5] { + id: "accel-l8-1", position: [-0.43, 0, -1.35], size: [0.35, 0.3], direction: [0.87, 0, 0.5], @@ -252,6 +255,7 @@ export const LEVELS: LevelConfig[] = [ }, // 頂点3: 右奥 (0.43, -1.35) → 爆弾へ [-0.87, 0, 0.5] { + id: "accel-l8-2", position: [0.43, 0, -1.35], size: [0.35, 0.3], direction: [-0.87, 0, 0.5], @@ -260,6 +264,7 @@ export const LEVELS: LevelConfig[] = [ // 罠2: 爆弾(0, 1.1)を重心とする正三角形の各頂点に配置 // 頂点1: テーブル中央側 (0, 0.6) → 爆弾へ [0, 0, 1] { + id: "accel-l8-3", position: [0, 0, 0.6], size: [0.35, 0.3], direction: [0, 0, 1], @@ -267,6 +272,7 @@ export const LEVELS: LevelConfig[] = [ }, // 頂点2: 右奥 (0.43, 1.35) → 爆弾へ [-0.87, 0, -0.5] { + id: "accel-l8-4", position: [0.43, 0, 1.35], size: [0.35, 0.3], direction: [-0.87, 0, -0.5], @@ -274,6 +280,7 @@ export const LEVELS: LevelConfig[] = [ }, // 頂点3: 左奥 (-0.43, 1.35) → 爆弾へ [0.87, 0, -0.5] { + id: "accel-l8-5", position: [-0.43, 0, 1.35], size: [0.35, 0.3], direction: [0.87, 0, -0.5], From cf0443c8785b1c999b5e24bcdb444a064297fff0 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 15:06:35 +0900 Subject: [PATCH 09/24] =?UTF-8?q?level2,=20level3=E3=81=AE=E3=83=9C?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=81=AE=E9=85=8D=E7=BD=AE=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 47 ++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index 275a15b..ce74442 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -3,6 +3,8 @@ import poolballs1 from "@/assets/ballTexture/poolballs1.png"; import poolballs2 from "@/assets/ballTexture/poolballs2.png"; import poolballs3 from "@/assets/ballTexture/poolballs3.png"; import poolballs4 from "@/assets/ballTexture/poolballs4.png"; +import poolballs5 from "@/assets/ballTexture/poolballs5.png"; +import poolballs6 from "@/assets/ballTexture/poolballs6.png"; import tableIce from "@/assets/tableTexture/tableIce.svg"; import { OFFSET_Y, @@ -94,15 +96,8 @@ export const LEVELS: LevelConfig[] = [ id: "level2", name: "Level 2", description: "3球を7打以内に落とす", - shotLimit: 7, + shotLimit: 15, cueBallId: "poolballs0", - portals: [ - { - entry: [-0.25, 0, -0.45], - exit: [0.5, 0, 0.55], - radius: 0.12, - }, - ], balls: [ { id: "poolballs0", @@ -113,25 +108,35 @@ export const LEVELS: LevelConfig[] = [ { id: "poolballs1", textureUrl: poolballs1, - position: [0.12, 0.2, 0], + position: [0.25, 0.2, -0.1], }, { id: "poolballs2", textureUrl: poolballs2, - position: [0.25, 0.2, 0], + position: [0.25, 0.2, 0.1], }, { id: "poolballs3", textureUrl: poolballs3, position: [0.38, 0.2, 0], }, + { + id: "poolballs4", + textureUrl: poolballs4, + position: [0.5, 0.2, 0.5], + }, + { + id: "poolballs5", + textureUrl: poolballs5, + position: [0.5, 0.2, -0.5], + }, ], }, { id: "level3", name: "Level 3 - Ice Floor", description: "氷の床で4球を8打以内に落とす", - shotLimit: 8, + shotLimit: 15, cueBallId: "poolballs0", table: { clothTextureUrl: tableIce, @@ -142,28 +147,38 @@ export const LEVELS: LevelConfig[] = [ { id: "poolballs0", textureUrl: poolballs0, - position: [-0.8, 0.2, 0], + position: [0, 0.2, 0], shootable: true, }, { id: "poolballs1", textureUrl: poolballs1, - position: [0.15, 0.2, -0.2], + position: [0.2, 0.2, -0.07], }, { id: "poolballs2", textureUrl: poolballs2, - position: [0.32, 0.2, 0], + position: [-0.2, 0.2, 0.07], }, { id: "poolballs3", textureUrl: poolballs3, - position: [0.15, 0.2, 0.2], + position: [0.2, 0.2, 0.07], }, { id: "poolballs4", textureUrl: poolballs4, - position: [0.52, 0.2, 0], + position: [-0.2, 0.2, -0.07], + }, + { + id: "poolballs5", + textureUrl: poolballs5, + position: [0.3, 0.2, 0], + }, + { + id: "poolballs6", + textureUrl: poolballs6, + position: [-0.3, 0.2, 0], }, ], }, From b3ec45b8380ce03b93f00c20d00982c9125cdea9 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 15:24:42 +0900 Subject: [PATCH 10/24] =?UTF-8?q?=E3=83=9E=E3=82=B0=E3=83=8D=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=82=B3=E3=83=B3=E3=83=88=E3=83=AD=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=A8=E5=85=A5=E5=8A=9B?= =?UTF-8?q?=E6=96=B9=E5=90=91=E3=81=AEUI=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/index.tsx | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index c23d41c..7671282 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -103,6 +103,29 @@ export default function GameScene() { const [ballStates, setBallStates] = useState>({}); const [bombStates, setBombStates] = useState>({}); const [magnetEnabled] = useState(true); // マグネットコントロールのフラグ + const [pressedKey, setPressedKey] = useState(null); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const key = e.key.toLowerCase(); + if (key === "a" || key === "d") { + setPressedKey(key); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + const key = e.key.toLowerCase(); + if (key === "a" || key === "d") { + setPressedKey(null); + } + }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, []); + const ballPositionsRef = useRef>({}); const gameEndedRef = useRef(false); const hasSeenMovementSinceShotRef = useRef(false); @@ -501,6 +524,23 @@ export default function GameScene() { {isCharging && ( )} + {magnetEnabled && ( +
+
+ ← +
+
+ 🧲 +
+
+ → +
+
+ )} ); } From 2b474b5c4c4c72a2896e1bd57c1a0e29ae86ca93 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 15:57:27 +0900 Subject: [PATCH 11/24] =?UTF-8?q?=E9=81=8A=E3=81=B3=E6=96=B9=E8=AA=AC?= =?UTF-8?q?=E6=98=8E=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 16 ++++- package.json | 1 + src/pages/HowToPlayModal.tsx | 123 +++++++++++++++++++++++++++++++++++ src/pages/MenuScreen.tsx | 29 +++++++-- src/pages/howToPlay.tsx | 0 5 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 src/pages/HowToPlayModal.tsx create mode 100644 src/pages/howToPlay.tsx diff --git a/package-lock.json b/package-lock.json index 13a669c..77c2df5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@react-three/cannon": "^6.6.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", + "lucide-react": "^1.14.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.1", @@ -1874,6 +1875,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/maath": { "version": "0.10.8", "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", @@ -1958,9 +1968,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index e4fbe4e..e3c7a27 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@react-three/cannon": "^6.6.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", + "lucide-react": "^1.14.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.1", diff --git a/src/pages/HowToPlayModal.tsx b/src/pages/HowToPlayModal.tsx new file mode 100644 index 0000000..9bd652e --- /dev/null +++ b/src/pages/HowToPlayModal.tsx @@ -0,0 +1,123 @@ +import { X } from "lucide-react"; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +export function HowToPlayModal({ isOpen, onClose }: Props) { + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ 遊び方 +

+ +
+ + {/* Content */} +
+ {/* Basic Rule */} +
+

+ Basic Rules +

+

+ 白い球(キューボール)を打ち、ステージ上のすべての的球を6つのポケットのいずれかに落とすとクリアとなります。 + + ※各ステージには「打数制限(SHOT + LIMIT)」があります。制限内に全てのボールを落としましょう。 + +

+
+ + {/* Controls */} +
+

+ Controls +

+
+
+

+ 視点操作 (Mouse) +

+
    +
  • + + 右クリック + Drag: + {" "} + 平行移動 +
  • +
  • + + 左クリック + Drag: + {" "} + 視点回転 +
  • +
  • + Scroll:{" "} + 拡大・縮小 +
  • +
+
+
+

+ キュー注目時 (Keyboard) +

+
    +
  • + + W / S: + {" "} + 上下回転 +
  • +
  • + + A / D: + {" "} + 左右回転 +
  • +
  • + ※精密な角度調整が可能です +
  • +
+
+
+
+ + {/* Gimmicks */} +
+

+ Gimmicks +

+

+ ステージによっては特殊なギミックが登場します。 + 障害物の動きを見極め、反動や隙間を利用して戦略的にクリアを目指してください。 +

+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/pages/MenuScreen.tsx b/src/pages/MenuScreen.tsx index 512692f..e2655c0 100644 --- a/src/pages/MenuScreen.tsx +++ b/src/pages/MenuScreen.tsx @@ -1,7 +1,12 @@ +import { HelpCircle } from "lucide-react"; +import { useState } from "react"; import { Link } from "react-router-dom"; import { LEVELS } from "@/gamescene/constants/levels"; +import { HowToPlayModal } from "./HowToPlayModal"; export function MenuScreen() { + const [isHowToPlayOpen, setIsHowToPlayOpen] = useState(false); + return (
@@ -11,10 +16,21 @@ export function MenuScreen() {

BILLIARDS

-

- 指定打数以内で全ての的球を落とすソロモードです。まずはLevel - 1に挑戦してください。 -

+ +
+

+ 指定打数以内で全ての的球を落とすソロモードです。まずはLevel + 1に挑戦してください。 +

+ +
{LEVELS.map((level) => ( @@ -42,6 +58,11 @@ export function MenuScreen() { ))}
+ + setIsHowToPlayOpen(false)} + />
); } diff --git a/src/pages/howToPlay.tsx b/src/pages/howToPlay.tsx new file mode 100644 index 0000000..e69de29 From eafcbb3c70516b47d215c7f8b69264e439cccf64 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:15:07 +0900 Subject: [PATCH 12/24] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E9=96=8B=E5=A7=8B=E6=99=82=E3=81=AE=E3=83=A2=E3=83=BC=E3=83=80?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/gimmicDocs/StartModal.tsx | 39 +++++++++++++++++++++++++ src/gamescene/index.tsx | 21 +++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/gamescene/gimmicDocs/StartModal.tsx diff --git a/src/gamescene/gimmicDocs/StartModal.tsx b/src/gamescene/gimmicDocs/StartModal.tsx new file mode 100644 index 0000000..1a857fb --- /dev/null +++ b/src/gamescene/gimmicDocs/StartModal.tsx @@ -0,0 +1,39 @@ +type Props = { + title: string; + description: string; + onClose: () => void; +}; + +/** + * ステージ開始時に表示されるギミック説明用のモーダル。 + * UIフレームは共通化し、内容はプロップスで受け取る。 + */ +export function StartModal({ title, description, onClose }: Props) { + return ( +
+
+
+ MISSION DETAILS +
+

+ {title} +

+
+ {description.split("\n").map((line, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: simple static text +

+ {line} +

+ ))} +
+ +
+
+ ); +} diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index 7671282..a32f42b 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -26,6 +26,7 @@ import { StartBanner } from "./components/StartBanner"; import { TrajectoryLineRaycast } from "./components/TrajectoryLineRaycast"; import { getLevelConfig } from "./constants/levels"; import { BALL_RADIUS, calcStrikeDuration } from "./constants/physics"; +import { StartModal } from "./gimmicDocs/StartModal"; import { findCueRespawnPosition } from "./utils/cueRespawn"; type BallState = { @@ -102,6 +103,7 @@ export default function GameScene() { const [pendingShotResolution, setPendingShotResolution] = useState(false); const [ballStates, setBallStates] = useState>({}); const [bombStates, setBombStates] = useState>({}); + const [isStartModalOpen, setIsStartModalOpen] = useState(true); const [magnetEnabled] = useState(true); // マグネットコントロールのフラグ const [pressedKey, setPressedKey] = useState(null); @@ -134,6 +136,7 @@ export default function GameScene() { setBallStates(initialBallState); setBombStates(initialBombState); setMovingBalls({}); + setIsStartModalOpen(true); setIsCharging(false); setShowRoundStart(false); setShotCount(0); @@ -453,6 +456,7 @@ export default function GameScene() { allowMagnet={ball.shootable && magnetEnabled} onSelect={ ball.shootable && + !isStartModalOpen && !isCharging && !isStrikeAnimating && !anyBallMoving && @@ -514,6 +518,23 @@ export default function GameScene() { remainingBalls={remainingTargetBalls} /> )} + {isStartModalOpen && ( + setIsStartModalOpen(false)} + /> + )} + {!isStartModalOpen && ( + + )} {bombExploded && (

From 2e3886d7ad0cd375c6d29d6332a397e4dfedb52d Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:26:47 +0900 Subject: [PATCH 13/24] =?UTF-8?q?=E3=82=AE=E3=83=9F=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=81=AE=E8=AA=AC=E6=98=8E=E3=83=97=E3=83=AD=E3=83=91=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=92level=E3=81=AB=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 6 ++++++ src/gamescene/index.tsx | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index ce74442..b3e94d9 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -49,6 +49,7 @@ export type LevelConfig = { name: string; shotLimit: number; description: string; + gimmic?: string; cueBallId: string; portals?: PortalConfig[]; table?: { @@ -136,6 +137,7 @@ export const LEVELS: LevelConfig[] = [ id: "level3", name: "Level 3 - Ice Floor", description: "氷の床で4球を8打以内に落とす", + gimmic: "氷の床のため、滑りやすくなっています", shotLimit: 15, cueBallId: "poolballs0", table: { @@ -186,6 +188,8 @@ export const LEVELS: LevelConfig[] = [ id: "level4", name: "Level 4 - Switch Gate", description: "3球を10打以内に落とす", + gimmic: + "動くスイッチに強くぶつけると、ポケットが1分間開きます\n開いたすきを狙ってボールをすべて落としてください", shotLimit: 15, cueBallId: "poolballs0", gate: { @@ -223,6 +227,8 @@ export const LEVELS: LevelConfig[] = [ id: "level5", name: "Level 5 - Bomb!", description: "爆弾を避けて2球を5打以内に落とす", + gimmic: + "爆弾に触ると爆発してゲームオーバーになります。\n爆弾に触らないように気を付けよう!", shotLimit: 5, cueBallId: "poolballs0", bombs: [{ id: "bomb0", position: [0.2, 0.2, -0.5] }], diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index a32f42b..a4112c6 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -521,7 +521,9 @@ export default function GameScene() { {isStartModalOpen && ( setIsStartModalOpen(false)} /> )} From 5fe653117a2bfaa42cfe12dee120f5f124eb3e56 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:30:08 +0900 Subject: [PATCH 14/24] =?UTF-8?q?levels=E3=81=AEdescription=E3=81=A8gimmic?= =?UTF-8?q?=E3=81=AE=E6=96=87=E7=AB=A0=E3=81=AE=E5=86=85=E5=AE=B9=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index b3e94d9..1b67853 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -96,7 +96,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level2", name: "Level 2", - description: "3球を7打以内に落とす", + description: "3球を15打以内に落とす", shotLimit: 15, cueBallId: "poolballs0", balls: [ @@ -136,7 +136,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level3", name: "Level 3 - Ice Floor", - description: "氷の床で4球を8打以内に落とす", + description: "氷の床で4球を15打以内に落とす", gimmic: "氷の床のため、滑りやすくなっています", shotLimit: 15, cueBallId: "poolballs0", @@ -187,9 +187,9 @@ export const LEVELS: LevelConfig[] = [ { id: "level4", name: "Level 4 - Switch Gate", - description: "3球を10打以内に落とす", + description: "スイッチを起動させ3球を15打以内に落とす", gimmic: - "動くスイッチに強くぶつけると、ポケットが1分間開きます\n開いたすきを狙ってボールをすべて落としてください", + "動いているスイッチに球を強くぶつけると、\nポケットが1分間開きます\n開いたすきを狙ってボールをすべて落としてください", shotLimit: 15, cueBallId: "poolballs0", gate: { From 021dab0c50fef057b0b1696027feeeb72dff81f1 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Wed, 6 May 2026 16:53:29 +0900 Subject: [PATCH 15/24] =?UTF-8?q?merge=20conflict=E8=A7=A3=E6=B6=88?= =?UTF-8?q?=E6=99=82=E3=81=AE=E3=83=9F=E3=82=B9=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/components/Ball.tsx | 36 ------------------------------- 1 file changed, 36 deletions(-) diff --git a/src/gamescene/components/Ball.tsx b/src/gamescene/components/Ball.tsx index 35e428a..de072e8 100644 --- a/src/gamescene/components/Ball.tsx +++ b/src/gamescene/components/Ball.tsx @@ -278,42 +278,6 @@ export function Ball({ }); } - if (processedFloors) { - processedFloors.forEach((floor) => { - const dx = p[0] - floor.position[0]; - const dz = p[2] - floor.position[2]; - - // 事前計算済みの値を使ってローカル座標に変換 - const localX = dx * floor.cosAngle - dz * floor.sinAngle; - const localZ = dx * floor.sinAngle + dz * floor.cosAngle; - - const isInside = - Math.abs(localX) <= floor.halfWidth && - Math.abs(localZ) <= floor.halfLength; - const wasInside = floorsOnRef.current.has(floor.id); - - if (isInside && !wasInside) { - // 床に入った瞬間、速度とスピンを完全に上書きする - const dir = floor.normalizedDir; - - // 速度(velocity)を強制上書き。strength を直接のスピードとして扱う - api.velocity.set(dir.x * floor.strength, 0, dir.z * floor.strength); - - // 直進後に変なカーブを描かないように、ボールの回転(スピン)をリセット - api.angularVelocity.set(0, 0, 0); - floorsOnRef.current.add(floor.id); - - if (dashAudioRef.current) { - dashAudioRef.current.currentTime = 0; - void dashAudioRef.current.play(); - } - } else if (!isInside && wasInside) { - // 床から出た - floorsOnRef.current.delete(floor.id); - } - }); - } - if (hasPocketed.current) return; if (p[1] <= POCKET_Y_THRESHOLD) { From d0b9a25896282804095ceb89f779e2533f97721c Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:53:54 +0900 Subject: [PATCH 16/24] =?UTF-8?q?levels=E3=81=AEdescription=E3=81=AE?= =?UTF-8?q?=E3=83=9C=E3=83=BC=E3=83=AB=E6=95=B0=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index 1b67853..1574ded 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -96,7 +96,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level2", name: "Level 2", - description: "3球を15打以内に落とす", + description: "5球を15打以内に落とす", shotLimit: 15, cueBallId: "poolballs0", balls: [ @@ -136,7 +136,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level3", name: "Level 3 - Ice Floor", - description: "氷の床で4球を15打以内に落とす", + description: "氷の床で6球を15打以内に落とす", gimmic: "氷の床のため、滑りやすくなっています", shotLimit: 15, cueBallId: "poolballs0", From 0f8f1695466ecdefcda3382c2f51ee76dd7e2a2e Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 15:06:35 +0900 Subject: [PATCH 17/24] =?UTF-8?q?level2,=20level3=E3=81=AE=E3=83=9C?= =?UTF-8?q?=E3=83=BC=E3=83=AB=E3=81=AE=E9=85=8D=E7=BD=AE=E3=82=92=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 47 ++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index 65d3ee8..fddf2aa 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -3,6 +3,8 @@ import poolballs1 from "@/assets/ballTexture/poolballs1.png"; import poolballs2 from "@/assets/ballTexture/poolballs2.png"; import poolballs3 from "@/assets/ballTexture/poolballs3.png"; import poolballs4 from "@/assets/ballTexture/poolballs4.png"; +import poolballs5 from "@/assets/ballTexture/poolballs5.png"; +import poolballs6 from "@/assets/ballTexture/poolballs6.png"; import tableIce from "@/assets/tableTexture/tableIce.svg"; import { OFFSET_Y, @@ -103,15 +105,8 @@ export const LEVELS: LevelConfig[] = [ id: "level2", name: "Level 2", description: "3球を7打以内に落とす", - shotLimit: 7, + shotLimit: 15, cueBallId: "poolballs0", - portals: [ - { - entry: [-0.25, 0, -0.45], - exit: [0.5, 0, 0.55], - radius: 0.12, - }, - ], balls: [ { id: "poolballs0", @@ -122,25 +117,35 @@ export const LEVELS: LevelConfig[] = [ { id: "poolballs1", textureUrl: poolballs1, - position: [0.12, 0.2, 0], + position: [0.25, 0.2, -0.1], }, { id: "poolballs2", textureUrl: poolballs2, - position: [0.25, 0.2, 0], + position: [0.25, 0.2, 0.1], }, { id: "poolballs3", textureUrl: poolballs3, position: [0.38, 0.2, 0], }, + { + id: "poolballs4", + textureUrl: poolballs4, + position: [0.5, 0.2, 0.5], + }, + { + id: "poolballs5", + textureUrl: poolballs5, + position: [0.5, 0.2, -0.5], + }, ], }, { id: "level3", name: "Level 3 - Ice Floor", description: "氷の床で4球を8打以内に落とす", - shotLimit: 8, + shotLimit: 15, cueBallId: "poolballs0", table: { clothTextureUrl: tableIce, @@ -151,28 +156,38 @@ export const LEVELS: LevelConfig[] = [ { id: "poolballs0", textureUrl: poolballs0, - position: [-0.8, 0.2, 0], + position: [0, 0.2, 0], shootable: true, }, { id: "poolballs1", textureUrl: poolballs1, - position: [0.15, 0.2, -0.2], + position: [0.2, 0.2, -0.07], }, { id: "poolballs2", textureUrl: poolballs2, - position: [0.32, 0.2, 0], + position: [-0.2, 0.2, 0.07], }, { id: "poolballs3", textureUrl: poolballs3, - position: [0.15, 0.2, 0.2], + position: [0.2, 0.2, 0.07], }, { id: "poolballs4", textureUrl: poolballs4, - position: [0.52, 0.2, 0], + position: [-0.2, 0.2, -0.07], + }, + { + id: "poolballs5", + textureUrl: poolballs5, + position: [0.3, 0.2, 0], + }, + { + id: "poolballs6", + textureUrl: poolballs6, + position: [-0.3, 0.2, 0], }, ], }, From 07369bb488098a94468825c23c35395032844414 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 15:24:42 +0900 Subject: [PATCH 18/24] =?UTF-8?q?=E3=83=9E=E3=82=B0=E3=83=8D=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=82=B3=E3=83=B3=E3=83=88=E3=83=AD=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=A8=E5=85=A5=E5=8A=9B?= =?UTF-8?q?=E6=96=B9=E5=90=91=E3=81=AEUI=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/index.tsx | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index 0f3e73c..17a14cc 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -104,6 +104,29 @@ export default function GameScene() { const [ballStates, setBallStates] = useState>({}); const [bombStates, setBombStates] = useState>({}); const [magnetEnabled] = useState(true); // マグネットコントロールのフラグ + const [pressedKey, setPressedKey] = useState(null); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const key = e.key.toLowerCase(); + if (key === "a" || key === "d") { + setPressedKey(key); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + const key = e.key.toLowerCase(); + if (key === "a" || key === "d") { + setPressedKey(null); + } + }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, []); + const ballPositionsRef = useRef>({}); const gameEndedRef = useRef(false); const hasSeenMovementSinceShotRef = useRef(false); @@ -509,6 +532,23 @@ export default function GameScene() { {isCharging && ( )} + {magnetEnabled && ( +

+
+ ← +
+
+ 🧲 +
+
+ → +
+
+ )}
); } From 838689d509ae9b01d86b02d779d2c11db4fe42eb Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 15:57:27 +0900 Subject: [PATCH 19/24] =?UTF-8?q?=E9=81=8A=E3=81=B3=E6=96=B9=E8=AA=AC?= =?UTF-8?q?=E6=98=8E=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 16 ++++- package.json | 1 + src/pages/HowToPlayModal.tsx | 123 +++++++++++++++++++++++++++++++++++ src/pages/MenuScreen.tsx | 29 +++++++-- src/pages/howToPlay.tsx | 0 5 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 src/pages/HowToPlayModal.tsx create mode 100644 src/pages/howToPlay.tsx diff --git a/package-lock.json b/package-lock.json index 13a669c..77c2df5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@react-three/cannon": "^6.6.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", + "lucide-react": "^1.14.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.1", @@ -1874,6 +1875,15 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/maath": { "version": "0.10.8", "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", @@ -1958,9 +1968,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index e4fbe4e..e3c7a27 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@react-three/cannon": "^6.6.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", + "lucide-react": "^1.14.0", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.1", diff --git a/src/pages/HowToPlayModal.tsx b/src/pages/HowToPlayModal.tsx new file mode 100644 index 0000000..9bd652e --- /dev/null +++ b/src/pages/HowToPlayModal.tsx @@ -0,0 +1,123 @@ +import { X } from "lucide-react"; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +export function HowToPlayModal({ isOpen, onClose }: Props) { + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ 遊び方 +

+ +
+ + {/* Content */} +
+ {/* Basic Rule */} +
+

+ Basic Rules +

+

+ 白い球(キューボール)を打ち、ステージ上のすべての的球を6つのポケットのいずれかに落とすとクリアとなります。 + + ※各ステージには「打数制限(SHOT + LIMIT)」があります。制限内に全てのボールを落としましょう。 + +

+
+ + {/* Controls */} +
+

+ Controls +

+
+
+

+ 視点操作 (Mouse) +

+
    +
  • + + 右クリック + Drag: + {" "} + 平行移動 +
  • +
  • + + 左クリック + Drag: + {" "} + 視点回転 +
  • +
  • + Scroll:{" "} + 拡大・縮小 +
  • +
+
+
+

+ キュー注目時 (Keyboard) +

+
    +
  • + + W / S: + {" "} + 上下回転 +
  • +
  • + + A / D: + {" "} + 左右回転 +
  • +
  • + ※精密な角度調整が可能です +
  • +
+
+
+
+ + {/* Gimmicks */} +
+

+ Gimmicks +

+

+ ステージによっては特殊なギミックが登場します。 + 障害物の動きを見極め、反動や隙間を利用して戦略的にクリアを目指してください。 +

+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/pages/MenuScreen.tsx b/src/pages/MenuScreen.tsx index 512692f..e2655c0 100644 --- a/src/pages/MenuScreen.tsx +++ b/src/pages/MenuScreen.tsx @@ -1,7 +1,12 @@ +import { HelpCircle } from "lucide-react"; +import { useState } from "react"; import { Link } from "react-router-dom"; import { LEVELS } from "@/gamescene/constants/levels"; +import { HowToPlayModal } from "./HowToPlayModal"; export function MenuScreen() { + const [isHowToPlayOpen, setIsHowToPlayOpen] = useState(false); + return (
@@ -11,10 +16,21 @@ export function MenuScreen() {

BILLIARDS

-

- 指定打数以内で全ての的球を落とすソロモードです。まずはLevel - 1に挑戦してください。 -

+ +
+

+ 指定打数以内で全ての的球を落とすソロモードです。まずはLevel + 1に挑戦してください。 +

+ +
{LEVELS.map((level) => ( @@ -42,6 +58,11 @@ export function MenuScreen() { ))}
+ + setIsHowToPlayOpen(false)} + />
); } diff --git a/src/pages/howToPlay.tsx b/src/pages/howToPlay.tsx new file mode 100644 index 0000000..e69de29 From 7ec96ed048e17321add6e4efaac1e4655d98aa18 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:15:07 +0900 Subject: [PATCH 20/24] =?UTF-8?q?=E3=82=B9=E3=83=86=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E9=96=8B=E5=A7=8B=E6=99=82=E3=81=AE=E3=83=A2=E3=83=BC=E3=83=80?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/gimmicDocs/StartModal.tsx | 39 +++++++++++++++++++++++++ src/gamescene/index.tsx | 21 +++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/gamescene/gimmicDocs/StartModal.tsx diff --git a/src/gamescene/gimmicDocs/StartModal.tsx b/src/gamescene/gimmicDocs/StartModal.tsx new file mode 100644 index 0000000..1a857fb --- /dev/null +++ b/src/gamescene/gimmicDocs/StartModal.tsx @@ -0,0 +1,39 @@ +type Props = { + title: string; + description: string; + onClose: () => void; +}; + +/** + * ステージ開始時に表示されるギミック説明用のモーダル。 + * UIフレームは共通化し、内容はプロップスで受け取る。 + */ +export function StartModal({ title, description, onClose }: Props) { + return ( +
+
+
+ MISSION DETAILS +
+

+ {title} +

+
+ {description.split("\n").map((line, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: simple static text +

+ {line} +

+ ))} +
+ +
+
+ ); +} diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index 17a14cc..c945d69 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -27,6 +27,7 @@ import { StartBanner } from "./components/StartBanner"; import { TrajectoryLineRaycast } from "./components/TrajectoryLineRaycast"; import { getLevelConfig } from "./constants/levels"; import { BALL_RADIUS, calcStrikeDuration } from "./constants/physics"; +import { StartModal } from "./gimmicDocs/StartModal"; import { findCueRespawnPosition } from "./utils/cueRespawn"; type BallState = { @@ -103,6 +104,7 @@ export default function GameScene() { const [pendingShotResolution, setPendingShotResolution] = useState(false); const [ballStates, setBallStates] = useState>({}); const [bombStates, setBombStates] = useState>({}); + const [isStartModalOpen, setIsStartModalOpen] = useState(true); const [magnetEnabled] = useState(true); // マグネットコントロールのフラグ const [pressedKey, setPressedKey] = useState(null); @@ -135,6 +137,7 @@ export default function GameScene() { setBallStates(initialBallState); setBombStates(initialBombState); setMovingBalls({}); + setIsStartModalOpen(true); setIsCharging(false); setShowRoundStart(false); setShotCount(0); @@ -455,6 +458,7 @@ export default function GameScene() { allowMagnet={ball.shootable && magnetEnabled} onSelect={ ball.shootable && + !isStartModalOpen && !isCharging && !isStrikeAnimating && !anyBallMoving && @@ -522,6 +526,23 @@ export default function GameScene() { remainingBalls={remainingTargetBalls} /> )} + {isStartModalOpen && ( + setIsStartModalOpen(false)} + /> + )} + {!isStartModalOpen && ( + + )} {bombExploded && (

From 1d038eff3e0d6534d77bdae87d85b9d2f466241a Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:26:47 +0900 Subject: [PATCH 21/24] =?UTF-8?q?=E3=82=AE=E3=83=9F=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=81=AE=E8=AA=AC=E6=98=8E=E3=83=97=E3=83=AD=E3=83=91=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=92level=E3=81=AB=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 6 ++++++ src/gamescene/index.tsx | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index fddf2aa..fc35971 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -57,6 +57,7 @@ export type LevelConfig = { name: string; shotLimit: number; description: string; + gimmic?: string; cueBallId: string; portals?: PortalConfig[]; table?: { @@ -145,6 +146,7 @@ export const LEVELS: LevelConfig[] = [ id: "level3", name: "Level 3 - Ice Floor", description: "氷の床で4球を8打以内に落とす", + gimmic: "氷の床のため、滑りやすくなっています", shotLimit: 15, cueBallId: "poolballs0", table: { @@ -195,6 +197,8 @@ export const LEVELS: LevelConfig[] = [ id: "level4", name: "Level 4 - Switch Gate", description: "3球を10打以内に落とす", + gimmic: + "動くスイッチに強くぶつけると、ポケットが1分間開きます\n開いたすきを狙ってボールをすべて落としてください", shotLimit: 15, cueBallId: "poolballs0", gate: { @@ -232,6 +236,8 @@ export const LEVELS: LevelConfig[] = [ id: "level5", name: "Level 5 - Bomb!", description: "爆弾を避けて2球を5打以内に落とす", + gimmic: + "爆弾に触ると爆発してゲームオーバーになります。\n爆弾に触らないように気を付けよう!", shotLimit: 5, cueBallId: "poolballs0", bombs: [{ id: "bomb0", position: [0.2, 0.2, -0.5] }], diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index c945d69..00cc261 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -529,7 +529,9 @@ export default function GameScene() { {isStartModalOpen && ( setIsStartModalOpen(false)} /> )} From 6d3e70fb1ae426ca60396a16f6c81d9de9745e98 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:30:08 +0900 Subject: [PATCH 22/24] =?UTF-8?q?levels=E3=81=AEdescription=E3=81=A8gimmic?= =?UTF-8?q?=E3=81=AE=E6=96=87=E7=AB=A0=E3=81=AE=E5=86=85=E5=AE=B9=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index fc35971..4779bb3 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -105,7 +105,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level2", name: "Level 2", - description: "3球を7打以内に落とす", + description: "3球を15打以内に落とす", shotLimit: 15, cueBallId: "poolballs0", balls: [ @@ -145,7 +145,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level3", name: "Level 3 - Ice Floor", - description: "氷の床で4球を8打以内に落とす", + description: "氷の床で4球を15打以内に落とす", gimmic: "氷の床のため、滑りやすくなっています", shotLimit: 15, cueBallId: "poolballs0", @@ -196,9 +196,9 @@ export const LEVELS: LevelConfig[] = [ { id: "level4", name: "Level 4 - Switch Gate", - description: "3球を10打以内に落とす", + description: "スイッチを起動させ3球を15打以内に落とす", gimmic: - "動くスイッチに強くぶつけると、ポケットが1分間開きます\n開いたすきを狙ってボールをすべて落としてください", + "動いているスイッチに球を強くぶつけると、\nポケットが1分間開きます\n開いたすきを狙ってボールをすべて落としてください", shotLimit: 15, cueBallId: "poolballs0", gate: { From 9e67170ea9f077d20593a86af3086fbe629c41bb Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 16:53:54 +0900 Subject: [PATCH 23/24] =?UTF-8?q?levels=E3=81=AEdescription=E3=81=AE?= =?UTF-8?q?=E3=83=9C=E3=83=BC=E3=83=AB=E6=95=B0=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gamescene/constants/levels.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gamescene/constants/levels.ts b/src/gamescene/constants/levels.ts index 4779bb3..48836af 100644 --- a/src/gamescene/constants/levels.ts +++ b/src/gamescene/constants/levels.ts @@ -105,7 +105,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level2", name: "Level 2", - description: "3球を15打以内に落とす", + description: "5球を15打以内に落とす", shotLimit: 15, cueBallId: "poolballs0", balls: [ @@ -145,7 +145,7 @@ export const LEVELS: LevelConfig[] = [ { id: "level3", name: "Level 3 - Ice Floor", - description: "氷の床で4球を15打以内に落とす", + description: "氷の床で6球を15打以内に落とす", gimmic: "氷の床のため、滑りやすくなっています", shotLimit: 15, cueBallId: "poolballs0", From c20db9d9b40298c9b82227a8022bfaf818576f19 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Wed, 6 May 2026 17:11:13 +0900 Subject: [PATCH 24/24] =?UTF-8?q?=E7=A3=81=E7=9F=B3=E3=81=A8=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=95=E3=82=A9=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=82=92?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 10 ++++++++++ package.json | 1 + src/gamescene/index.tsx | 10 ++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77c2df5..32b4070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "lucide-react": "^1.14.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.1", "three": "^0.183.2" }, @@ -2033,6 +2034,15 @@ "react": "^19.2.4" } }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-router": { "version": "7.13.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", diff --git a/package.json b/package.json index e3c7a27..7bbff81 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lucide-react": "^1.14.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.1", "three": "^0.183.2" }, diff --git a/src/gamescene/index.tsx b/src/gamescene/index.tsx index 00cc261..b481249 100644 --- a/src/gamescene/index.tsx +++ b/src/gamescene/index.tsx @@ -9,6 +9,8 @@ import { useRef, useState, } from "react"; +import { IoInformationCircleOutline } from "react-icons/io5"; +import { PiMagnetFill } from "react-icons/pi"; import { useNavigate, useParams } from "react-router-dom"; import billiardHallHdr from "../assets/backgroundHDR/billiard_hall_1k.hdr"; import { AccelerationFloor } from "./components/AccelerationFloor"; @@ -539,10 +541,10 @@ export default function GameScene() { )} {bombExploded && ( @@ -562,8 +564,8 @@ export default function GameScene() { > ←

-
- 🧲 +
+