치지직 앱으로 연동하고, 연동 확인된 ID만 접속할 수 있도록 수정, 개발용으로는 채널ID를 수동으로 접속할 수 있도록 변경.
최초 추천 고정 배치 추가.
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import Dice from './dice';
|
||||
import ChatFeedPanel, { type ChatFeedItem } from './chat-feed';
|
||||
import { Chessman } from './chessman';
|
||||
import ChessmanPiece from './chessman-piece';
|
||||
import { applyLandEvent, type PlaneCell } from './cell-event';
|
||||
import { getAssets } from './asset-storage';
|
||||
import { SHOW_DEBUG_UI } from '@/lib/ui/debug';
|
||||
import {
|
||||
buildPlaneCells,
|
||||
cellColorFor,
|
||||
@@ -29,12 +30,15 @@ export default function DicePlane({
|
||||
onConsumeDice,
|
||||
chatFeed = [],
|
||||
showChatFeed = false,
|
||||
settingsButton = null,
|
||||
}: {
|
||||
requireDiceQueue?: boolean;
|
||||
diceQueue?: number;
|
||||
onConsumeDice?: (count?: number) => Promise<{ ok: boolean; error?: string }>;
|
||||
chatFeed?: ChatFeedItem[];
|
||||
showChatFeed?: boolean;
|
||||
/** 판 안쪽 왼쪽 아래(둘레 칸 밖)에 배치할 설정 버튼 */
|
||||
settingsButton?: ReactNode;
|
||||
}) {
|
||||
const [boardReady, setBoardReady] = useState(false);
|
||||
const [planeSize, setPlaneSize] = useState<{ width: number; height: number }>({
|
||||
@@ -57,7 +61,9 @@ export default function DicePlane({
|
||||
const [landedKey, setLandedKey] = useState(0);
|
||||
const [statusText, setStatusText] = useState('시계방향');
|
||||
const [islandTurnsLeft, setIslandTurnsLeft] = useState(0);
|
||||
const [pickingDestination, setPickingDestination] = useState(false);
|
||||
const moveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingRollRef = useRef(0);
|
||||
|
||||
// SSR/초기 렌더에서 랜덤 판을 쓰지 않고, 브라우저에 저장된 설정을 로드
|
||||
useEffect(() => {
|
||||
@@ -72,6 +78,7 @@ export default function DicePlane({
|
||||
setLandedLabel(null);
|
||||
setStatusText('시계방향');
|
||||
setIslandTurnsLeft(0);
|
||||
setPickingDestination(false);
|
||||
setDicePlaneArray(cells);
|
||||
setBoardReady(true);
|
||||
}, []);
|
||||
@@ -104,6 +111,15 @@ export default function DicePlane({
|
||||
setStatusText(man.facing === 1 ? '시계방향' : '반시계방향');
|
||||
setIslandTurnsLeft(man.islandTurnsLeft);
|
||||
|
||||
if (result?.chooseDestination) {
|
||||
pendingRollRef.current = roll;
|
||||
setPickingDestination(true);
|
||||
showBanner(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setPickingDestination(false);
|
||||
|
||||
if (result?.teleportTo != null) {
|
||||
setChessmanIndex(result.teleportTo);
|
||||
setHopKey((k) => k + 1);
|
||||
@@ -116,6 +132,19 @@ export default function DicePlane({
|
||||
[showBanner],
|
||||
);
|
||||
|
||||
const onCellPick = useCallback(
|
||||
(index: number) => {
|
||||
if (!pickingDestination || moving) return;
|
||||
const man = chessmanRef.current;
|
||||
man.teleport(index);
|
||||
setChessmanIndex(index);
|
||||
setHopKey((k) => k + 1);
|
||||
setPickingDestination(false);
|
||||
applyLanding(index, pendingRollRef.current);
|
||||
},
|
||||
[pickingDestination, moving, applyLanding],
|
||||
);
|
||||
|
||||
const animateMove = useCallback(
|
||||
(steps: number, roll: number) => {
|
||||
const amount = Math.max(0, Math.trunc(steps));
|
||||
@@ -153,6 +182,7 @@ export default function DicePlane({
|
||||
if (roll === 0) return;
|
||||
|
||||
setLandedLabel(null);
|
||||
setPickingDestination(false);
|
||||
const man = chessmanRef.current;
|
||||
|
||||
if (man.isOnIsland) {
|
||||
@@ -217,6 +247,16 @@ export default function DicePlane({
|
||||
return (
|
||||
<div
|
||||
key={plane.id}
|
||||
role={pickingDestination ? 'button' : undefined}
|
||||
tabIndex={pickingDestination ? 0 : undefined}
|
||||
onClick={() => onCellPick(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (!pickingDestination) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onCellPick(index);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
gridColumn: plane.x + 1,
|
||||
gridRow: plane.y + 1,
|
||||
@@ -238,12 +278,16 @@ export default function DicePlane({
|
||||
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,
|
||||
: pickingDestination
|
||||
? '0 0 0 2px rgba(125, 211, 252, 0.85)'
|
||||
: 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,
|
||||
zIndex: pickingDestination ? 8 : isLanded ? 4 : undefined,
|
||||
cursor: pickingDestination ? 'pointer' : undefined,
|
||||
pointerEvents: pickingDestination ? 'auto' : undefined,
|
||||
textShadow:
|
||||
showContent && plane.imageSrc
|
||||
? '0 1px 3px rgba(0,0,0,0.85)'
|
||||
@@ -276,13 +320,34 @@ export default function DicePlane({
|
||||
>
|
||||
<Dice
|
||||
onRolled={onDiceRolled}
|
||||
disabled={moving}
|
||||
disabled={moving || pickingDestination}
|
||||
requireDiceQueue={requireDiceQueue}
|
||||
diceQueue={diceQueue}
|
||||
onConsumeDice={onConsumeDice}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settingsButton && (
|
||||
<div
|
||||
style={{
|
||||
// 둘레 칸 밖: 안쪽 영역 왼쪽 아래
|
||||
gridColumn: 2,
|
||||
gridRow: Math.max(3, planeSize.height - 1),
|
||||
zIndex: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
padding: 4,
|
||||
boxSizing: 'border-box',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ pointerEvents: 'auto' }}>{settingsButton}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showChatFeed && (
|
||||
<div
|
||||
style={{
|
||||
@@ -336,10 +401,12 @@ export default function DicePlane({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pointer-events-none absolute bottom-3 left-3 z-20 rounded bg-black/65 px-3 py-1 text-sm text-white">
|
||||
{statusText}
|
||||
{islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''}
|
||||
</div>
|
||||
{SHOW_DEBUG_UI && (
|
||||
<div className="pointer-events-none absolute bottom-3 left-3 z-20 rounded bg-black/65 px-3 py-1 text-sm text-white">
|
||||
{statusText}
|
||||
{islandTurnsLeft > 0 ? ` · 무인도 ${islandTurnsLeft}턴` : ''}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{landedLabel && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user