꾸미기 시스템: 바닥 장식 타일 9종 (deco.png)
- deco 시트 9분할 (흙마당·바위 터·돌바닥·나무 데크·자갈길·그루터기 쉼터· 꽃잔디·돌광장·나무길), 원본은 assets-src 보관 - 상점에 🌿 꾸미기 섹션 (3열 그리드, 여러 개 배치 가능) - 구매 → 배치 모드: 연속 배치 지원 (클릭마다 비용 차감, ESC로 종료), 같은 타일 재배치 시 교체, 건물·나무·물 타일 차단 - 정사각 탑뷰 아트를 아핀 변환으로 아이소 다이아몬드에 매핑해 바닥에 밀착 - 배치된 장식 클릭 → 패널에서 🗑 치우기 (건물 히트가 우선) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
새 이슈에서 참조
사용자 차단