주루마블 기본 알고리즘 생성중.

- 시작위치 생성
- 각 위치별 추가 이벤트시 사용될 이벤트 알고리즘
- Rapier, Three.js를 통한 주사위.
This commit is contained in:
kimyu
2026-09-07 20:50:21 +09:00
parent 7608b166b3
commit fc6eb5fce1
17 changed files with 1430 additions and 56 deletions

88
app/game/cell-event.ts Normal file
View File

@@ -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<string, CellEvent> = {
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);
}

View File

@@ -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 (
<div
aria-label="chessman"
style={{
gridColumn: x + 1,
gridRow: y + 1,
zIndex: 5,
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
key={hopKey}
src={src}
alt="chessman"
draggable={false}
className="drinkmarble-chessman-token"
style={{
width: '58%',
height: '58%',
minWidth: 22,
minHeight: 22,
objectFit: 'contain',
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.35))',
animation: hopKey > 0 ? 'drinkmarble-chessman-hop 220ms ease-out' : undefined,
}}
/>
</div>
);
}

93
app/game/chessman.ts Normal file
View File

@@ -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;
}
}

280
app/game/dice.tsx Normal file
View File

@@ -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 (
<>
<CuboidCollider args={[t, h, f]} position={[f + t, h, 0]} />
<CuboidCollider args={[t, h, f]} position={[-(f + t), h, 0]} />
<CuboidCollider args={[f, h, t]} position={[0, h, f + t]} />
<CuboidCollider args={[f, h, t]} position={[0, h, -(f + t)]} />
</>
);
}
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 (
<mesh castShadow>
<boxGeometry args={[1, 1, 1]} />
{textures.map((texture, index) => (
<meshStandardMaterial
key={texture.uuid}
attach={`material-${index}`}
map={texture}
roughness={0.55}
metalness={0.05}
/>
))}
</mesh>
);
}
function DiceSettleWatcher({
diceRef,
rolling,
onSettled,
}: {
diceRef: React.RefObject<RapierRigidBody | null>;
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<RapierRigidBody | null>;
rolling: boolean;
onSettled: (value: number) => void;
}) {
return (
<Canvas
orthographic
camera={{ position: [0, 20, 0.01], zoom: 1 }}
className="h-full w-full"
gl={{ alpha: true }}
>
<FitSquareFloor />
<ambientLight intensity={0.85} />
<directionalLight position={[4, 10, 2]} intensity={0.9} />
<Physics>
<RigidBody type="fixed" colliders={false} position={[0, 0, 0]}>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[FLOOR_SIZE, FLOOR_SIZE]} />
<meshBasicMaterial color="blue" />
</mesh>
<CuboidCollider args={[FLOOR_HALF, 0.05, FLOOR_HALF]} position={[0, -0.05, 0]} />
<InvisibleWalls />
</RigidBody>
<RigidBody
ref={diceRef}
type="dynamic"
position={[0, 4, 0]}
colliders="cuboid"
restitution={0.2}
friction={0.6}
>
<Suspense fallback={null}>
<DiceMesh />
</Suspense>
</RigidBody>
<DiceSettleWatcher diceRef={diceRef} rolling={rolling} onSettled={onSettled} />
</Physics>
</Canvas>
);
}
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<RapierRigidBody>(null);
const [rolling, setRolling] = useState(false);
const [result, setResult] = useState<number | null>(null);
const onSettled = useCallback(
(value: number) => {
setResult(value);
setRolling(false);
onRolled?.(value);
},
[onRolled],
);
const busy = rolling || disabled;
return (
<div className="relative h-full w-full" style={{ containerType: 'size' }}>
<div className="absolute top-0 left-0 aspect-square w-[min(100%,100cqh)]">
<RapierDiceExample diceRef={diceRef} rolling={rolling} onSettled={onSettled} />
<button
type="button"
className="absolute bottom-2 left-2 z-10 rounded bg-black/70 px-3 py-1 text-sm text-white disabled:opacity-50"
disabled={busy}
onClick={() => {
setResult(null);
setRolling(true);
rollDice(diceRef.current);
}}
>
Roll
</button>
<div className="pointer-events-none absolute right-2 bottom-2 z-10 rounded bg-black/70 px-3 py-1 text-sm text-white">
{rolling
? '굴리는 중…'
: disabled
? '이동 중…'
: result != null
? `결과: ${result}`
: '결과: —'}
</div>
</div>
</div>
);
}

