diff --git a/src/app/page.tsx b/src/app/page.tsx index a2484ff..01417fd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,26 +1,5 @@ import Game from "@/components/game/Game"; -import DbStatus from "@/components/DbStatus"; export default function Home() { - return ( -
-
-
- 🏑 고양이 λ§ˆμ„ - λ°©μΉ˜ν˜• 경영 κ²Œμž„ β€” 1단계 -
-
- -
- -
- - -
- ); + return ; } diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 856d9f2..9c3e76b 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -1,9 +1,15 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useGame } from "@/hooks/useGame"; import VillageView from "@/components/game/VillageView"; -import { engine, effectiveRate, activeRecipe, fullnessPenalty } from "@/lib/game/engine"; +import DbStatus from "@/components/DbStatus"; +import { + engine, + effectiveRate, + activeRecipe, + fullnessPenalty, +} from "@/lib/game/engine"; import { APTITUDE_LABEL, BREEDS, @@ -14,36 +20,158 @@ import { PASSIVES, TOOL_IMG, } from "@/lib/game/data"; -import type { AptitudeKey } from "@/lib/game/types"; -import type { Building, Cat, GameState } from "@/lib/game/types"; +import type { AptitudeKey, Building, Cat, GameState } from "@/lib/game/types"; + +type Panel = + | { type: "building"; id: string } + | { type: "hall" } + | { type: "kitchen_lot" } + | { type: "cats" } + | null; export default function Game() { const { state, dispatch } = useGame(); - const [selectedCatId, setSelectedCatId] = useState(null); - const [message, setMessage] = useState(null); + const [panel, setPanel] = useState(null); const [candidates, setCandidates] = useState(null); - - if (!state) { - return ( -
λ§ˆμ„ λΆˆλŸ¬μ˜€λŠ” 쀑…
- ); - } - - const selectedCat = state.cats.find((c) => c.id === selectedCatId) ?? null; + const [message, setMessage] = useState(null); + const msgTimer = useRef | null>(null); const run = (action: () => string | null | void) => { const error = dispatch(action); setMessage(error ?? null); + if (msgTimer.current) clearTimeout(msgTimer.current); + if (error) msgTimer.current = setTimeout(() => setMessage(null), 3500); }; + useEffect(() => () => { + if (msgTimer.current) clearTimeout(msgTimer.current); + }, []); + + if (!state) { + return ( +
+ λ§ˆμ„ λΆˆλŸ¬μ˜€λŠ” 쀑… +
+ ); + } + + const meals = state.buildings + .filter((b) => b.type === "kitchen") + .reduce((sum, b) => sum + Math.floor(b.stored), 0); + const openedBuilding = + panel?.type === "building" + ? state.buildings.find((b) => b.id === panel.id) + : undefined; + return ( -
- setCandidates(engine.generateCandidates())} - onUpgrade={() => run(() => engine.upgradeHall())} +
+ { + if (id === "hall") setPanel({ type: "hall" }); + else if (id === "kitchen_lot") setPanel({ type: "kitchen_lot" }); + else setPanel({ type: "building", id }); + }} /> + {/* ── 상단 HUD ── */} +
+
+ + + 🐱 {state.cats.length}/{state.hall.capacity} + + + 🐟 {Math.floor(state.resources.fish)} + + + πŸͺ΅ {Math.floor(state.resources.wood)} + + + πŸ– {meals} + +
+
+ + +
+
+ + {/* ── λ©”μ‹œμ§€ ν† μŠ€νŠΈ ── */} + {message && ( +
+ ⚠️ {message} +
+ )} + + {/* ── DB μƒνƒœ (μ’Œν•˜λ‹¨ λ―Έλ‹ˆ) ── */} +
+ +
+ + {/* ── 우츑 νŒ¨λ„ ── */} + {panel?.type === "hall" && ( + setPanel(null)}> + run(() => engine.upgradeHall())} /> + + )} + {panel?.type === "kitchen_lot" && ( + setPanel(null)}> + { + run(() => engine.buildKitchen()); + setPanel({ type: "building", id: "kitchen_1" }); + }} + /> + + )} + {openedBuilding && ( + setPanel(null)} + > + run(() => engine.assign(catId, openedBuilding.id))} + onUnassign={(catId) => run(() => engine.unassign(catId))} + onCollect={() => run(() => engine.collect(openedBuilding.id))} + /> + + )} + {panel?.type === "cats" && ( + setPanel(null)}> +
+ {state.cats.map((cat) => ( + + ))} +
+
+ )} + + {/* ── μž…μ–‘ λͺ¨λ‹¬ ── */} {candidates && ( setCandidates(null)} /> )} +
+ ); +} - +/* ──────────────── 곡톡 νŒ¨λ„ μ…Έ ──────────────── */ - {message && ( -

- ⚠️ {message} -

+function SidePanel({ + title, + icon, + onClose, + children, +}: { + title: string; + icon?: string; + onClose: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +/* ──────────────── νšŒκ΄€ νŒ¨λ„ ──────────────── */ + +function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => void }) { + const next = HALL_LEVELS[state.hall.level + 1]; + return ( +
+

+ λ§ˆμ„μ˜ 쀑심. νšŒκ΄€ 레벨이 수용 인원과 κ±΄λ¬ΌΒ·λ ˆμ‹œν”Ό ν•΄κΈˆμ„ κ²°μ •ν•©λ‹ˆλ‹€. +

+
+ ν˜„μž¬ 수용 인원 {state.cats.length}/{state.hall.capacity}마리 +
+ {next ? ( +
+

+ Lv{state.hall.level + 1} 증좕 β†’ 수용 {next.capacity}마리 +

+
    + {Object.entries(next.cost).map(([key, amount]) => { + const have = Math.floor( + state.resources[key as keyof typeof state.resources], + ); + const ok = have >= (amount ?? 0); + return ( +
  • + {ok ? "βœ“" : "Β·"} {key === "wood" ? "πŸͺ΅ λͺ©μž¬" : "🐟 생선"} {have}/{amount} +
  • + ); + })} + {next.reqCat && ( +
  • c.level >= next.reqCat!.level).length >= + next.reqCat.count + ? "text-emerald-600 dark:text-emerald-400" + : "" + } + > + {state.cats.filter((c) => c.level >= next.reqCat!.level).length >= + next.reqCat.count + ? "βœ“" + : "Β·"}{" "} + Lv{next.reqCat.level} 이상 고양이{" "} + {state.cats.filter((c) => c.level >= next.reqCat!.level).length}/ + {next.reqCat.count}마리 +
  • + )} +
+ {next.locked ? ( +

+ πŸ”’ {next.locked} +

+ ) : ( + + )} +
+ ) : ( +

νšŒκ΄€μ΄ 졜고 λ ˆλ²¨μž…λ‹ˆλ‹€.

+ )} +
+ ); +} + +/* ──────────────── λΆ€μ—Œ 곡터 νŒ¨λ„ ──────────────── */ + +function KitchenLotPanel({ wood, onBuild }: { wood: number; onBuild: () => void }) { + const canBuild = wood >= KITCHEN_BUILD_COST.wood; + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + λΆ€μ—Œ +

+ λΆ€μ—Œμ„ μ§€μœΌλ©΄ μš”λ¦¬ 적성 고양이가 μƒμ„ μœΌλ‘œ μš”λ¦¬λ₯Ό λ§Œλ“€κ³ , λ°°κ³ ν”ˆ + 고양이듀이 μ•Œμ•„μ„œ 먹으러 μ˜΅λ‹ˆλ‹€. +

+ +
+ ); +} + +/* ──────────────── 건물 νŒ¨λ„ (정보 + 배치) ──────────────── */ + +function BuildingPanel({ + building, + state, + onAssign, + onUnassign, + onCollect, +}: { + building: Building; + state: GameState; + onAssign: (catId: string) => void; + onUnassign: (catId: string) => void; + onCollect: () => void; +}) { + const isKitchen = building.type === "kitchen"; + const recipe = activeRecipe(state); + const assignedCats = building.assigned + .map((id) => state.cats.find((c) => c.id === id)) + .filter((c): c is Cat => Boolean(c)); + const totalRate = assignedCats.reduce( + (sum, c) => sum + effectiveRate(c, building.aptitude), + 0, + ); + const stored = Math.floor(building.stored); + const slotsLeft = building.maxSlots - assignedCats.length; + // 배치 후보: 이 건물 적성이 μžˆλŠ” λͺ¨λ“  고양이 (λ‹€λ₯Έ κ³³μ—μ„œ μΌν•˜λŠ” 고양이 포함) + const candidates = state.cats + .filter((c) => c.aptitude[building.aptitude] > 0 && c.assignedTo !== building.id) + .sort( + (a, b) => + effectiveRate(b, building.aptitude) - effectiveRate(a, building.aptitude), + ); + + return ( +
+ {/* 정보 */} +
+
+ ν•„μš” 적성: {APTITUDE_LABEL[building.aptitude]} Β· 슬둯{" "} + {assignedCats.length}/{building.maxSlots} +
+ {isKitchen ? ( + <> +
+ {recipe.emoji} {recipe.name} ( + {Object.entries(recipe.input) + .map(([k, v]) => `${k === "fish" ? "생선" : "λͺ©μž¬"} ${v}`) + .join(" + ")} + β†’ ν¬λ§Œλ„ +{recipe.fullness}) +
+
μš”λ¦¬ 속도 {totalRate.toFixed(2)}/초
+
+
+
+ + ) : ( +
생산 속도 {totalRate.toFixed(2)}/초
+ )} +
+ + {/* μ‚°μΆœ/μ„ λ°˜ */} + {isKitchen ? ( +
+ {recipe.emoji} μ„ λ°˜ {stored}/{KITCHEN_MEAL_CAP} β€” λ°°κ³ ν”ˆ 고양이가 μžλ™μœΌλ‘œ λ¨ΉμŠ΅λ‹ˆλ‹€ +
+ ) : ( + )} + {/* 배치된 고양이 */}
-

πŸ—οΈ 생산 건물

-

- 고양이λ₯Ό μ„ νƒν•œ λ’€ 빈자리λ₯Ό 클릭해 λ°°μΉ˜ν•˜μ„Έμš”. μ‚°μΆœλ¬Όμ€ 직접 νšŒμˆ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. -

-
- {state.buildings.map((b) => ( - run(() => engine.assign(catId, b.id))} - onUnassign={(catId) => run(() => engine.unassign(catId))} - onCollect={() => run(() => engine.collect(b.id))} +

+ μΌν•˜λŠ” 쀑 ({assignedCats.length}/{building.maxSlots}) +

+ {assignedCats.length === 0 && ( +

아직 아무도 μ—†μ–΄μš”

+ )} +
+ {assignedCats.map((cat) => ( + onUnassign(cat.id) }} /> ))} - {!state.buildings.some((b) => b.type === "kitchen") && ( - run(() => engine.buildKitchen())} - /> - )}
+ {/* 배치 κ°€λŠ₯ν•œ 고양이 */}
-

