튜토리얼 하이라이트: 눌러야 할 곳을 노란 점선 펄스로 안내
- DOM 버튼: 입양·상점·구매(부두/벌목장/부엌)·배치·증축하기·입양 선택 버튼에 점선 아웃라인이 커졌다 작아졌다 (tut-pulse 애니메이션) - 캔버스 건물: 부두·벌목장·회관 풋프린트 둘레에 행진하는 점선 다이아몬드 (크기 펄스 + 골드 글로우) - 단계·패널 상태에 따라 타깃 자동 전환 (상점 열면 구매 버튼으로, 건물 패널 열면 배치 버튼으로 이동) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
@@ -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>
|
||||
|
||||
새 이슈에서 참조
사용자 차단