View File

@@ -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<number, { type: string; eventId: string }> = {
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<string | null>(null);
const [landedKey, setLandedKey] = useState(0);
const [statusText, setStatusText] = useState('시계방향');
const [islandTurnsLeft, setIslandTurnsLeft] = useState(0);
const moveTimerRef = useRef<ReturnType<typeof setTimeout> | 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 (
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${PLANE_SIZE.width}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${PLANE_SIZE.height}, minmax(0, 1fr))`,
gap: 'clamp(6px, 1vw, 12px)',
padding: 'clamp(8px, 1.2vw, 16px)',
width: '100%',
height: '100%',
boxSizing: 'border-box',
}}
>
{dicePlaneArray.map((plane) => (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${PLANE_SIZE.width}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${PLANE_SIZE.height}, minmax(0, 1fr))`,
gap: 'clamp(6px, 1vw, 12px)',
padding: 'clamp(8px, 1.2vw, 16px)',
width: '100%',
height: '100%',
boxSizing: 'border-box',
}}
>
{dicePlaneArray.map((plane, index) => {
const isStart = index === 0;
const isLanded = !moving && index === chessmanIndex && landedLabel != null;
return (
<div
key={plane.id}
style={{
gridColumn: plane.x + 1,
gridRow: plane.y + 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: cellColor(plane, index),
color: 'white',
borderRadius: 8,
fontSize: 'clamp(11px, 1.6vw, 16px)',
fontWeight: 600,
textAlign: 'center',
padding: 4,
boxShadow: isLanded
? '0 0 0 3px #fde68a, 0 0 18px rgba(253, 224, 71, 0.75)'
: isStart
? 'inset 0 0 0 2px #a7f3d0'
: undefined,
transform: isLanded ? 'scale(1.04)' : undefined,
transition: 'box-shadow 180ms ease, transform 180ms ease',
zIndex: isLanded ? 4 : undefined,
}}
>
{isStart ? '시작' : plane.type}
</div>
);
})}
{chessmanCell && (
<ChessmanPiece x={chessmanCell.x} y={chessmanCell.y} hopKey={hopKey} />
)}
<div
key={plane.id}
style={{
gridColumn: plane.x + 1,
gridRow: plane.y + 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#e11d48',
color: 'white',
borderRadius: 8,
fontSize: 'clamp(11px, 1.6vw, 16px)',
fontWeight: 600,
textAlign: 'center',
padding: 4,
gridColumn: '2 / 4',
gridRow: '2 / 4',
minWidth: 0,
minHeight: 0,
zIndex: 6,
}}
>
{plane.type}
<Dice onRolled={onDiceRolled} disabled={moving} />
</div>
))}
</div>
<div className="pointer-events-none absolute top-3 right-3 z-20 rounded bg-black/65 px-3 py-1 text-sm text-white">
{statusText}
{islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''}
</div>
{landedLabel && (
<div
className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center"
aria-live="polite"
>
<div
key={landedKey}
style={{
padding: 'clamp(12px, 2vw, 28px) clamp(20px, 4vw, 48px)',
borderRadius: 16,
background: 'rgba(15, 23, 42, 0.78)',
color: '#fff7ed',
fontSize: 'clamp(1.6rem, 6vw, 4rem)',
fontWeight: 800,
letterSpacing: '0.04em',
textShadow: '0 4px 24px rgba(0,0,0,0.45)',
boxShadow: '0 12px 40px rgba(0,0,0,0.35)',
animation: 'drinkmarble-landed-pop 420ms ease-out',
maxWidth: '80%',
textAlign: 'center',
}}
>
{landedLabel}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,12 +1,12 @@
'use client';
import DicePlane from './diceplane';
import Dice from './dice';
export default function GamePage() {
return (
<div className="relative h-screen w-screen overflow-hidden">
<DicePlane />
<h1 className="pointer-events-none absolute inset-0 flex items-center justify-center text-4xl font-bold">
Game
</h1>
</div>
);
}

View File

@@ -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);
}
}

View File

@@ -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;

615
package-lock.json generated
View File

@@ -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
}
}
}
}
}

View File

@@ -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",

BIN
public/chessman/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/dice/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/dice/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/dice/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/dice/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/dice/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/dice/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB