@@ -542,7 +594,7 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
) : (
@@ -561,10 +613,12 @@ function ShopPanel({
state,
onBuy,
onBuyDeco,
+ highlightType,
}: {
state: GameState;
onBuy: (def: BuildDef) => void;
onBuyDeco: (key: string) => void;
+ highlightType?: string | null;
}) {
return (
@@ -617,7 +671,7 @@ function ShopPanel({
@@ -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}
@@ -1133,10 +1191,12 @@ function AdoptModal({
candidates,
onPick,
onClose,
+ highlightPick,
}: {
candidates: Cat[];
onPick: (cat: Cat) => void;
onClose: () => void;
+ highlightPick?: boolean;
}) {
return (
onPick(cat)}
- className="nature-btn mt-auto py-2 text-sm"
+ className={"nature-btn mt-auto py-2 text-sm" + (highlightPick ? " tut-highlight" : "")}
>
이 아이로 할래요
diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx
index 27d9acb..6f59b05 100644
--- a/src/components/game/VillageView.tsx
+++ b/src/components/game/VillageView.tsx
@@ -137,11 +137,13 @@ export default function VillageView({
placing,
onPlace,
onCancelPlace,
+ tutHighlight,
}: {
onBuildingClick?: (id: string) => void;
placing?: PlacingState | null;
onPlace?: (i: number, j: number) => void;
onCancelPlace?: () => void;
+ tutHighlight?: string | null; // 건물 id 또는 "hall" — 노란 점선 펄스
}) {
const canvasRef = useRef(null);
const wrapRef = useRef(null);
@@ -165,6 +167,8 @@ export default function VillageView({
const onCancelPlaceRef = useRef(onCancelPlace);
onCancelPlaceRef.current = onCancelPlace;
const hoverTileRef = useRef<{ i: number; j: number } | null>(null);
+ const tutHighlightRef = useRef(tutHighlight);
+ tutHighlightRef.current = tutHighlight;
// 전체화면 스케일 (컨테이너 크기에 맞춤)
useEffect(() => {
@@ -754,6 +758,42 @@ export default function VillageView({
sprites.sort((a, b) => a.y - b.y);
for (const s of sprites) s.draw();
+ // --- 튜토리얼 하이라이트: 대상 건물 둘레 노란 점선 펄스 ---
+ const tut = tutHighlightRef.current;
+ if (tut) {
+ const targetPos =
+ tut === "hall"
+ ? HALL_TILE
+ : state.buildings.find((b) => b.id === tut)?.pos;
+ const targetType =
+ tut === "hall"
+ ? "hall"
+ : state.buildings.find((b) => b.id === tut)?.type;
+ if (targetPos && targetType) {
+ const fp = footprintOf(targetType);
+ const grow = 6 + Math.sin(now / 300) * 5; // 커졌다 작아졌다
+ const a = vertex(targetPos.i, targetPos.j);
+ const b = vertex(targetPos.i + fp.w, targetPos.j);
+ const c = vertex(targetPos.i + fp.w, targetPos.j + fp.h);
+ const d = vertex(targetPos.i, targetPos.j + fp.h);
+ ctx.save();
+ ctx.beginPath();
+ ctx.moveTo(a.x, a.y - grow);
+ ctx.lineTo(b.x + grow, b.y);
+ ctx.lineTo(c.x, c.y + grow);
+ ctx.lineTo(d.x - grow, d.y);
+ ctx.closePath();
+ ctx.setLineDash([10, 7]);
+ ctx.lineDashOffset = -now / 40; // 행진하는 점선
+ ctx.strokeStyle = "#facc15";
+ ctx.lineWidth = 3.5;
+ ctx.shadowColor = "rgba(250,204,21,0.8)";
+ ctx.shadowBlur = 10 + Math.sin(now / 300) * 5;
+ ctx.stroke();
+ ctx.restore();
+ }
+ }
+
// --- 배치 모드 오버레이 (고스트 + 유효/무효 하이라이트) ---
if (placing) {
// 배치 가능한 앵커 타일 은은하게 표시