- 🐾 우리 고양이듀 ({state.cats.length}/{state.hall.capacity}) -

-
- {state.cats.map((cat) => ( - - setSelectedCatId(cat.id === selectedCatId ? null : cat.id) - } - /> - ))} +

+ 배치 κ°€λŠ₯ν•œ 고양이 +

+ {candidates.length === 0 && ( +

+ {APTITUDE_LABEL[building.aptitude]} 적성이 μžˆλŠ” 고양이가 μ—†μ–΄μš” +

+ )} +
+ {candidates.map((cat) => { + const workplace = state.buildings.find((b) => b.id === cat.assignedTo); + return ( + onAssign(cat.id), + }} + /> + ); + })}
+ {slotsLeft <= 0 && candidates.length > 0 && ( +

+ 슬둯이 가득 μ°Όμ–΄μš” β€” λ¨Όμ € ν•΄μ œν•˜κ±°λ‚˜ 건물을 μ¦μΆ•ν•˜μ„Έμš” +

+ )}
); } -/** νŒ¨μ‹œλΈŒ μŠ€ν‚¬ λ°°μ§€ β€” λ§ˆμš°μŠ€μ˜€λ²„ μ‹œ 상세 툴팁 */ +/** νŒ¨λ„μš© 고양이 ν–‰ */ +function CatRow({ + cat, + aptitude, + tag, + action, +}: { + cat: Cat; + aptitude: AptitudeKey; + tag: string | null; + action: { + label: string; + style: "primary" | "danger"; + disabled?: boolean; + onClick: () => void; + }; +}) { + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+ {cat.name}{" "} + Lv{cat.level} +
+
+ {APTITUDE_LABEL[aptitude]} {cat.aptitude[aptitude]} Β·{" "} + {effectiveRate(cat, aptitude).toFixed(2)}/초 + {fullnessPenalty(cat) < 1 && ( + λ°°κ³ ν”” + )} +
+ {tag &&
{tag}
} +
+ +
+ ); +} + +/* ──────────────── νŒ¨μ‹œλΈŒ λ°°μ§€ (툴팁) ──────────────── */ + function PassiveBadge({ passiveKey }: { passiveKey: string }) { const def = PASSIVES.find((p) => p.key === passiveKey); if (!def) return null; @@ -137,7 +535,82 @@ function PassiveBadge({ passiveKey }: { passiveKey: string }) { ); } -/** μž…μ–‘ λͺ¨λ‹¬ β€” 후보 3마리 쀑 1마리 선택 */ +/* ──────────────── 고양이 μΉ΄λ“œ (μ •λ³΄μš©) ──────────────── */ + +function CatCard({ cat, buildings }: { cat: Cat; buildings: Building[] }) { + const workplace = buildings.find((b) => b.id === cat.assignedTo); + const expNeeded = cat.level * 60; + + return ( +
+
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + + {cat.name}{" "} + Lv{cat.level} + + {cat.breedName} +
+ +
+
+
+ +

+ 🎣 λ‚šμ‹œ {cat.aptitude.fishing} Β· πŸͺ“ 벌λͺ© {cat.aptitude.logging} Β· 🍳 μš”λ¦¬{" "} + {cat.aptitude.cooking} Β· πŸ’ͺ κ·Όλ©΄ {cat.stats.diligence} +

+ +
+
+
+
+ + 🍽️ {Math.floor(cat.status.fullness)} (μ‹μš• {cat.stats.appetite}) + + {fullnessPenalty(cat) < 1 && ( + + λ°°κ³ ν”” + + )} +
+ + {cat.passives.length > 0 && ( +
+ {cat.passives.map((key) => ( + + ))} +
+ )} + +

+ {workplace ? ( + + {workplace.emoji} {workplace.name}μ—μ„œ μΌν•˜λŠ” 쀑 + + ) : ( + μ‰¬λŠ” 쀑 + )} +

+
+ ); +} + +/* ──────────────── μž…μ–‘ λͺ¨λ‹¬ ──────────────── */ + function AdoptModal({ candidates, onPick, @@ -231,273 +704,9 @@ function AdoptModal({ })}

- ν•œ 마리만 선택할 수 μžˆμ–΄μš” Β· λŠ₯λ ₯μΉ˜μ™€ νŒ¨μ‹œλΈŒμ— 마우슀λ₯Ό 올리면 상세 정보가 λ³΄μž…λ‹ˆλ‹€ + ν•œ 마리만 선택할 수 μžˆμ–΄μš” Β· νŒ¨μ‹œλΈŒμ— 마우슀λ₯Ό 올리면 상세 정보가 λ³΄μž…λ‹ˆλ‹€

); } - -function TopBar({ - state, - onAdopt, - onUpgrade, -}: { - state: GameState; - onAdopt: () => void; - onUpgrade: () => void; -}) { - const full = state.cats.length >= state.hall.capacity; - const meals = state.buildings - .filter((b) => b.type === "kitchen") - .reduce((sum, b) => sum + Math.floor(b.stored), 0); - const next = HALL_LEVELS[state.hall.level + 1]; - const nextLabel = next - ? `증좕 Lv${state.hall.level + 1}: λͺ©μž¬ ${next.cost.wood ?? 0}${next.cost.fish ? `, 생선 ${next.cost.fish}` : ""}${next.reqCat ? `, Lv${next.reqCat.level}↑ ${next.reqCat.count}마리` : ""} β†’ 수용 ${next.capacity}마리` - : "졜고 레벨"; - - return ( -
-
- πŸ›οΈ λ§ˆμ„ νšŒκ΄€ Lv{state.hall.level} - 🐱 {state.cats.length}/{state.hall.capacity}마리 - 🐟 {Math.floor(state.resources.fish)} - πŸͺ΅ {Math.floor(state.resources.wood)} - πŸ– {meals} -
- - -
-
-

{nextLabel}

-
- ); -} - -function BuildKitchenCard({ wood, onBuild }: { wood: number; onBuild: () => void }) { - const canBuild = wood >= KITCHEN_BUILD_COST.wood; - return ( -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - λΆ€μ—Œ - λΆ€μ—Œ (미건섀) -
-

- μš”λ¦¬ 적성 고양이λ₯Ό λ°°μΉ˜ν•΄ μƒμ„ μœΌλ‘œ μš”λ¦¬λ₯Ό λ§Œλ“­λ‹ˆλ‹€. λ°°κ³ ν”ˆ 고양이가 μ•Œμ•„μ„œ 먹으러 μ™€μš”. -

- -
- ); -} - -function BuildingCard({ - building, - state, - selectedCat, - onAssign, - onUnassign, - onCollect, -}: { - building: Building; - state: GameState; - selectedCat: Cat | null; - onAssign: (catId: string) => void; - onUnassign: (catId: string) => void; - onCollect: () => void; -}) { - const cats = state.cats; - const isKitchen = building.type === "kitchen"; - const recipe = activeRecipe(state); - const assignedCats = building.assigned - .map((id) => cats.find((c) => c.id === id)) - .filter((c): c is Cat => Boolean(c)); - const totalRate = assignedCats.reduce( - (sum, c) => sum + effectiveRate(c, building.aptitude), - 0, - ); - const stored = Math.floor(building.stored); - const emptySlots = building.maxSlots - assignedCats.length; - const canAssignSelected = - selectedCat !== null && - selectedCat.assignedTo !== building.id && - selectedCat.aptitude[building.aptitude] > 0; - - return ( -
-
-

- {/* eslint-disable-next-line @next/next/no-img-element */} - {building.name} - {building.name} Lv{building.level} -

- - ν•„μš” 적성: {APTITUDE_LABEL[building.aptitude]} - -
- {isKitchen ? ( -

- {recipe.emoji} {recipe.name} ( - {Object.entries(recipe.input) - .map(([k, v]) => `${k === "fish" ? "생선" : "λͺ©μž¬"} ${v}`) - .join(" + ")} - ) Β· μš”λ¦¬ 속도 {totalRate.toFixed(2)}/초 -

- ) : ( -

- 생산 속도 {totalRate.toFixed(2)}/초 -

- )} - -
- {assignedCats.map((cat) => ( - - ))} - {Array.from({ length: emptySlots }).map((_, i) => ( - - ))} -
- - {isKitchen ? ( -
- {recipe.emoji} μ„ λ°˜ {stored}/{KITCHEN_MEAL_CAP} β€” λ°°κ³ ν”ˆ 고양이가 μžλ™μœΌλ‘œ λ¨ΉμŠ΅λ‹ˆλ‹€ -
- ) : ( - - )} -
- ); -} - -function CatCard({ - cat, - buildings, - selected, - onSelect, -}: { - cat: Cat; - buildings: Building[]; - selected: boolean; - onSelect: () => void; -}) { - const workplace = buildings.find((b) => b.id === cat.assignedTo); - const expNeeded = cat.level * 60; - - return ( - - ); -} diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index 3239aa2..e33e752 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { engine, catRate } from "@/lib/game/engine"; import { catSprite, TOOL_IMG, type CatFrame } from "@/lib/game/data"; import type { AptitudeKey } from "@/lib/game/types"; @@ -86,8 +86,14 @@ function workSpot(buildingId: string, slotIndex: number) { }; } -export default function VillageView() { +export default function VillageView({ + onBuildingClick, +}: { + onBuildingClick?: (id: string) => void; +}) { const canvasRef = useRef(null); + const wrapRef = useRef(null); + const [scale, setScale] = useState(1); const animsRef = useRef>(new Map()); const bubblesRef = useRef([]); const toolImagesRef = useRef>(new Map()); @@ -96,8 +102,24 @@ export default function VillageView() { // src ν‚€ 기반 이미지 μΊμ‹œ (ν’ˆμ’…λ³„ μŠ€ν”„λΌμ΄νŠΈ μ§€μ—° λ‘œλ“œ) const imageCacheRef = useRef>(new Map()); const hitRectsRef = useRef< - Array<{ id: string; x: number; y: number; w: number; h: number }> + Array<{ id: string; kind: "collect" | "open"; x: number; y: number; w: number; h: number }> >([]); + const onBuildingClickRef = useRef(onBuildingClick); + onBuildingClickRef.current = onBuildingClick; + + // 전체화면 μŠ€μΌ€μΌ (μ»¨ν…Œμ΄λ„ˆ 크기에 맞좀) + useEffect(() => { + const el = wrapRef.current; + if (!el) return; + const update = () => + setScale( + Math.min(el.clientWidth / CANVAS_W, el.clientHeight / CANVAS_H) * 0.98, + ); + update(); + const ro = new ResizeObserver(update); + ro.observe(el); + return () => ro.disconnect(); + }, []); useEffect(() => { const canvas = canvasRef.current; @@ -368,11 +390,14 @@ export default function VillageView() { sprites.push({ y: y - 6, draw: () => drawEmoji(d.emoji, x, y + 4, 18) }); } + hitRectsRef.current = []; + // νšŒκ΄€ { const t = BUILDING_TILES.hall; const x = isoX(t.i, t.j); const y = isoY(t.i, t.j) + TH / 2; + hitRectsRef.current.push({ id: "hall", kind: "open", x: x - 34, y: y - 40, w: 68, h: 70 }); sprites.push({ y, draw: () => { @@ -386,8 +411,25 @@ export default function VillageView() { }); } + // λΆ€μ—Œ 미건섀 μ‹œ 곡터 ν‘œμ‹œ + if (!state.buildings.some((b) => b.type === "kitchen")) { + const t = BUILDING_TILES.kitchen_1; + const x = isoX(t.i, t.j); + const y = isoY(t.i, t.j) + TH / 2; + hitRectsRef.current.push({ id: "kitchen_lot", kind: "open", x: x - 30, y: y - 34, w: 60, h: 56 }); + sprites.push({ + y, + draw: () => { + drawEmoji("🚧", x, y + 2, 26); + ctx.font = "11px sans-serif"; + ctx.textAlign = "center"; + ctx.fillStyle = "rgba(107,114,128,0.9)"; + ctx.fillText("λΆ€μ—Œ 곡터", x, y + 16); + }, + }); + } + // 생산 건물 + 회수 λ°°μ§€ - hitRectsRef.current = []; for (const b of state.buildings) { const t = BUILDING_TILES[b.id]; if (!t) continue; @@ -395,8 +437,11 @@ export default function VillageView() { const y = isoY(t.i, t.j) + TH / 2; const stored = Math.floor(b.stored); const isKitchen = b.type === "kitchen"; - if (!isKitchen) - hitRectsRef.current.push({ id: b.id, x: x - 30, y: y - 44, w: 60, h: 72 }); + // 건물 본체 β†’ 정보 νŒ¨λ„ + hitRectsRef.current.push({ id: b.id, kind: "open", x: x - 30, y: y - 38, w: 60, h: 66 }); + // λ°°μ§€ β†’ λΉ λ₯Έ 회수 (생산 건물만) + if (!isKitchen && stored > 0) + hitRectsRef.current.push({ id: b.id, kind: "collect", x: x - 40, y: y - 62, w: 80, h: 26 }); sprites.push({ y, draw: () => { @@ -512,19 +557,29 @@ export default function VillageView() { const rect = canvas.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * CANVAS_W; const y = ((e.clientY - rect.top) / rect.height) * CANVAS_H; + const hit = (r: { x: number; y: number; w: number; h: number }) => + x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h; + + // 1) λ°°μ§€(λΉ λ₯Έ 회수) μš°μ„  for (const r of hitRectsRef.current) { - if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) { - const state = engine.peek(); - const b = state?.buildings.find((bb) => bb.id === r.id); - const amount = b ? Math.floor(b.stored) : 0; - if (b && amount > 0) { - engine.collect(r.id); - engine.save(); - bubblesRef.current.push({ - x: r.x + r.w / 2, y: r.y, born: performance.now(), - text: `+${amount} ${b.resource === "fish" ? "🐟" : "πŸͺ΅"}`, - }); - } + if (r.kind !== "collect" || !hit(r)) continue; + const s = engine.peek(); + const b = s?.buildings.find((bb) => bb.id === r.id); + const amount = b ? Math.floor(b.stored) : 0; + if (b && b.resource && amount > 0) { + engine.collect(r.id); + engine.save(); + bubblesRef.current.push({ + x: r.x + r.w / 2, y: r.y, born: performance.now(), + text: `+${amount} ${b.resource === "fish" ? "🐟" : "πŸͺ΅"}`, + }); + } + return; + } + // 2) 건물 본체 β†’ 정보 νŒ¨λ„ μ—΄κΈ° + for (const r of hitRectsRef.current) { + if (r.kind === "open" && hit(r)) { + onBuildingClickRef.current?.(r.id); return; } } @@ -538,14 +593,18 @@ export default function VillageView() { }, []); return ( -
+
-

- 건물 μœ„ μ£Όν™© λ°°μ§€λ₯Ό ν΄λ¦­ν•˜λ©΄ μ‚°μΆœλ¬Όμ„ νšŒμˆ˜ν•©λ‹ˆλ‹€ -

); }