diff --git a/assets-src/buildings/construction.png b/assets-src/buildings/construction.png new file mode 100644 index 0000000..7646cd1 Binary files /dev/null and b/assets-src/buildings/construction.png differ diff --git a/public/assets/buildings/construction.png b/public/assets/buildings/construction.png new file mode 100644 index 0000000..c1f33e7 Binary files /dev/null and b/public/assets/buildings/construction.png differ diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index f0d3ed0..4de9673 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -9,6 +9,7 @@ import { effectiveRate, activeRecipe, fullnessPenalty, + isUnderConstruction, } from "@/lib/game/engine"; import { APTITUDE_LABEL, @@ -408,6 +409,7 @@ function ShopPanel({
{def.name}
{costEntries.map(([k, v]) => `${RES_EMOJI[k]} ${v}`).join(" + ")} + {` · ⏱ ${def.buildTime}초`} {def.reqHallLv > 1 && ` · 회관 Lv${def.reqHallLv}`}
{def.desc}
@@ -470,6 +472,33 @@ function BuildingPanel({ effectiveRate(b, building.aptitude) - effectiveRate(a, building.aptitude), ); + if (isUnderConstruction(building)) { + const def = BUILD_DEFS.find((d) => d.type === building.type); + const total = (def?.buildTime ?? 60) * 1000; + const remainMs = Math.max(0, (building.constructionUntil ?? 0) - Date.now()); + const pct = Math.min(100, ((total - remainMs) / total) * 100); + const remainSec = Math.ceil(remainMs / 1000); + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + 건설 중 +

+ 🔨 열심히 짓는 중이에요… +

+
+
+
+

+ 남은 시간 {Math.floor(remainSec / 60)}:{String(remainSec % 60).padStart(2, "0")} +

+
+ ); + } + return (
{/* 정보 */} diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index 43ce923..a01d499 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { engine, catRate, canPlaceAt } from "@/lib/game/engine"; +import { engine, catRate, canPlaceAt, isUnderConstruction } from "@/lib/game/engine"; import { BOARD_COLS, BOARD_ROWS, @@ -565,17 +565,40 @@ export default function VillageView({ const baseY = y + (fp.h * TH) / 2; const stored = Math.floor(b.stored); const isKitchen = b.type === "kitchen"; + const constructing = isUnderConstruction(b); // 건물 본체 → 정보 패널 hitRectsRef.current.push({ id: b.id, kind: "open", x: x - imgW / 2, y: y - imgW * 0.6, w: imgW, h: imgW * 0.6 + (fp.h * TH) / 2, }); // 배지 → 빠른 회수 (생산 건물만) - if (!isKitchen && stored > 0) + if (!constructing && !isKitchen && stored > 0) hitRectsRef.current.push({ id: b.id, kind: "collect", x: x - 46, y: y - imgW * 0.72, w: 92, h: 26 }); sprites.push({ y, draw: () => { + if (constructing) { + const cImg = getImage(BUILDING_IMG.construction); + if (cImg) { + const h = (cImg.height / cImg.width) * imgW; + ctx.drawImage(cImg, x - imgW / 2, baseY + 4 - h, imgW, h); + } else { + drawEmoji("🏗️", x, y + 8, 42); + } + const remain = Math.max(0, Math.ceil(((b.constructionUntil ?? 0) - Date.now()) / 1000)); + const mm = Math.floor(remain / 60); + const ss = remain % 60; + drawPlankBadge(x, y - imgW * 0.5, `🔨 ${mm}:${String(ss).padStart(2, "0")}`); + ctx.font = `13px ${juaFamily}`; + ctx.textAlign = "center"; + ctx.strokeStyle = "rgba(255,252,238,0.75)"; + ctx.lineWidth = 3; + ctx.strokeText(`${b.name} 건설 중`, x, baseY + 17); + ctx.lineWidth = 1; + ctx.fillStyle = "#4a3418"; + ctx.fillText(`${b.name} 건설 중`, x, baseY + 17); + return; + } const bImg = getImage(BUILDING_IMG[b.type]); if (bImg) { // 건물 일러스트 (자체 잔디 베이스 포함 — 그림자 불필요) diff --git a/src/lib/game/data.ts b/src/lib/game/data.ts index 14f8ace..dc26527 100644 --- a/src/lib/game/data.ts +++ b/src/lib/game/data.ts @@ -25,6 +25,7 @@ export function catSprite(breed: string, frame: CatFrame): string { // 건물 일러스트 (public/assets/buildings/) export const BUILDING_IMG: Record = { + construction: "/assets/buildings/construction.png", hall: "/assets/buildings/hall.png", dock: "/assets/buildings/dock.png", lumberyard: "/assets/buildings/lumberyard.png", @@ -151,18 +152,19 @@ export interface BuildDef { maxSlots: number; cost: Partial>; reqHallLv: number; + buildTime: number; // 건설 소요 시간 (초) desc: string; } export const BUILD_DEFS: BuildDef[] = [ { id: "kitchen_1", type: "kitchen", name: "부엌", emoji: "🍳", - aptitude: "cooking", maxSlots: 2, cost: { wood: 60 }, reqHallLv: 1, + aptitude: "cooking", maxSlots: 2, cost: { wood: 60 }, reqHallLv: 1, buildTime: 60, desc: "요리 적성 고양이가 생선으로 요리를 만들고, 배고픈 고양이들이 알아서 먹으러 옵니다.", }, { id: "quarry_1", type: "quarry", name: "채석장", emoji: "⛏️", - aptitude: "mining", resource: "stone", maxSlots: 3, cost: { wood: 120 }, reqHallLv: 2, + aptitude: "mining", resource: "stone", maxSlots: 3, cost: { wood: 120 }, reqHallLv: 2, buildTime: 120, desc: "채굴 적성 고양이가 돌을 캡니다. 돌은 회관 증축과 중급 건물에 필요합니다.", }, ]; diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts index 362a08a..d6cc3cf 100644 --- a/src/lib/game/engine.ts +++ b/src/lib/game/engine.ts @@ -162,6 +162,11 @@ function passiveFoodMul(cat: Cat): number { return mul; } +/** 건설 중인가 */ +export function isUnderConstruction(b: Building): boolean { + return b.constructionUntil !== undefined && Date.now() < b.constructionUntil; +} + /** 사각 풋프린트 겹침 검사 */ function rectsOverlap( a: { i: number; j: number; w: number; h: number }, @@ -256,6 +261,14 @@ export const engine = { tick(dt: number) { if (!state) return; for (const building of state.buildings) { + // 건설 완료 체크 + if (building.constructionUntil !== undefined) { + if (Date.now() >= building.constructionUntil) { + building.constructionUntil = undefined; + } else { + continue; // 건설 중 — 생산 없음 + } + } let produced = 0; for (const catId of building.assigned) { const cat = state.cats.find((c) => c.id === catId); @@ -354,6 +367,7 @@ export const engine = { pos: { i: pos.i, j: pos.j }, level: 1, maxSlots: def.maxSlots, aptitude: def.aptitude, resource: def.resource, assigned: [], stored: 0, progress: 0, + constructionUntil: Date.now() + def.buildTime * 1000, }); return null; }, @@ -415,7 +429,8 @@ export const engine = { } for (const req of next.reqBuildings ?? []) { const b = state.buildings.find((x) => x.type === req.type); - if (!b || b.level < (req.level ?? 1)) missing.push(req.label); + if (!b || isUnderConstruction(b) || b.level < (req.level ?? 1)) + missing.push(req.label); } if (next.reqCat) { const count = state.cats.filter((c) => c.level >= next.reqCat!.level).length; @@ -434,6 +449,8 @@ export const engine = { const cat = state.cats.find((c) => c.id === catId); const building = state.buildings.find((b) => b.id === buildingId); if (!cat || !building) return null; + if (isUnderConstruction(building)) + return `${building.name}은(는) 아직 건설 중입니다`; if (cat.aptitude[building.aptitude] === 0) return `${cat.name}은(는) ${building.name} 적성이 없습니다`; if (building.assigned.length >= building.maxSlots) diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index eb376b0..df33c89 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -47,6 +47,7 @@ export interface Building { assigned: string[]; // cat id stored: number; // 생산 건물: 회수 대기 산출물 / 부엌: 보관 중인 요리 수 progress?: number; // 부엌: 현재 요리 진행 작업량 + constructionUntil?: number; // 건설 완료 예정 timestamp (없으면 완공 상태) } export interface GameState {