꾸미기 시스템: 바닥 장식 타일 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개의 파일을 삭제
+67 -1
파일 보기
@@ -17,6 +17,7 @@ import {
BUILD_DEFS,
BUILDING_IMG,
catSprite,
DECO_DEFS,
HALL_LEVELS,
KITCHEN_UPGRADE_COST,
mealCap,
@@ -35,6 +36,7 @@ type Panel =
| { type: "hall" }
| { type: "shop" }
| { type: "cats" }
| { type: "deco"; id: string }
| null;
export default function Game() {
@@ -92,11 +94,16 @@ export default function Game() {
<VillageView
onBuildingClick={(id) => {
if (id === "hall") setPanel({ type: "hall" });
else if (id.startsWith("deco:")) setPanel({ type: "deco", id: id.slice(5) });
else setPanel({ type: "building", id });
}}
placing={placing}
onPlace={(i, j) => {
if (!placing) return;
if (placing.deco) {
run(() => engine.placeDeco(placing.deco!, { i, j }));
return; // 연속 배치 (ESC/취소로 종료)
}
const error = placing.moveId
? run(() => engine.moveBuilding(placing.moveId!, { i, j }))
: run(() => engine.build(placing.type as BuildDef["type"], { i, j }));
@@ -108,7 +115,9 @@ export default function Game() {
{/* ── 배치 모드 힌트 ── */}
{placing && (
<div className="wood-plank font-jua absolute bottom-6 left-1/2 z-40 flex -translate-x-1/2 items-center gap-3 px-5 py-2.5 text-sm">
📍 {placing.moveId ? "옮길" : "배치할"} 위치를 클릭하세요
📍 {placing.deco
? "꾸밀 타일을 클릭하세요 (연속 배치 가능)"
: `${placing.moveId ? "옮길" : "배치할"} 위치를 클릭하세요`}
{placing.type === "dock" && " (부두는 물가에만)"}
<button
onClick={() => setPlacing(null)}
@@ -209,6 +218,10 @@ export default function Game() {
setPanel(null);
setPlacing({ type: def.type, aptitude: def.aptitude });
}}
onBuyDeco={(key) => {
setPanel(null);
setPlacing({ deco: key });
}}
/>
</SidePanel>
)}
@@ -240,6 +253,29 @@ export default function Game() {
/>
</SidePanel>
)}
{panel?.type === "deco" && (() => {
const deco = state.decors?.find((d) => d.id === panel.id);
const def = deco ? DECO_DEFS.find((x) => x.key === deco.kind) : undefined;
if (!deco || !def) return null;
return (
<SidePanel title={`🌿 ${def.name}`} onClose={() => setPanel(null)}>
<div className="space-y-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={def.img} alt={def.name} className="mx-auto h-24 object-contain" />
<p className="text-center text-sm text-[#6f5c39]">마을을 꾸미는 바닥 장식이에요.</p>
<button
onClick={() => {
run(() => engine.removeDeco(deco.id));
setPanel(null);
}}
className="wood-plank w-full py-2 text-sm hover:brightness-110"
>
🗑 치우기
</button>
</div>
</SidePanel>
);
})()}
{panel?.type === "cats" && (
<SidePanel title={`🐾 우리 고양이들 (${state.cats.length}/${state.hall.capacity})`} onClose={() => setPanel(null)}>
<div className="space-y-3">
@@ -395,15 +431,18 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
function ShopPanel({
state,
onBuy,
onBuyDeco,
}: {
state: GameState;
onBuy: (def: BuildDef) => void;
onBuyDeco: (key: string) => void;
}) {
return (
<div className="space-y-3">
<p className="text-xs text-[#8a7551]">
구매 후 마을의 원하는 타일을 클릭해 배치합니다. 비용은 배치 확정 시 차감돼요.
</p>
<h3 className="font-jua text-sm text-[#8a7551]">🏗️ 건물</h3>
{BUILD_DEFS.map((def) => {
const built = state.buildings.some((b) => b.type === def.type);
const hallOk = state.hall.level >= def.reqHallLv;
@@ -455,6 +494,33 @@ function ShopPanel({
</div>
);
})}
<h3 className="font-jua mt-4 text-sm text-[#8a7551]">🌿 꾸미기 (여러 개 배치 가능)</h3>
<div className="grid grid-cols-3 gap-2">
{DECO_DEFS.map((def) => {
const affordable = Object.entries(def.cost).every(
([key, amount]) =>
state.resources[key as keyof GameState["resources"]] >= (amount ?? 0),
);
return (
<button
key={def.key}
onClick={() => onBuyDeco(def.key)}
disabled={!affordable}
className="card-nature flex flex-col items-center gap-1 p-2 hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-40"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={def.img} alt={def.name} className="h-12 w-12 object-contain" />
<span className="font-jua text-[12px] text-[#3f2c14]">{def.name}</span>
<span className="font-num text-[10px] text-[#8a6316]">
{Object.entries(def.cost)
.map(([k, v]) => `${RES_EMOJI[k]} ${v}`)
.join(" ")}
</span>
</button>
);
})}
</div>
</div>
);
}