From ec71624a29e0bc353d8be5c975e2035f790b481c Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 14 Aug 2026 09:42:18 +0900 Subject: [PATCH] =?UTF-8?q?Phase=20C:=20=EB=83=A5=EC=BD=94=EC=9D=B8=20?= =?UTF-8?q?=EA=B2=BD=EC=A0=9C=20=E2=80=94=20=EC=9E=A5=ED=84=B0=C2=B7?= =?UTF-8?q?=EC=B0=BD=EA=B3=A0=20=EC=83=81=ED=95=9C=C2=B7=EC=9E=85=EC=96=91?= =?UTF-8?q?=20=EB=B9=84=EC=9A=A9=C2=B7=EC=A6=89=EC=8B=9C=20=EC=99=84?= =?UTF-8?q?=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๋ƒฅ์ฝ”์ธ(๐Ÿช™) ์žฌํ™” + HUD ํ‘œ์‹œ, ๋ณต๋ฉ์ด ํŒจ์‹œ๋ธŒ ์‹คํšจ๊ณผ(์ผํ•˜๋Š” ์ค‘ ํ‰๊ท  100์ดˆ๋‹น 1) - ์žฅํ„ฐ ๊ฑด๋ฌผ(๋ชฉ์žฌ 120, ํšŒ๊ด€ Lv2): ํŒ๋งค ํ’ˆ๋ชฉ ์„ ํƒ โ†’ ๋ถ„๋‹น 5๊ฐœ ์ž๋™ ํŒ๋งค ๊ฐ€๊ฒฉ: ์ƒ์„  1ยท๋ชฉ์žฌ 2ยท๋Œ 3ยท๊ตฌ์ด 4ยทํ›ˆ์ œ 10 โ€” ๊ฐ€๊ณตํ• ์ˆ˜๋ก ์ด๋“ - ์ฐฝ๊ณ  ์ƒํ•œ: ์ž์›๋ณ„ ๊ธฐ๋ณธ 500, ์ฐฝ๊ณ  ๊ฑด๋ฌผ(+500)๋กœ ํ™•์žฅ. ๊ฐ€๋“ ์ฐจ๋ฉด ํšŒ์ˆ˜ ์ฐจ๋‹จ (๋ถ€๋ถ„ ํšŒ์ˆ˜ยท"์ฐฝ๊ณ  ๊ฐ€๋“" ํ”ผ๋“œ๋ฐฑ), ๊ฑด๋ฌผ ์ ์žฌ๋„ 150 ํ•œ๋„๋กœ ์ƒ์‚ฐ ์ •์ง€ - ์ž…์–‘ ๋น„์šฉ: 3๋งˆ๋ฆฌ์งธ๋ถ€ํ„ฐ 100+(n-2)ร—50, ๋ชจ๋‹ฌ์— ๋น„์šฉ ํ‘œ์‹œ. ์žฌ์ถ”์ฒจ ๐ŸŽฒ 30์ฝ”์ธ - ์ฆ‰์‹œ ์™„๊ณต: ๊ฑด์„ค ์ค‘ ํŒจ๋„์—์„œ ๋‚จ์€ 10์ดˆ๋‹น 1์ฝ”์ธ - ์š”๋ฆฌ ํŒ๋งค๋Š” ๋ถ€์—Œ ์„ ๋ฐ˜์—์„œ ์ฐจ๊ฐ โ€” "๋จน์ผ๊นŒ ํŒ”๊นŒ" ์„ ํƒ ์™„์„ฑ Co-Authored-By: Claude Fable 5 --- src/components/game/Game.tsx | 119 ++++++++++++++++++++++++-- src/components/game/VillageView.tsx | 11 ++- src/lib/game/data.ts | 45 +++++++++- src/lib/game/engine.ts | 124 ++++++++++++++++++++++++++-- src/lib/game/types.ts | 6 +- 5 files changed, 286 insertions(+), 19 deletions(-) diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 1807743..911b128 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -8,8 +8,10 @@ import { engine, effectiveRate, activeRecipe, + adoptCost, fullnessPenalty, isUnderConstruction, + storageCap, } from "@/lib/game/engine"; import { APTITUDE_LABEL, @@ -20,7 +22,10 @@ import { DECO_DEFS, HALL_LEVELS, KITCHEN_UPGRADE_COST, + MEAL_PRICES, mealCap, + REROLL_COST, + SELL_PRICES, PASSIVES, RECIPES, TOOL_IMG, @@ -264,6 +269,7 @@ export default function Game() { ["๐Ÿชต", Math.floor(state.resources.wood)], ["๐Ÿชจ", Math.floor(state.resources.stone)], ["๐Ÿ–", meals], + ["๐Ÿช™", Math.floor(state.coins ?? 0)], ].map(([icon, value]) => ( run(() => engine.collect(openedBuilding.id))} onUpgradeKitchen={() => run(() => engine.upgradeKitchen())} highlightAssign={tutTarget === "assign"} + onRush={() => run(() => engine.rushConstruction(openedBuilding.id))} + onSetSell={(key) => run(() => engine.setSellKey(openedBuilding.id, key))} onMove={() => { setPanel(null); setPlacing({ @@ -468,9 +476,25 @@ export default function Game() { { + const err = run(() => engine.payReroll()); + if (!err) + setCandidates( + engine.generateCandidates( + 3, + state.tutorialStep === 1 + ? "fishing" + : state.tutorialStep === 4 + ? "logging" + : undefined, + ), + ); + }} onPick={(cat) => { - run(() => engine.adoptCandidate(cat)); - setCandidates(null); + const err = run(() => engine.adoptCandidate(cat)); + if (!err) setCandidates(null); }} onClose={() => setCandidates(null)} /> @@ -721,6 +745,8 @@ function BuildingPanel({ onUpgradeKitchen, onMove, highlightAssign, + onRush, + onSetSell, }: { building: Building; state: GameState; @@ -730,6 +756,8 @@ function BuildingPanel({ onUpgradeKitchen: () => void; onMove: () => void; highlightAssign?: boolean; + onRush: () => void; + onSetSell: (key: string | null) => void; }) { const isKitchen = building.type === "kitchen"; const recipe = activeRecipe(state); @@ -773,6 +801,62 @@ function BuildingPanel({

๋‚จ์€ ์‹œ๊ฐ„ {Math.floor(remainSec / 60)}:{String(remainSec % 60).padStart(2, "0")}

+ + + ); + } + + if (building.type === "market") { + // ์žฅํ„ฐ: ํŒ๋งค ํ’ˆ๋ชฉ ์„ ํƒ + const options: Array<{ key: string; label: string; price: number; stock: number }> = [ + { key: "fish", label: "๐ŸŸ ์ƒ์„ ", price: SELL_PRICES.fish, stock: Math.floor(state.resources.fish) }, + { key: "wood", label: "๐Ÿชต ๋ชฉ์žฌ", price: SELL_PRICES.wood, stock: Math.floor(state.resources.wood) }, + { key: "stone", label: "๐Ÿชจ ๋Œ", price: SELL_PRICES.stone, stock: Math.floor(state.resources.stone) }, + { + key: "meals", + label: `${recipe.emoji} ์š”๋ฆฌ(${recipe.name})`, + price: MEAL_PRICES[recipe.key] ?? 4, + stock: state.buildings + .filter((b) => b.type === "kitchen") + .reduce((sum, b) => sum + Math.floor(b.stored), 0), + }, + ]; + return ( +
+

+ ํŒ๋งค ํ’ˆ๋ชฉ์„ ์ •ํ•˜๋ฉด ๋ถ„๋‹น 5๊ฐœ์”ฉ ์ž๋™์œผ๋กœ ํŒ”์•„ ๋ƒฅ์ฝ”์ธ์„ ๋ฒŒ์–ด์š”. ๊ฐ€๊ณตํ•œ + ์š”๋ฆฌ์ผ์ˆ˜๋ก ๊ฐ’์ด ์ข‹๋‹ต๋‹ˆ๋‹ค! +

+
+ +
+
+ {options.map((o) => ( + + ))} +
+ {!building.sellKey && ( +

์ง€๊ธˆ์€ ์•„๋ฌด๊ฒƒ๋„ ํŒ”์ง€ ์•Š์•„์š”

+ )}
); } @@ -793,6 +877,11 @@ function BuildingPanel({ ๐Ÿ“ ์ด๋™ + {building.type === "warehouse" && ( +
+ ํ˜„์žฌ ๋ณด๊ด€ ์ƒํ•œ: {storageCap(state)} / ์ž์› +
+ )} {building.type === "cat_house" && (

@@ -1192,11 +1281,17 @@ function AdoptModal({ onPick, onClose, highlightPick, + cost, + onReroll, + coins, }: { candidates: Cat[]; onPick: (cat: Cat) => void; onClose: () => void; highlightPick?: boolean; + cost: number; + onReroll: () => void; + coins: number; }) { return (
-

๐Ÿ˜ป ์ž…์–‘ํ•  ๊ณ ์–‘์ด๋ฅผ ์„ ํƒํ•˜์„ธ์š”

+

+ ๐Ÿ˜ป ์ž…์–‘ํ•  ๊ณ ์–‘์ด๋ฅผ ์„ ํƒํ•˜์„ธ์š”{" "} + + {cost > 0 ? `(๋น„์šฉ ๐Ÿช™${cost})` : "(๋ฌด๋ฃŒ!)"} + +

); })}
-

+

+ +
+

ํ•œ ๋งˆ๋ฆฌ๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด์š” ยท ํŒจ์‹œ๋ธŒ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ์ƒ์„ธ ์ •๋ณด๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค

diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index 6f59b05..38fd90e 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -908,13 +908,16 @@ export default function VillageView({ 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); + if (b && b.resource && Math.floor(b.stored) > 0 && s) { + const before = Math.floor(s.resources[b.resource]); + const err = engine.collect(r.id); engine.save(); + const gained = Math.floor(s.resources[b.resource]) - before; bubblesRef.current.push({ x: r.x + r.w / 2, y: r.y, born: performance.now(), - text: `+${amount} ${RES_EMOJI[b.resource]}`, + text: err + ? "๐Ÿ“ฆ ์ฐฝ๊ณ  ๊ฐ€๋“!" + : `+${gained} ${RES_EMOJI[b.resource]}`, }); } return; diff --git a/src/lib/game/data.ts b/src/lib/game/data.ts index a52b4a2..3c53527 100644 --- a/src/lib/game/data.ts +++ b/src/lib/game/data.ts @@ -123,6 +123,8 @@ export const FOOTPRINTS: Record = { kitchen: { w: 2, h: 2 }, cat_house: { w: 2, h: 2 }, sun_deck: { w: 2, h: 2 }, + market: { w: 2, h: 2 }, + warehouse: { w: 2, h: 2 }, }; export const footprintOf = (type: string) => FOOTPRINTS[type] ?? { w: 1, h: 1 }; @@ -149,7 +151,15 @@ export const APT_POINT_EVERY = 3; // 3๋ ˆ๋ฒจ๋งˆ๋‹ค ์ ์„ฑ ํฌ์ธํŠธ 1 export interface BuildDef { id: string; // ๊ฑด๋ฌผ ์ธ์Šคํ„ด์Šค id (๋‹จ์ผ ๊ฑด์„ค) - type: "dock" | "lumberyard" | "quarry" | "kitchen" | "cat_house" | "sun_deck"; + type: + | "dock" + | "lumberyard" + | "quarry" + | "kitchen" + | "cat_house" + | "sun_deck" + | "market" + | "warehouse"; kind?: "work" | "passive"; // passive: ๊ณ ์–‘์ด ๋ฐฐ์น˜ ์—†์Œ (๊ธฐ๋ณธ work) name: string; emoji: string; @@ -193,6 +203,16 @@ export const BUILD_DEFS: BuildDef[] = [ aptitude: "grooming", maxSlots: 0, cost: { wood: 120, stone: 40 }, reqHallLv: 3, buildTime: 90, desc: "๋”ฐ๋ˆํ•œ ํ–‡์‚ด ์•„๋ž˜ ๋’น๊ตด๋’น๊ตด. ๋งˆ์„ ์ „์ฒด์˜ ํ–‰๋ณต๋„ ํšŒ๋ณต์ด 1.5๋ฐฐ ๋นจ๋ผ์ง‘๋‹ˆ๋‹ค.", }, + { + id: "market_1", type: "market", name: "์žฅํ„ฐ", emoji: "๐Ÿช", kind: "passive", + aptitude: "crafting", maxSlots: 0, cost: { wood: 120 }, reqHallLv: 2, buildTime: 60, + desc: "์ž์›๊ณผ ์š”๋ฆฌ๋ฅผ ๋ƒฅ์ฝ”์ธ์œผ๋กœ ๋ฐ”๊ฟ”์ค๋‹ˆ๋‹ค. ํŒ๋งค ํ’ˆ๋ชฉ์„ ์ •ํ•˜๋ฉด ๋ถ„๋‹น 5๊ฐœ์”ฉ ์ž๋™ ํŒ๋งค!", + }, + { + id: "warehouse_1", type: "warehouse", name: "์ฐฝ๊ณ ", emoji: "๐Ÿ“ฆ", kind: "passive", + aptitude: "crafting", maxSlots: 0, cost: { wood: 200, stone: 100 }, reqHallLv: 3, buildTime: 120, + desc: "์ž์› ๋ณด๊ด€ ์ƒํ•œ์„ +500 ๋Š˜๋ ค์ค๋‹ˆ๋‹ค. ๊ฝ‰ ์ฐฌ ์ฐฝ๊ณ ๋Š” ์ƒ์‚ฐ์„ ๋ฉˆ์ถ”๊ฒŒ ํ•ด์š”.", + }, ]; // ---------- ๊พธ๋ฏธ๊ธฐ (๋ฐ”๋‹ฅ ์žฅ์‹) ---------- @@ -249,6 +269,29 @@ export const mealCap = (kitchenLv: number) => export const EAT_THRESHOLD = 65; // ์ด ํฌ๋งŒ๋„ ์ดํ•˜๋ฉด ๋จน์œผ๋Ÿฌ ๊ฐ export const FULLNESS_DECAY_PER_SEC = 0.03; // ร— ์‹์š• ์ˆ˜์น˜ +// ---------- ๊ฒฝ์ œ (Phase C) ---------- + +// ํŒ๋งค๊ฐ€ (๊ฐœ๋‹น ๋ƒฅ์ฝ”์ธ) +export const SELL_PRICES: Record = { + fish: 1, + wood: 2, + stone: 3, +}; +// ์š”๋ฆฌ ํŒ๋งค๊ฐ€ (๋ ˆ์‹œํ”ผ ํ‚ค ๊ธฐ์ค€) +export const MEAL_PRICES: Record = { + grilled_fish: 4, + smoked_fish: 10, +}; +export const MARKET_RATE_PER_SEC = 5 / 60; // ๋ถ„๋‹น 5๊ฐœ ํŒ๋งค +export const BASE_STORAGE = 500; // ์ž์›๋ณ„ ๊ธฐ๋ณธ ๋ณด๊ด€ ์ƒํ•œ +export const WAREHOUSE_STORAGE = 500; // ์ฐฝ๊ณ  1์ฑ„๋‹น ์ถ”๊ฐ€ ์ƒํ•œ +export const BUILDING_STORE_CAP = 150; // ๊ฑด๋ฌผ๋ณ„ ์‚ฐ์ถœ๋ฌผ ์ ์žฌ ํ•œ๋„ (๊ฐ€๋“ ์ฐจ๋ฉด ์ƒ์‚ฐ ์ •์ง€) +export const ADOPT_FREE_CATS = 2; // ์ฒ˜์Œ 2๋งˆ๋ฆฌ๋Š” ๋ฌด๋ฃŒ +export const ADOPT_BASE_COST = 100; +export const ADOPT_COST_PER_CAT = 50; +export const REROLL_COST = 30; // ์ž…์–‘ ์žฌ์ถ”์ฒจ +export const RUSH_COST_PER_10S = 1; // ์ฆ‰์‹œ ์™„๊ณต: ๋‚จ์€ 10์ดˆ๋‹น 1์ฝ”์ธ + // ---------- ์ฒด๋ ฅยทํ–‰๋ณต๋„ (Phase B) ---------- export const ENERGY_DRAIN_PER_SEC = 2 / 60; // ์ž‘์—… ์ค‘ ์ฒด๋ ฅ ์†Œ๋ชจ (๋ถ„๋‹น 2) diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts index 10fef7e..0d312de 100644 --- a/src/lib/game/engine.ts +++ b/src/lib/game/engine.ts @@ -16,8 +16,19 @@ import { BREEDS, BUILD_DEFS, CAT_NAMES, + ADOPT_BASE_COST, + ADOPT_COST_PER_CAT, + ADOPT_FREE_CATS, + BASE_STORAGE, + BUILDING_STORE_CAP, DECO_DEFS, DEFAULT_BUILDING_POS, + MARKET_RATE_PER_SEC, + MEAL_PRICES, + REROLL_COST, + RUSH_COST_PER_10S, + SELL_PRICES, + WAREHOUSE_STORAGE, EAT_THRESHOLD, EXP_PER_LEVEL, FULLNESS_DECAY_PER_SEC, @@ -112,6 +123,7 @@ function createInitialState(): GameState { version: 1, boardVersion: BOARD_VERSION, tutorialStep: 0, + coins: 0, resources: { fish: 0, wood: 0, stone: 0 }, cats: [], buildings: [], @@ -214,6 +226,21 @@ export function canPlaceAt( return true; } +/** ์ž์›๋ณ„ ๋ณด๊ด€ ์ƒํ•œ (์ฐฝ๊ณ ๋กœ ํ™•์žฅ) */ +export function storageCap(s: GameState): number { + const warehouses = s.buildings.filter( + (b) => b.type === "warehouse" && !isUnderConstruction(b), + ).length; + return BASE_STORAGE + warehouses * WAREHOUSE_STORAGE; +} + +/** ๋‹ค์Œ ์ž…์–‘ ๋น„์šฉ (์ฒ˜์Œ 2๋งˆ๋ฆฌ ๋ฌด๋ฃŒ) */ +export function adoptCost(s: GameState): number { + const n = s.cats.length; + if (n < ADOPT_FREE_CATS) return 0; + return ADOPT_BASE_COST + (n - ADOPT_FREE_CATS) * ADOPT_COST_PER_CAT; +} + /** ๋ฐ์ฝ”๋ฅผ ๋†“์„ ์ˆ˜ ์žˆ๋Š” ํƒ€์ผ์ธ๊ฐ€ (์ž”๋”” ์œ„, ๊ฑด๋ฌผยท๋‚˜๋ฌดยท๋ฌผ ์ œ์™ธ) */ export function canPlaceDecoAt(s: GameState, i: number, j: number): boolean { if (!Number.isInteger(i) || !Number.isInteger(j)) return false; @@ -278,6 +305,7 @@ export const engine = { b.pos ??= { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 5 }) }; state.decors ??= []; state.tutorialStep ??= 99; // ๊ธฐ์กด ๋งˆ์„์€ ํŠœํ† ๋ฆฌ์–ผ ์ƒ๋žต + state.coins ??= 0; // ๋ณด๋“œ ๊ฐœํŽธ(ํฌ๊ธฐยทํ’‹ํ”„๋ฆฐํŠธ ๋ณ€๊ฒฝ) ์‹œ ๊ฑด๋ฌผ ์œ„์น˜๋ฅผ ์ƒˆ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žฌ๋ฐฐ์น˜ if (state.boardVersion !== BOARD_VERSION) { for (const b of state.buildings) @@ -349,6 +377,9 @@ export const engine = { cat.stats.diligence = Math.min(150, cat.stats.diligence + rand(1, 3)); if (cat.level % APT_POINT_EVERY === 0) cat.aptitudePoints += 1; } + // ๋ณต๋ฉ์ด: ์ผํ•˜๋Š” ์ค‘ ํ‰๊ท  100์ดˆ์— 1๋ƒฅ์ฝ”์ธ + if (cat.passives.includes("lucky") && Math.random() < dt / 100) + state.coins = (state.coins ?? 0) + 1; } if (building.type === "kitchen") { // ์š”๋ฆฌ: ์žฌ๋ฃŒ๊ฐ€ ์žˆ๊ณ  ์„ ๋ฐ˜์— ์ž๋ฆฌ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์ง„ํ–‰ @@ -364,7 +395,42 @@ export const engine = { building.progress -= recipe.work; } } else { - building.stored += produced; + // ๊ฑด๋ฌผ ์ ์žฌ ํ•œ๋„ โ€” ๊ฐ€๋“ ์ฐจ๋ฉด ์ƒ์‚ฐ ์ •์ง€ (ํšŒ์ˆ˜ ์œ ๋„) + building.stored = Math.min(BUILDING_STORE_CAP, building.stored + produced); + } + // ์žฅํ„ฐ: ์ž๋™ ํŒ๋งค + if ( + building.type === "market" && + building.sellKey && + !isUnderConstruction(building) + ) { + building.progress = (building.progress ?? 0) + MARKET_RATE_PER_SEC * dt; + while (building.progress >= 1) { + let sold = false; + if (building.sellKey === "meals") { + const kitchen = state.buildings.find( + (b) => b.type === "kitchen" && b.stored >= 1, + ); + if (kitchen) { + kitchen.stored -= 1; + state.coins = + (state.coins ?? 0) + (MEAL_PRICES[activeRecipe(state).key] ?? 4); + sold = true; + } + } else { + const key = building.sellKey as keyof GameState["resources"]; + if (state.resources[key] >= 1) { + state.resources[key] -= 1; + state.coins = (state.coins ?? 0) + (SELL_PRICES[building.sellKey] ?? 1); + sold = true; + } + } + if (!sold) { + building.progress = 1; // ํŒ” ๊ฒƒ์ด ์—†์œผ๋ฉด ๋Œ€๊ธฐ + break; + } + building.progress -= 1; + } } } @@ -501,18 +567,55 @@ export const engine = { return Array.from({ length: count }, () => generateCat(forceApt)); }, - /** ํ›„๋ณด ์ค‘ ํ•œ ๋งˆ๋ฆฌ ์ž…์–‘ ํ™•์ • */ + /** ํ›„๋ณด ์ค‘ ํ•œ ๋งˆ๋ฆฌ ์ž…์–‘ ํ™•์ • (3๋งˆ๋ฆฌ์งธ๋ถ€ํ„ฐ ๋ƒฅ์ฝ”์ธ ์†Œ๋ชจ) */ adoptCandidate(cat: Cat): string | null { if (!state) return null; if (state.cats.length >= state.hall.capacity) return "์ˆ˜์šฉ ์ธ์›์ด ๊ฐ€๋“ ์ฐผ์Šต๋‹ˆ๋‹ค (ํšŒ๊ด€ ์ฆ์ถ• ํ•„์š”)"; + const cost = adoptCost(state); + if ((state.coins ?? 0) < cost) + return `๋ƒฅ์ฝ”์ธ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค (ํ•„์š”: ๐Ÿช™${cost})`; + state.coins = (state.coins ?? 0) - cost; state.cats.push(cat); return null; }, + /** ์ž…์–‘ ์žฌ์ถ”์ฒจ ๋น„์šฉ ์ง€๋ถˆ */ + payReroll(): string | null { + if (!state) return null; + if ((state.coins ?? 0) < REROLL_COST) + return `๋ƒฅ์ฝ”์ธ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค (ํ•„์š”: ๐Ÿช™${REROLL_COST})`; + state.coins = (state.coins ?? 0) - REROLL_COST; + return null; + }, + + /** ์ฆ‰์‹œ ์™„๊ณต (๋‚จ์€ 10์ดˆ๋‹น 1๋ƒฅ์ฝ”์ธ) */ + rushConstruction(buildingId: string): string | null { + if (!state) return null; + const b = state.buildings.find((x) => x.id === buildingId); + if (!b || b.constructionUntil === undefined) return null; + const remain = Math.max(0, b.constructionUntil - Date.now()); + const cost = Math.max(1, Math.ceil(remain / 10000) * RUSH_COST_PER_10S); + if ((state.coins ?? 0) < cost) + return `๋ƒฅ์ฝ”์ธ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค (ํ•„์š”: ๐Ÿช™${cost})`; + state.coins = (state.coins ?? 0) - cost; + b.constructionUntil = Date.now() - 1; + return null; + }, + + /** ์žฅํ„ฐ ํŒ๋งค ํ’ˆ๋ชฉ ์„ค์ • */ + setSellKey(marketId: string, key: string | null): string | null { + if (!state) return null; + const m = state.buildings.find((b) => b.id === marketId); + if (!m || m.type !== "market") return null; + m.sellKey = key; + m.progress = 0; + return null; + }, + /** ๊ณตํ†ต ๊ฑด์„ค: ์ƒ์  ๊ตฌ๋งค ํ›„ ์„ ํƒํ•œ ํƒ€์ผ์— ๋ฐฐ์น˜ (๋น„์šฉ์€ ๋ฐฐ์น˜ ํ™•์ • ์‹œ ์ฐจ๊ฐ) */ build( - type: "dock" | "lumberyard" | "quarry" | "kitchen" | "cat_house" | "sun_deck", + type: BuildingType, pos: { i: number; j: number }, ): string | null { if (!state) return null; @@ -571,6 +674,7 @@ export const engine = { consume(state, def.cost); state.decors ??= []; state.tutorialStep ??= 99; // ๊ธฐ์กด ๋งˆ์„์€ ํŠœํ† ๋ฆฌ์–ผ ์ƒ๋žต + state.coins ??= 0; // ๊ฐ™์€ ํƒ€์ผ์˜ ๊ธฐ์กด ์žฅ์‹ ์ œ๊ฑฐ ํ›„ ๋ฐฐ์น˜ state.decors = state.decors.filter( (d) => !(d.pos.i === pos.i && d.pos.j === pos.j), @@ -682,14 +786,18 @@ export const engine = { cat.assignedTo = null; }, - collect(buildingId: string) { - if (!state) return; + collect(buildingId: string): string | null { + if (!state) return null; const building = state.buildings.find((b) => b.id === buildingId); - if (!building || !building.resource) return; // ๋ถ€์—Œ์€ ํšŒ์ˆ˜ ๋Œ€์ƒ ์•„๋‹˜ (๊ณ ์–‘์ด๊ฐ€ ์ง์ ‘ ๋จน์Œ) - const amount = Math.floor(building.stored); - if (amount <= 0) return; + if (!building || !building.resource) return null; // ๋ถ€์—Œ์€ ํšŒ์ˆ˜ ๋Œ€์ƒ ์•„๋‹˜ + const cap = storageCap(state); + const space = Math.max(0, cap - Math.floor(state.resources[building.resource])); + const amount = Math.min(Math.floor(building.stored), space); + if (amount <= 0) + return space <= 0 ? "์ฐฝ๊ณ ๊ฐ€ ๊ฐ€๋“ ์ฐผ์–ด์š”! ์žฅํ„ฐ์—์„œ ํŒ”๊ฑฐ๋‚˜ ์ฐฝ๊ณ ๋ฅผ ์ง€์–ด์ฃผ์„ธ์š”" : null; building.stored -= amount; state.resources[building.resource] += amount; + return null; }, save() { diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 284887d..d345f8b 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -15,7 +15,9 @@ export type BuildingType = | "kitchen" | "quarry" | "cat_house" - | "sun_deck"; + | "sun_deck" + | "market" + | "warehouse"; export interface Cat { id: string; @@ -57,6 +59,7 @@ export interface Building { stored: number; // ์ƒ์‚ฐ ๊ฑด๋ฌผ: ํšŒ์ˆ˜ ๋Œ€๊ธฐ ์‚ฐ์ถœ๋ฌผ / ๋ถ€์—Œ: ๋ณด๊ด€ ์ค‘์ธ ์š”๋ฆฌ ์ˆ˜ progress?: number; // ๋ถ€์—Œ: ํ˜„์žฌ ์š”๋ฆฌ ์ง„ํ–‰ ์ž‘์—…๋Ÿ‰ constructionUntil?: number; // ๊ฑด์„ค ์™„๋ฃŒ ์˜ˆ์ • timestamp (์—†์œผ๋ฉด ์™„๊ณต ์ƒํƒœ) + sellKey?: string | null; // ์žฅํ„ฐ: ํŒ๋งค ํ’ˆ๋ชฉ (fish/wood/stone/meals) } export interface DecoTile { @@ -69,6 +72,7 @@ export interface GameState { version: 1; boardVersion?: number; // ๋ณด๋“œ ๊ฐœํŽธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋งˆ์ปค tutorialStep?: number; // ํŠœํ† ๋ฆฌ์–ผ ์ง„ํ–‰ (99 = ์™„๋ฃŒ) + coins?: number; // ๋ƒฅ์ฝ”์ธ decors?: DecoTile[]; // ๊พธ๋ฏธ๊ธฐ ๋ฐ”๋‹ฅ ํƒ€์ผ resources: Record; cats: Cat[];