꾸미기 시스템: 바닥 장식 타일 9종 (deco.png)

- deco 시트 9분할 (흙마당·바위 터·돌바닥·나무 데크·자갈길·그루터기 쉼터·
  꽃잔디·돌광장·나무길), 원본은 assets-src 보관
- 상점에 🌿 꾸미기 섹션 (3열 그리드, 여러 개 배치 가능)
- 구매 → 배치 모드: 연속 배치 지원 (클릭마다 비용 차감, ESC로 종료),
  같은 타일 재배치 시 교체, 건물·나무·물 타일 차단
- 정사각 탑뷰 아트를 아핀 변환으로 아이소 다이아몬드에 매핑해 바닥에 밀착
- 배치된 장식 클릭 → 패널에서 🗑 치우기 (건물 히트가 우선)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 17:44:47 +09:00
co-authored by Claude Fable 5
부모 1ff8d0b8ca
커밋 a90d9b217f
15개의 변경된 파일과 214개의 추가작업 그리고 10개의 파일을 삭제
+53
파일 보기
@@ -16,6 +16,7 @@ import {
BREEDS,
BUILD_DEFS,
CAT_NAMES,
DECO_DEFS,
DEFAULT_BUILDING_POS,
EAT_THRESHOLD,
EXP_PER_LEVEL,
@@ -227,6 +228,26 @@ export function canPlaceAt(
return true;
}
/** 데코를 놓을 수 있는 타일인가 (잔디 위, 건물·나무·물 제외) */
export function canPlaceDecoAt(s: GameState, i: number, j: number): boolean {
if (!Number.isInteger(i) || !Number.isInteger(j)) return false;
if (i < 0 || j < 0 || i >= BOARD_COLS || j >= BOARD_ROWS) return false;
if (isWaterTile(i)) return false;
if (TREE_TILES.some((t) => t.i === i && t.j === j)) return false;
const hallFp = footprintOf("hall");
if (
i >= HALL_TILE.i && i < HALL_TILE.i + hallFp.w &&
j >= HALL_TILE.j && j < HALL_TILE.j + hallFp.h
)
return false;
for (const b of s.buildings) {
const fp = footprintOf(b.type);
if (i >= b.pos.i && i < b.pos.i + fp.w && j >= b.pos.j && j < b.pos.j + fp.h)
return false;
}
return true;
}
/** 현재 해금된 레시피 중 가장 마지막 것 (부엌이 요리할 메뉴) */
export function activeRecipe(s: GameState): RecipeDef {
const kitchenLv = s.buildings.find((b) => b.type === "kitchen")?.level ?? 1;
@@ -269,6 +290,7 @@ export const engine = {
}
for (const b of state.buildings)
b.pos ??= { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 5 }) };
state.decors ??= [];
// 보드 개편(크기·풋프린트 변경) 시 건물 위치를 새 기본값으로 재배치
if (state.boardVersion !== BOARD_VERSION) {
for (const b of state.buildings)
@@ -525,6 +547,37 @@ export const engine = {
return null;
},
/** 꾸미기 배치 (같은 타일의 기존 장식은 교체) */
placeDeco(kind: string, pos: { i: number; j: number }): string | null {
if (!state) return null;
const def = DECO_DEFS.find((d) => d.key === kind);
if (!def) return null;
if (!canPlaceDecoAt(state, pos.i, pos.j)) return "그곳에는 놓을 수 없습니다";
const missing = Object.entries(def.cost).filter(
([key, amount]) =>
state!.resources[key as keyof GameState["resources"]] < (amount ?? 0),
);
if (missing.length > 0)
return `자원이 부족합니다 (${missing
.map(([k, v]) => `${RES_LABEL[k] ?? k} ${v}`)
.join(", ")})`;
consume(state, def.cost);
state.decors ??= [];
// 같은 타일의 기존 장식 제거 후 배치
state.decors = state.decors.filter(
(d) => !(d.pos.i === pos.i && d.pos.j === pos.j),
);
state.decors.push({ id: nextId("deco"), kind, pos: { ...pos } });
return null;
},
/** 꾸미기 제거 (환불 없음) */
removeDeco(id: string): string | null {
if (!state?.decors) return null;
state.decors = state.decors.filter((d) => d.id !== id);
return null;
},
/** 부엌 증축 Lv1→2: 훈제 생선 해금 조건 + 선반 확장 */
upgradeKitchen(): string | null {
if (!state) return null;