diff --git a/app/game/cell-event.ts b/app/game/cell-event.ts new file mode 100644 index 0000000..7b9d004 --- /dev/null +++ b/app/game/cell-event.ts @@ -0,0 +1,88 @@ +import type { Chessman } from './chessman'; + +export type PlaneCell = { + id: number; + x: number; + y: number; + /** UI에 보이는 이름 */ + type: string; + /** 효과가 있을 때만 설정. 없으면 착지 시 아무 일도 없음 */ + eventId?: string | null; +}; + +export type TurnContext = { + chessman: Chessman; + board: PlaneCell[]; + roll: number; + landIndex: number; +}; + +export type TurnResult = { + message: string; + /** 비행기 등: 즉시 이동할 칸 */ + teleportTo?: number; +}; + +export interface CellEvent { + readonly id: string; + apply(ctx: TurnContext): TurnResult; +} + +/** 무인도: 최대 3턴 대기, 주사위 6이면 즉시 탈출 가능 */ +export class IslandEvent implements CellEvent { + readonly id = 'island'; + + apply(ctx: TurnContext): TurnResult { + ctx.chessman.enterIsland(3, 6); + return { message: '무인도! 3턴 대기 (6이면 탈출)' }; + } +} + +/** 역방향: 진행 방향을 반대로 */ +export class ReverseEvent implements CellEvent { + readonly id = 'reverse'; + + apply(ctx: TurnContext): TurnResult { + const dir = ctx.chessman.reverseDirection(); + return { + message: dir === 1 ? '역방향! 다시 시계방향' : '역방향! 반시계로 진행', + }; + } +} + +/** 비행기: 지정한 칸으로 즉시 이동 */ +export class AirplaneEvent implements CellEvent { + readonly id = 'airplane'; + + constructor(private readonly targetIndex: number) {} + + apply(ctx: TurnContext): TurnResult { + const target = + ((this.targetIndex % ctx.board.length) + ctx.board.length) % ctx.board.length; + ctx.chessman.teleport(target); + const dest = ctx.board[target]; + return { + message: `비행기! → ${dest?.type ?? target}`, + teleportTo: target, + }; + } +} + +export const CELL_EVENTS: Record = { + island: new IslandEvent(), + reverse: new ReverseEvent(), + // 기본 도착지는 시작(0). 판 생성 시 덮어쓸 수 있다. + airplane: new AirplaneEvent(0), +}; + +export function getCellEvent(eventId: string | null | undefined): CellEvent | null { + if (!eventId) return null; + return CELL_EVENTS[eventId] ?? null; +} + +export function applyLandEvent(ctx: TurnContext): TurnResult | null { + const cell = ctx.board[ctx.landIndex]; + const event = getCellEvent(cell?.eventId); + if (!event) return null; + return event.apply(ctx); +} diff --git a/app/game/chessman-piece.tsx b/app/game/chessman-piece.tsx new file mode 100644 index 0000000..543f94c --- /dev/null +++ b/app/game/chessman-piece.tsx @@ -0,0 +1,51 @@ +'use client'; + +const DEFAULT_CHESSMAN_SRC = '/chessman/default.png'; + +/** 주사위판 그리드 위에 올리는 말 표시. public/chessman/*.png 로 커스텀 가능. */ +export default function ChessmanPiece({ + x, + y, + hopKey, + src = DEFAULT_CHESSMAN_SRC, +}: { + x: number; + y: number; + /** 칸이 바뀔 때마다 증가시켜 hop 애니메이션을 다시 재생한다. */ + hopKey: number; + /** public/chessman 하위 이미지 경로. 기본값: /chessman/default.png */ + src?: string; +}) { + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + chessman 0 ? 'drinkmarble-chessman-hop 220ms ease-out' : undefined, + }} + /> +
+ ); +} diff --git a/app/game/chessman.ts b/app/game/chessman.ts new file mode 100644 index 0000000..4a477b3 --- /dev/null +++ b/app/game/chessman.ts @@ -0,0 +1,93 @@ +/** 주사위판 위를 이동하는 말. */ +export class Chessman { + private index: number; + private readonly boardLength: number; + /** 1: 시계방향, -1: 반시계방향 */ + private direction: 1 | -1 = 1; + /** 무인도: 남은 대기 턴, 탈출 가능 눈 */ + private island: { turnsLeft: number; escapeOn: number } | null = null; + + constructor(boardLength: number, startIndex = 0) { + if (boardLength <= 0) { + throw new Error('Chessman boardLength must be positive'); + } + this.boardLength = boardLength; + this.index = ((startIndex % boardLength) + boardLength) % boardLength; + } + + get position(): number { + return this.index; + } + + get facing(): 1 | -1 { + return this.direction; + } + + get isOnIsland(): boolean { + return this.island != null; + } + + get islandTurnsLeft(): number { + return this.island?.turnsLeft ?? 0; + } + + reverseDirection(): 1 | -1 { + this.direction = this.direction === 1 ? -1 : 1; + return this.direction; + } + + enterIsland(turns: number, escapeOn = 6) { + this.island = { turnsLeft: Math.max(1, Math.trunc(turns)), escapeOn }; + } + + clearIsland() { + this.island = null; + } + + /** + * 무인도 상태에서의 주사위 처리. + * - escape: 6(또는 escapeOn)으로 탈출 → 이후 정상 이동 + * - wait: 턴 소모 후 대기 (turnsLeft 0이면 자동 해제, 이번 턴은 이동 없음) + * - free: 이미 무인도 아님 + */ + resolveIslandRoll(roll: number): 'escape' | 'wait' | 'free' { + if (!this.island) return 'free'; + + if (roll === this.island.escapeOn) { + this.island = null; + return 'escape'; + } + + this.island.turnsLeft -= 1; + if (this.island.turnsLeft <= 0) { + this.island = null; + } + return 'wait'; + } + + teleport(index: number): number { + this.index = ((index % this.boardLength) + this.boardLength) % this.boardLength; + return this.index; + } + + moveOne(): number { + return this.move(1); + } + + /** steps칸만큼 현재 방향으로 이동 */ + move(steps: number): number { + const amount = Math.trunc(steps); + if (amount === 0) return this.index; + const delta = amount * this.direction; + this.index = (this.index + delta) % this.boardLength; + if (this.index < 0) this.index += this.boardLength; + return this.index; + } + + reset(startIndex = 0): number { + this.index = ((startIndex % this.boardLength) + this.boardLength) % this.boardLength; + this.direction = 1; + this.island = null; + return this.index; + } +} diff --git a/app/game/dice.tsx b/app/game/dice.tsx new file mode 100644 index 0000000..ab5ef89 --- /dev/null +++ b/app/game/dice.tsx @@ -0,0 +1,280 @@ +'use client'; + +import { Suspense, useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber'; +import { + Physics, + RigidBody, + CuboidCollider, + type RapierRigidBody, +} from '@react-three/rapier'; +import { + Quaternion, + SRGBColorSpace, + TextureLoader, + Vector3, + type OrthographicCamera, +} from 'three'; + +const FLOOR_SIZE = 10; +const FLOOR_HALF = FLOOR_SIZE / 2; +const WALL_THICKNESS = 0.2; +const WALL_HEIGHT = 10; +const WALL_HALF_T = WALL_THICKNESS / 2; +const WALL_HALF_H = WALL_HEIGHT / 2; + +const STILL_SPEED_SQ = 0.05; +const STILL_FRAMES = 18; + +// boxGeometry material 순서: +X, -X, +Y, -Y, +Z, -Z +const DICE_FACE_TEXTURES = [ + '/dice/2.png', // +X + '/dice/5.png', // -X + '/dice/1.png', // +Y + '/dice/6.png', // -Y + '/dice/3.png', // +Z + '/dice/4.png', // -Z +] as const; + +// 결과 판정: 로컬 법선이 월드 +Y에 가장 가까운 면 +const DICE_FACES = [ + { value: 1, localNormal: new Vector3(0, 1, 0) }, + { value: 6, localNormal: new Vector3(0, -1, 0) }, + { value: 2, localNormal: new Vector3(1, 0, 0) }, + { value: 5, localNormal: new Vector3(-1, 0, 0) }, + { value: 3, localNormal: new Vector3(0, 0, 1) }, + { value: 4, localNormal: new Vector3(0, 0, -1) }, +]; + +const _quat = new Quaternion(); +const _normal = new Vector3(); +const WORLD_UP = new Vector3(0, 1, 0); + +function readTopFace(body: RapierRigidBody): number { + const r = body.rotation(); + _quat.set(r.x, r.y, r.z, r.w); + + let bestValue = 1; + let bestDot = -Infinity; + for (const face of DICE_FACES) { + _normal.copy(face.localNormal).applyQuaternion(_quat); + const dot = _normal.dot(WORLD_UP); + if (dot > bestDot) { + bestDot = dot; + bestValue = face.value; + } + } + return bestValue; +} + +function FitSquareFloor() { + const { camera, size } = useThree(); + + useLayoutEffect(() => { + const cam = camera as OrthographicCamera; + cam.left = -FLOOR_HALF; + cam.right = FLOOR_HALF; + cam.top = FLOOR_HALF; + cam.bottom = -FLOOR_HALF; + cam.near = 0.1; + cam.far = 50; + cam.updateProjectionMatrix(); + }, [camera, size.width, size.height]); + + return null; +} + +function InvisibleWalls() { + const t = WALL_HALF_T; + const h = WALL_HALF_H; + const f = FLOOR_HALF; + + return ( + <> + + + + + + ); +} + +function DiceMesh() { + const textures = useLoader(TextureLoader, [...DICE_FACE_TEXTURES]); + + useLayoutEffect(() => { + for (const texture of textures) { + texture.colorSpace = SRGBColorSpace; + texture.anisotropy = 8; + texture.needsUpdate = true; + } + }, [textures]); + + return ( + + + {textures.map((texture, index) => ( + + ))} + + ); +} + +function DiceSettleWatcher({ + diceRef, + rolling, + onSettled, +}: { + diceRef: React.RefObject; + rolling: boolean; + onSettled: (value: number) => void; +}) { + const stillFrames = useRef(0); + const reported = useRef(false); + + useFrame(() => { + const body = diceRef.current; + if (!body || !rolling) { + stillFrames.current = 0; + reported.current = false; + return; + } + + const lv = body.linvel(); + const av = body.angvel(); + const speedSq = + lv.x * lv.x + lv.y * lv.y + lv.z * lv.z + av.x * av.x + av.y * av.y + av.z * av.z; + + if (speedSq < STILL_SPEED_SQ || body.isSleeping()) { + stillFrames.current += 1; + if (stillFrames.current >= STILL_FRAMES && !reported.current) { + reported.current = true; + onSettled(readTopFace(body)); + } + } else { + stillFrames.current = 0; + } + }); + + return null; +} + +function RapierDiceExample({ + diceRef, + rolling, + onSettled, +}: { + diceRef: React.RefObject; + rolling: boolean; + onSettled: (value: number) => void; +}) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function rollDice(body: RapierRigidBody | null) { + if (!body) return; + + body.wakeUp(); + body.applyImpulse( + { x: (Math.random() - 0.5) * 8, y: 4 + Math.random() * 4, z: (Math.random() - 0.5) * 8 }, + true, + ); + body.applyTorqueImpulse( + { x: (Math.random() - 0.5) * 6, y: (Math.random() - 0.5) * 6, z: (Math.random() - 0.5) * 6 }, + true, + ); +} + +export default function Dice({ + onRolled, + disabled = false, +}: { + onRolled?: (value: number) => void; + disabled?: boolean; +}) { + const diceRef = useRef(null); + const [rolling, setRolling] = useState(false); + const [result, setResult] = useState(null); + + const onSettled = useCallback( + (value: number) => { + setResult(value); + setRolling(false); + onRolled?.(value); + }, + [onRolled], + ); + + const busy = rolling || disabled; + + return ( +
+
+ + +
+ {rolling + ? '굴리는 중…' + : disabled + ? '이동 중…' + : result != null + ? `결과: ${result}` + : '결과: —'} +
+
+
+ ); +} diff --git a/app/game/diceplane.tsx b/app/game/diceplane.tsx index c4728ea..466c94d 100644 --- a/app/game/diceplane.tsx +++ b/app/game/diceplane.tsx @@ -1,77 +1,307 @@ 'use client'; -import { useState } from 'react'; - -type PlaneCell = { id: number; x: number; y: number; type: string }; +import { useCallback, useEffect, useRef, useState } from 'react'; +import Dice from './dice'; +import { Chessman } from './chessman'; +import ChessmanPiece from './chessman-piece'; +import { + applyLandEvent, + type PlaneCell, +} from './cell-event'; const PLANE_SIZE = { width: 9, height: 7 } as const; +const STEP_MS = 280; const CELL_TYPES = ['한잔', '안주', '모두한잔', '두잔', '벌칙1', '벌칙2'] as const; +/** 특수 칸 배치 (28칸 기준 인덱스) */ +const SPECIAL_CELLS: Record = { + 7: { type: '무인도', eventId: 'island' }, + 14: { type: '역방향', eventId: 'reverse' }, + 21: { type: '비행기', eventId: 'airplane' }, // → 시작(0) +}; + function randomType() { return CELL_TYPES[Math.floor(Math.random() * CELL_TYPES.length)]; } +function cellLabel(cell: PlaneCell | undefined, index: number) { + if (!cell) return ''; + return index === 0 ? '시작' : cell.type; +} + +function cellColor(cell: PlaneCell, index: number) { + if (index === 0) return '#059669'; + switch (cell.eventId) { + case 'island': + return '#0f766e'; + case 'reverse': + return '#7c3aed'; + case 'airplane': + return '#0284c7'; + default: + return '#e11d48'; + } +} + /** 시계방향 둘레 칸. 모서리는 한 번만 넣어서 9x7 기준 28칸이 된다. */ -function buildPerimeter(width: number, height: number): PlaneCell[] { +function buildPerimeter(width: number, height: number, assignType: () => string): PlaneCell[] { const cells: PlaneCell[] = []; let id = 0; for (let x = 0; x < width; x++) { - cells.push({ id: id++, x, y: 0, type: randomType() }); + cells.push({ id: id++, x, y: 0, type: assignType() }); } for (let y = 1; y < height; y++) { - cells.push({ id: id++, x: width - 1, y, type: randomType() }); + cells.push({ id: id++, x: width - 1, y, type: assignType() }); } for (let x = width - 2; x >= 0; x--) { - cells.push({ id: id++, x, y: height - 1, type: randomType() }); + cells.push({ id: id++, x, y: height - 1, type: assignType() }); } for (let y = height - 2; y >= 1; y--) { - cells.push({ id: id++, x: 0, y, type: randomType() }); + cells.push({ id: id++, x: 0, y, type: assignType() }); + } + + if (cells[0]) { + cells[0].type = '시작'; + cells[0].eventId = null; + } + + for (const [indexText, special] of Object.entries(SPECIAL_CELLS)) { + const index = Number(indexText); + if (cells[index]) { + cells[index].type = special.type; + cells[index].eventId = special.eventId; + } } return cells; } export default function DicePlane() { - const [dicePlaneArray] = useState(() => - buildPerimeter(PLANE_SIZE.width, PLANE_SIZE.height), + const [dicePlaneArray, setDicePlaneArray] = useState(() => + buildPerimeter(PLANE_SIZE.width, PLANE_SIZE.height, () => 'none'), + ); + const chessmanRef = useRef(new Chessman(28)); + const boardRef = useRef(dicePlaneArray); + const [chessmanIndex, setChessmanIndex] = useState(0); + const [hopKey, setHopKey] = useState(0); + const [moving, setMoving] = useState(false); + const [landedLabel, setLandedLabel] = useState(null); + const [landedKey, setLandedKey] = useState(0); + const [statusText, setStatusText] = useState('시계방향'); + const [islandTurnsLeft, setIslandTurnsLeft] = useState(0); + const moveTimerRef = useRef | null>(null); + + useEffect(() => { + const cells = buildPerimeter(PLANE_SIZE.width, PLANE_SIZE.height, randomType); + chessmanRef.current = new Chessman(cells.length, 0); + boardRef.current = cells; + setChessmanIndex(0); + setHopKey(0); + setLandedLabel(null); + setStatusText('시계방향'); + setIslandTurnsLeft(0); + setDicePlaneArray(cells); + }, []); + + useEffect(() => { + boardRef.current = dicePlaneArray; + }, [dicePlaneArray]); + + useEffect(() => { + return () => { + if (moveTimerRef.current) clearTimeout(moveTimerRef.current); + }; + }, []); + + const showBanner = useCallback((message: string) => { + setLandedLabel(message); + setLandedKey((k) => k + 1); + }, []); + + const applyLanding = useCallback( + (landIndex: number, roll: number) => { + const man = chessmanRef.current; + const result = applyLandEvent({ + chessman: man, + board: boardRef.current, + roll, + landIndex, + }); + + setStatusText(man.facing === 1 ? '시계방향' : '반시계방향'); + setIslandTurnsLeft(man.islandTurnsLeft); + + if (result?.teleportTo != null) { + setChessmanIndex(result.teleportTo); + setHopKey((k) => k + 1); + showBanner(result.message); + return; + } + + showBanner(result?.message ?? cellLabel(boardRef.current[landIndex], landIndex)); + }, + [showBanner], ); + const animateMove = useCallback( + (steps: number, roll: number) => { + const amount = Math.max(0, Math.trunc(steps)); + if (amount === 0) { + setMoving(false); + return; + } + + setMoving(true); + let remaining = amount; + + const stepOnce = () => { + const next = chessmanRef.current.moveOne(); + setChessmanIndex(next); + setHopKey((k) => k + 1); + remaining -= 1; + + if (remaining > 0) { + moveTimerRef.current = setTimeout(stepOnce, STEP_MS); + } else { + moveTimerRef.current = null; + setMoving(false); + applyLanding(next, roll); + } + }; + + stepOnce(); + }, + [applyLanding], + ); + + const onDiceRolled = useCallback( + (value: number) => { + const roll = Math.max(0, Math.trunc(value)); + if (roll === 0) return; + + setLandedLabel(null); + const man = chessmanRef.current; + + if (man.isOnIsland) { + const outcome = man.resolveIslandRoll(roll); + setIslandTurnsLeft(man.islandTurnsLeft); + if (outcome === 'wait') { + showBanner( + man.isOnIsland + ? `무인도 대기 (남은 ${man.islandTurnsLeft}턴)` + : '무인도 3턴 종료!', + ); + return; + } + // escape with 6 + showBanner('무인도 탈출!'); + } + + animateMove(roll, roll); + }, + [animateMove, showBanner], + ); + + const chessmanCell = dicePlaneArray[chessmanIndex] ?? dicePlaneArray[0]; + return ( -
- {dicePlaneArray.map((plane) => ( +
+
+ {dicePlaneArray.map((plane, index) => { + const isStart = index === 0; + const isLanded = !moving && index === chessmanIndex && landedLabel != null; + return ( +
+ {isStart ? '시작' : plane.type} +
+ ); + })} + + {chessmanCell && ( + + )} +
- {plane.type} +
- ))} +
+ +
+ {statusText} + {islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''} +
+ + {landedLabel && ( +
+
+ {landedLabel} +
+
+ )}
); } diff --git a/app/game/page.tsx b/app/game/page.tsx index c131967..90c0432 100644 --- a/app/game/page.tsx +++ b/app/game/page.tsx @@ -1,12 +1,12 @@ +'use client'; + import DicePlane from './diceplane'; +import Dice from './dice'; export default function GamePage() { return (
-

- Game -

); } diff --git a/app/globals.css b/app/globals.css index a2dc41e..5cb8e46 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,3 +24,32 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +@keyframes drinkmarble-chessman-hop { + 0% { + transform: translateY(0) scale(1); + } + 40% { + transform: translateY(-32%) scale(1.12); + } + 100% { + transform: translateY(0) scale(1); + } +} + +@keyframes drinkmarble-landed-pop { + 0% { + opacity: 0; + transform: scale(0.72) translateY(12px); + } + 55% { + opacity: 1; + transform: scale(1.08) translateY(0); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + + diff --git a/next.config.ts b/next.config.ts index e9ffa30..f4e2cd5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,12 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // localhost가 아닌 IP로 개발 서버에 붙을 때 /_next/* (HMR, Rapier, Three 청크 등)가 + // 기본으로 막힌다. 허용하지 않으면 Canvas/물리 스크립트가 로드되지 않아 주사위가 안 보인다. + allowedDevOrigins: [ + "172.30.1.31", // LAN + "175.214.193.201", // 외부 접속 IP + ], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index a72cff1..eb4ed0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,14 @@ "name": "drinkmarble", "version": "0.1.0", "dependencies": { + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", + "@react-three/rapier": "^2.2.0", "next": "16.3.3", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "three": "^0.185.1", + "three.js": "^0.77.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -214,6 +219,14 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -259,6 +272,11 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.19.2.tgz", + "integrity": "sha512-AZHL1jqUF55QJkJyU1yKeh4ImX2J93bVLIezT1+o0FZqTix6O06MOaqpKoJ4MmbDCsoZmwO+qc471/SDMDm2AA==" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1003,6 +1021,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", @@ -1233,6 +1267,107 @@ "node": ">=12.4.0" } }, + "node_modules/@react-three/drei": { + "version": "10.7.8", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.8.tgz", + "integrity": "sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.7.0.tgz", + "integrity": "sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/rapier": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-three/rapier/-/rapier-2.2.0.tgz", + "integrity": "sha512-mVsqbKXlGZoN+XrqdhzFZUQmy8pibEOVzl4k7LC+LHe84bQnYBSagy1Hvbda6bL1PJDdTFyiDiBk5buKFinNIQ==", + "dependencies": { + "@dimforge/rapier3d-compat": "0.19.2", + "suspend-react": "^0.1.3", + "three-stdlib": "^2.35.12" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.4", + "react": "^19", + "three": ">=0.159.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1503,6 +1638,11 @@ "tailwindcss": "4.3.3" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1513,6 +1653,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1540,11 +1685,15 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, "dependencies": { "csstype": "^3.2.2" } @@ -1558,6 +1707,42 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==" + }, + "node_modules/@types/three": { + "version": "0.185.4", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.4.tgz", + "integrity": "sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/three/node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.68.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", @@ -2138,6 +2323,22 @@ "win32" ] }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -2411,6 +2612,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/baseline-browser-mapping": { "version": "2.11.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz", @@ -2422,6 +2642,14 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -2477,6 +2705,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2533,6 +2784,18 @@ "node": ">=6" } }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001810", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", @@ -2603,11 +2866,27 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2620,8 +2899,7 @@ "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -2737,6 +3015,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2758,6 +3044,11 @@ "node": ">=0.10.0" } }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3450,6 +3741,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3689,6 +3985,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3809,6 +4110,30 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.1.tgz", + "integrity": "sha512-DlzIkeBAS9IIQ432k3BUf3HlwbsR0+trB1i2lDdN2gUkNkrehFurh0/48M5c1/EjlDkdGng1gwZIpwyPxvdZ/g==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3818,6 +4143,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4124,6 +4454,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4269,8 +4604,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/iterator.prototype": { "version": "1.1.5", @@ -4289,6 +4623,17 @@ "node": ">= 0.4" } }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4423,6 +4768,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -4714,6 +5067,15 @@ "yallist": "^3.0.2" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4741,6 +5103,19 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5130,7 +5505,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5195,6 +5569,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5204,6 +5583,15 @@ "node": ">= 0.8.0" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5269,6 +5657,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5311,6 +5713,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5562,7 +5972,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5574,7 +5983,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -5665,6 +6073,29 @@ "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5853,6 +6284,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -5872,6 +6311,45 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz", + "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==" + }, + "node_modules/three.js": { + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/three.js/-/three.js-0.77.0.tgz", + "integrity": "sha512-ouSy18ZEiwz+aEsN5psLd7hftf08cyVw/zV1XFYjPhDDnUwIr+bEGryJAy6z3VbSJjuIQUbbM8FtWMZjGdqD+w==" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -5929,6 +6407,33 @@ "node": ">=8.0" } }, + "node_modules/troika-three-text": { + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.5.tgz", + "integrity": "sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.5", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.5.tgz", + "integrity": "sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -5970,6 +6475,41 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6192,11 +6732,36 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6339,6 +6904,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 96e78fa..7c5cd3d 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,20 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 13000", "build": "next build", - "start": "next start", + "start": "next start -p 13000", "lint": "eslint" }, "dependencies": { + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", + "@react-three/rapier": "^2.2.0", "next": "16.3.3", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "three": "^0.185.1", + "three.js": "^0.77.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/public/chessman/default.png b/public/chessman/default.png new file mode 100644 index 0000000..4730821 Binary files /dev/null and b/public/chessman/default.png differ diff --git a/public/dice/1.png b/public/dice/1.png new file mode 100644 index 0000000..23c44a0 Binary files /dev/null and b/public/dice/1.png differ diff --git a/public/dice/2.png b/public/dice/2.png new file mode 100644 index 0000000..24f977c Binary files /dev/null and b/public/dice/2.png differ diff --git a/public/dice/3.png b/public/dice/3.png new file mode 100644 index 0000000..46c09d5 Binary files /dev/null and b/public/dice/3.png differ diff --git a/public/dice/4.png b/public/dice/4.png new file mode 100644 index 0000000..5c0fa35 Binary files /dev/null and b/public/dice/4.png differ diff --git a/public/dice/5.png b/public/dice/5.png new file mode 100644 index 0000000..9e9ad2c Binary files /dev/null and b/public/dice/5.png differ diff --git a/public/dice/6.png b/public/dice/6.png new file mode 100644 index 0000000..16091b3 Binary files /dev/null and b/public/dice/6.png differ