튜토리얼 하이라이트: 눌러야 할 곳을 노란 점선 펄스로 안내

- DOM 버튼: 입양·상점·구매(부두/벌목장/부엌)·배치·증축하기·입양 선택
  버튼에 점선 아웃라인이 커졌다 작아졌다 (tut-pulse 애니메이션)
- 캔버스 건물: 부두·벌목장·회관 풋프린트 둘레에 행진하는 점선 다이아몬드
  (크기 펄스 + 골드 글로우)
- 단계·패널 상태에 따라 타깃 자동 전환 (상점 열면 구매 버튼으로,
  건물 패널 열면 배치 버튼으로 이동)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 18:06:12 +09:00
co-authored by Claude Fable 5
부모 6225403290
커밋 a4c6764731
3개의 변경된 파일과 126개의 추가작업 그리고 8개의 파일을 삭제
+68 -8
파일 보기
@@ -167,6 +167,38 @@ export default function Game() {
? state.buildings.find((b) => b.id === panel.id)
: undefined;
// 튜토리얼: 지금 눌러야 할 곳 하이라이트 타깃
const tutStep = state.tutorialStep ?? TUTORIAL_DONE;
const tutTarget: string | null = (() => {
if (tutStep >= TUTORIAL_STEPS.length) return null;
const dock = state.buildings.find((b) => b.type === "dock");
const lumber = state.buildings.find((b) => b.type === "lumberyard");
switch (tutStep) {
case 1:
case 4:
return candidates ? "adopt-pick" : "adopt";
case 2:
return panel?.type === "shop" ? "shop-dock" : placing ? null : "shop";
case 3:
if (!dock) return null;
if (panel?.type === "building" && panel.id === dock.id) return "assign";
return `canvas:${dock.id}`;
case 5:
return panel?.type === "shop" ? "shop-lumberyard" : placing ? null : "shop";
case 6:
if (!lumber) return null;
if (panel?.type === "building" && panel.id === lumber.id) return "assign";
return `canvas:${lumber.id}`;
case 7:
return panel?.type === "shop" ? "shop-kitchen" : placing ? null : "shop";
case 8:
return panel?.type === "hall" ? "hall-upgrade" : "canvas:hall";
default:
return null;
}
})();
const tut = (key: string) => (tutTarget === key ? " tut-highlight" : "");
return (
<div className="fixed inset-0 overflow-hidden">
<VillageView
@@ -188,6 +220,9 @@ export default function Game() {
if (!error) setPlacing(null);
}}
onCancelPlace={() => setPlacing(null)}
tutHighlight={
tutTarget?.startsWith("canvas:") ? tutTarget.slice(7) : null
}
/>
{/* ── 배치 모드 힌트 ── */}
@@ -247,7 +282,7 @@ export default function Game() {
onClick={() =>
setPanel(panel?.type === "shop" ? null : { type: "shop" })
}
className="wood-pill font-jua flex items-center gap-1.5 px-4 py-2 text-[15px] hover:brightness-125"
className={"wood-pill font-jua flex items-center gap-1.5 px-4 py-2 text-[15px] hover:brightness-125" + tut("shop")}
>
🛒 상점
</button>
@@ -273,7 +308,7 @@ export default function Game() {
)
}
disabled={state.cats.length >= state.hall.capacity}
className="nature-btn flex items-center gap-1.5 px-4 py-2 text-[15px] disabled:cursor-not-allowed disabled:opacity-50"
className={"nature-btn flex items-center gap-1.5 px-4 py-2 text-[15px] disabled:cursor-not-allowed disabled:opacity-50" + tut("adopt")}
>
😻 입양
</button>
@@ -336,13 +371,20 @@ export default function Game() {
{/* ── 우측 패널 ── */}
{panel?.type === "hall" && (
<SidePanel title={`🏛️ 마을 회관 Lv${state.hall.level}`} onClose={() => setPanel(null)}>
<HallPanel state={state} onUpgrade={() => run(() => engine.upgradeHall())} />
<HallPanel
state={state}
onUpgrade={() => run(() => engine.upgradeHall())}
highlightUpgrade={tutTarget === "hall-upgrade"}
/>
</SidePanel>
)}
{panel?.type === "shop" && (
<SidePanel title="🛒 건물 상점" onClose={() => setPanel(null)}>
<ShopPanel
state={state}
highlightType={
tutTarget?.startsWith("shop-") ? tutTarget.slice(5) : null
}
onBuy={(def) => {
setPanel(null);
setPlacing({ type: def.type, aptitude: def.aptitude });
@@ -371,6 +413,7 @@ export default function Game() {
onUnassign={(catId) => run(() => engine.unassign(catId))}
onCollect={() => run(() => engine.collect(openedBuilding.id))}
onUpgradeKitchen={() => run(() => engine.upgradeKitchen())}
highlightAssign={tutTarget === "assign"}
onMove={() => {
setPanel(null);
setPlacing({
@@ -424,6 +467,7 @@ export default function Game() {
{candidates && (
<AdoptModal
candidates={candidates}
highlightPick={tutTarget === "adopt-pick"}
onPick={(cat) => {
run(() => engine.adoptCandidate(cat));
setCandidates(null);
@@ -480,7 +524,15 @@ function SidePanel({
/* ──────────────── 회관 패널 ──────────────── */
function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => void }) {
function HallPanel({
state,
onUpgrade,
highlightUpgrade,
}: {
state: GameState;
onUpgrade: () => void;
highlightUpgrade?: boolean;
}) {
const next = HALL_LEVELS[state.hall.level + 1];
return (
<div className="space-y-4">
@@ -542,7 +594,7 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
) : (
<button
onClick={onUpgrade}
className="nature-btn w-full py-2 text-sm"
className={"nature-btn w-full py-2 text-sm" + (highlightUpgrade ? " tut-highlight" : "")}
>
🏗️ 증축하기
</button>
@@ -561,10 +613,12 @@ function ShopPanel({
state,
onBuy,
onBuyDeco,
highlightType,
}: {
state: GameState;
onBuy: (def: BuildDef) => void;
onBuyDeco: (key: string) => void;
highlightType?: string | null;
}) {
return (
<div className="space-y-3">
@@ -617,7 +671,7 @@ function ShopPanel({
<button
onClick={() => onBuy(def)}
disabled={!hallOk || !affordable}
className="amber-btn shrink-0 px-3.5 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-40"
className={"amber-btn shrink-0 px-3.5 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-40" + (highlightType === def.type ? " tut-highlight" : "")}
>
{!hallOk ? `회관 Lv${def.reqHallLv}` : !affordable ? "자원 부족" : "구매"}
</button>
@@ -666,6 +720,7 @@ function BuildingPanel({
onCollect,
onUpgradeKitchen,
onMove,
highlightAssign,
}: {
building: Building;
state: GameState;
@@ -674,6 +729,7 @@ function BuildingPanel({
onCollect: () => void;
onUpgradeKitchen: () => void;
onMove: () => void;
highlightAssign?: boolean;
}) {
const isKitchen = building.type === "kitchen";
const recipe = activeRecipe(state);
@@ -896,6 +952,7 @@ function BuildingPanel({
label: "배치",
style: "primary",
disabled: slotsLeft <= 0,
highlight: highlightAssign,
onClick: () => onAssign(cat.id),
}}
/>
@@ -926,6 +983,7 @@ function CatRow({
label: string;
style: "primary" | "danger";
disabled?: boolean;
highlight?: boolean;
onClick: () => void;
};
}) {
@@ -959,7 +1017,7 @@ function CatRow({
action.style === "primary"
? "nature-btn"
: "wood-plank hover:brightness-110"
}`}
}${action.highlight ? " tut-highlight" : ""}`}
>
{action.label}
</button>
@@ -1133,10 +1191,12 @@ function AdoptModal({
candidates,
onPick,
onClose,
highlightPick,
}: {
candidates: Cat[];
onPick: (cat: Cat) => void;
onClose: () => void;
highlightPick?: boolean;
}) {
return (
<div
@@ -1214,7 +1274,7 @@ function AdoptModal({
<button
onClick={() => onPick(cat)}
className="nature-btn mt-auto py-2 text-sm"
className={"nature-btn mt-auto py-2 text-sm" + (highlightPick ? " tut-highlight" : "")}
>
이 아이로 할래요
</button>