건설 시간 시스템: 배치 후 공사장 표시 → 시간 경과 시 완공

- BuildDef.buildTime 추가 (부엌 60초, 채석장 120초), 상점에 ⏱ 표시
- 배치 확정 시 constructionUntil 타임스탬프 설정 (오프라인에도 진행)
- 건설 중: 공사장 일러스트 + 🔨 카운트다운 판자 배지 + "건설 중" 라벨,
  생산·배치 불가, 회관 증축 요건에도 미인정
- 패널: 공사장 이미지 + 진행 바 + 남은 시간 표시
- 완공 시 tick에서 자동 전환

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 16:08:28 +09:00
co-authored by Claude Fable 5
부모 1f858352a0
커밋 c50e12400b
7개의 변경된 파일과 77개의 추가작업 그리고 5개의 파일을 삭제
+4 -2
파일 보기
@@ -25,6 +25,7 @@ export function catSprite(breed: string, frame: CatFrame): string {
// 건물 일러스트 (public/assets/buildings/)
export const BUILDING_IMG: Record<string, string> = {
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<Record<"fish" | "wood" | "stone", number>>;
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: "채굴 적성 고양이가 돌을 캡니다. 돌은 회관 증축과 중급 건물에 필요합니다.",
},
];
+18 -1
파일 보기
@@ -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)
+1
파일 보기
@@ -47,6 +47,7 @@ export interface Building {
assigned: string[]; // cat id
stored: number; // 생산 건물: 회수 대기 산출물 / 부엌: 보관 중인 요리 수
progress?: number; // 부엌: 현재 요리 진행 작업량
constructionUntil?: number; // 건설 완료 예정 timestamp (없으면 완공 상태)
}
export interface GameState {