From fc6eb5fce10d37dcb406fedb3dd2e3a9f8514a8a Mon Sep 17 00:00:00 2001 From: kimyu Date: Mon, 7 Sep 2026 20:50:21 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A3=BC=EB=A3=A8=EB=A7=88=EB=B8=94=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=A4=91.=20-=20=EC=8B=9C=EC=9E=91=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=83=9D=EC=84=B1=20-=20=EA=B0=81=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=B3=84=20=EC=B6=94=EA=B0=80=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=EC=8B=9C=20=EC=82=AC=EC=9A=A9=EB=90=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20-?= =?UTF-8?q?=20Rapier,=20Three.js=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=82=AC=EC=9C=84.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game/cell-event.ts | 88 ++++++ app/game/chessman-piece.tsx | 51 +++ app/game/chessman.ts | 93 ++++++ app/game/dice.tsx | 280 ++++++++++++++++ app/game/diceplane.tsx | 306 +++++++++++++++--- app/game/page.tsx | 6 +- app/globals.css | 29 ++ next.config.ts | 7 +- package-lock.json | 615 +++++++++++++++++++++++++++++++++++- package.json | 11 +- public/chessman/default.png | Bin 0 -> 4004 bytes public/dice/1.png | Bin 0 -> 2286 bytes public/dice/2.png | Bin 0 -> 2520 bytes public/dice/3.png | Bin 0 -> 2747 bytes public/dice/4.png | Bin 0 -> 2743 bytes public/dice/5.png | Bin 0 -> 2983 bytes public/dice/6.png | Bin 0 -> 3072 bytes 17 files changed, 1430 insertions(+), 56 deletions(-) create mode 100644 app/game/cell-event.ts create mode 100644 app/game/chessman-piece.tsx create mode 100644 app/game/chessman.ts create mode 100644 app/game/dice.tsx create mode 100644 public/chessman/default.png create mode 100644 public/dice/1.png create mode 100644 public/dice/2.png create mode 100644 public/dice/3.png create mode 100644 public/dice/4.png create mode 100644 public/dice/5.png create mode 100644 public/dice/6.png 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 0000000000000000000000000000000000000000..47308213641064827c4192045d2a891763b70397 GIT binary patch literal 4004 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 K$O|$>?%x5yN|AE_ literal 0 HcmV?d00001 diff --git a/public/dice/1.png b/public/dice/1.png new file mode 100644 index 0000000000000000000000000000000000000000..23c44a03087832429de22b8bb0217a179abbe340 GIT binary patch literal 2286 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 F{{_5<&V2v? literal 0 HcmV?d00001 diff --git a/public/dice/3.png b/public/dice/3.png new file mode 100644 index 0000000000000000000000000000000000000000..46c09d58d11d7a551dd0dd00e69e57643b7fa6cf GIT binary patch literal 2747 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 AtpET3 literal 0 HcmV?d00001 diff --git a/public/dice/5.png b/public/dice/5.png new file mode 100644 index 0000000000000000000000000000000000000000..9e9ad2c950c3ff745a6fcb62c52289f3a806ecbb GIT binary patch literal 2983 zcmeHJdr*^C7C&DCA%Ga5QspHXF$)4JvY@sg`D_sdDYB@;tBXBfLXu>zfb@QGATeOAfoty~3BY<#2=KWIzH9E7`a%r=Rzudn?<6b;K7yjNy07t`)v z6$fHt6UypAY8I=aX7oN?I)7hQh68>1+C$u-S5T4>8^vZ)54^hO^?WyG^RD>1OcQo6 zt{y8%eRefdx>Y#*s_w2P8NS|TI3PBPrAkcUn3qtw{6<#-b!_OULQBl?tT#B$K?{pkM95{_rK=`d|lnM4Q$m;iKXSr=a^Hw7ZQ zYu~wTe1?JsavBhzpCl{dx0}BuhQ;m5j#Ex*TN-(GxoQ)~!{FyuuGSS_&#_nsf7rH# zuibsZ+J7FlAtJ0%+w))nGU<_FdK5&@#^X9w8Z6mZc`Yp4!s}EVJSa|l+rM4#cPuW&Z5?*Xp^g1%)j&rn4SM>gUn8 zYk2f?#+}VG?z-62==7K`!$>wd&E0gEarYSFJ4N2vW35u9`0LN+>tpZhx5)_|jQFFV zuW9{mKSL=qby2#|e+wDbXVIFC9%9a!a;abu__y6_LO^TR!;NXkxqr{&6w0T-Az~xS zP$>m}B9DypZ+D#smNy@wl{t^=4G-ibcPG6tLCKUsNlm?Feuc#WYW&?x=iN`CjjC3l zslyPy^MMulFgL1P(Zjf_ZCtN>@8&|9Lmu8&I^3K{b9@KYF)$*N@ynU{^+B41%BNeCYWCyy@NRMkElj+AVGt+p)}~w z^rf)mG(yz96*{4MGL6ebKFNFLh~3dnbl~yLaFAx6!|2cv;DuB%t%Ak`j=CLbq~Z%3 zJ;UU;6qW=Kl2EE%I-3exmZ9ZnukA)LC-quNRDXSOBXSTRA9LV2^NeS%KzM$}W~eqZ z$twK${)9D)KmU0eXSQ$F^cxc+J}T%PQZT106a5?UK4|fXIHVgKr+xV6m&qwHoj)Il~9UT@A6!ZAx*2Lc&YkJuf$ea8|=715+ zvbKRjz@IGKqh1fp{Z3aV9igbM?2*O-Hgs-PYRO0u8EKgmE@toisorqTBO;@lt3kP~ zB*G|hP-oIHu@;K3C^j8DI4_JaoEEEL!uO?Fp{(jbM{tif4jz>j6oT}hYl!&&Rh4+e z{w`AFbN((;|0j`pL+m%q{ZDEO4}wow>nhz#RdVvjSYS%y*@o^S*^J8JaC^iH@V>{- z)Ts5@YXpJcUb33BFxR&V<@oDXp>NyG%*N?IC`Y^-Ztt1W9a8zb9;-}6nPqhbyshPvXfMwa^S1OhEf)A zguVP8TY?7pmnN5gkZFLHtclG(L><~*>IrhiiSVhx&XbMUF2HqST|!$S;R(VK89(BL zC^uIvZYD&H4dMm^-z()}6X5A?-_-pjA~X6z7&|Y~3Su?)dDqi)HvK=H9B@IhTUnYn zD8bStBDA(8ieJjD_J{L!%3=^aQtyq+D5g|@s8tnMLfhbEajiHNa~^IGVD3-9>5fUP zv?s2wl0b$X_u(hG2Gx^E`>F9((BBhO|B>%Xd~Be5j3!50Otev%R6@jl7XndJ`g*Vf swX23W1H2mmLT)`@`s;gOB;?M$4nEFPT}5fPHGsZ=b-{k;-{WNe8|Vz=zyJUM literal 0 HcmV?d00001 diff --git a/public/dice/6.png b/public/dice/6.png new file mode 100644 index 0000000000000000000000000000000000000000..16091b3a6e3954374243bebbd859edd46407e953 GIT binary patch literal 3072 zcmbW3X;f3!7RPr60tzvpU=buB=mS9^4h*794@9&|g(suPAZ1WSt5umV;1DeqpGC?7 zlt`C~nkY*Q@~p^ZP_#gwih#@k7J@R*qv4&K%W}~#>TACI_dWNVm2>z0?f*IF_S)}Y zE>>9#0L-0sHjV&P6jDKkkSG;v0f21zP8+LzCmZ^j8;^7psMtEBU*D~qFwC-Ar6E^% zf*R_!+R9^6Fc#_mh(0216u75uDx#=W79yN2s6YWr0C@m9d@;mK*_&d!yGGu>pjbx~ zdRE80%UUZoSaoF*W7DPZYb>sn2dw;v$$c+{|A(bB|4=4{zhXVNG%%ZEW7X<1brZ_H zBMecubhlXdWg8QJ&*GF6zny0C{?H7VRXu?%i+)flH~{wIV=JdE?c~zcPF$ddPnNee zNmIXl`K~)i0i#(^yUgW16By;9PRiTggLhbH&_ez!H9RZ?wLY`^`mKT$6l#@ z#9ZQ^m3KrAbW$qCy5Y@HxNmkA6|YixIbrb3bC@yMc_~wj$sLPcl%ozeEpSC<&Pqbp zFi=ajqOZ)Ox+vzJz7}45jOLFgjumP@28RS0bv-isFZ!}CRH@e;tYI?70EE< zKa{}aM;88E`j#<37;K%Q0`K=tJBFOXAa;(%nLP5r#S*U3C3V6qV#E!_-vB7ZnD1l7 zv3i++s}eL|02%-?eRpP&ZR5%)lxc90D7R&yrpygZfMb3wmPOY(Y*R>i>(cM0?4b35iPsL-a~QV! zAb5S2Jk8ri3Jq-8kS&7={1Sa^6rl95M0`o1k@DLi$)NS2#1#%&=ZO#O*9vvDeRG}; ziGgUWXDPxK8{EoOO+6d1X|F<}2c9 zB={z|GLo1S-jVCL%|uTL?HMmDRT-M!o3)@nw@=a~ES)4>Y&!)zU?q zz-G8YZ-_Vs5MhS2)gJmy6QQsu$|{AP^4YV zRdEP|@yk)mQ4S+%xx#3pq<94-*k7St2FeZ;G+tMAAsXNC_lmZ-cj_ z4w9;=rYZa1$2bij22nS%YN zn@!NGjXeY>XxZQoAvGsmo!2h}^5`yc_KRPG*}n*7zu51V#@7{t%6;!2U+D7%|E@Gh zk1l-oiGS?ZJv~pg@;oIJn(!4_f9{eXJCJKTze~KY^uwUMz@F%y6Y@PJzirN@ddmxB zSZ}zJ{xUzfp2*K$cPYdXCp~{TAv{uUkiySTMfk~B&ORrA?NWrL(si{Pv(2x;MUy9J z%I2ohWL}kgSfjrLN@JKTBc9kXalj0nYI7+%9AaIh)kx0@l z^68Gi&*p*-Ro>!00(NYR6joL&6ei_-rvs~hzK5nTCkC|wHLi{SGcYp$NDO?K66pC zgyT_#p6cQ{bV1XW-CNORJvI`L?gylY(s|}yG?0tUWV7(Lduou=S?SPA+$;3uuvRK1 zDf(h04F(yxS@$SlY8oNqHNy}+v?T5-0W{GX@P5`a_gb_S3@G5szS?7!N)e3?$U~77 zRdVC$wxEqI1x`b}n>MsQu%p54m6wIF1MLP0lvB5t6RVaTnA0nw1OmS9=G^N}7fTgD z_deGx;8nYm2IK5=HFFs=(_w&y#qWzeM)fq9pgcS3mD;(Xr2XT8gZQh~7AG(2fN6E% zVz8fkRyXJ87TYo~VRCLn7fhxzuAHwE2k@dS9=TBP_pQD5(S53Tn{V?SxEMY9a0_EU zT~t})xd_WSTGaWfP6sO9dkyGN!e@>a4IQb|2d7y5gU~ema^Qyo;S#((!xP(ZH$SLE zlaeX6eoQ&31j=ppkNx#kHD*T#W*H(U2p_#EZOMQgamOG9!uvlxltFm6EXD@=XbSKy g)g=F4HRb&r_a<3zYS8SE51`-9?e;cz|LL9ZKagD*dH?_b literal 0 HcmV?d00001