diff --git a/public/assets/buildings/workshop.png b/public/assets/buildings/workshop.png new file mode 100644 index 0000000..b4f1ad1 Binary files /dev/null and b/public/assets/buildings/workshop.png differ diff --git a/public/assets/icons/block.png b/public/assets/icons/block.png new file mode 100644 index 0000000..36564f8 Binary files /dev/null and b/public/assets/icons/block.png differ diff --git a/public/assets/icons/plank.png b/public/assets/icons/plank.png new file mode 100644 index 0000000..5ccd76f Binary files /dev/null and b/public/assets/icons/plank.png differ diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index d4d1f92..20ab31f 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -27,6 +27,8 @@ import { HALL_LEVELS, ICON_IMG, KITCHEN_UPGRADE_COST, + CRAFT_RECIPES, + MARKET_LEVELS, MEAL_PRICES, mealCap, REROLL_COST, @@ -38,8 +40,12 @@ import { } from "@/lib/game/data"; import type { AptitudeKey, Building, Cat, GameState } from "@/lib/game/types"; -const RES_EMOJI: Record = { fish: "🐟", wood: "πŸͺ΅", stone: "πŸͺ¨" }; -const RES_LABEL: Record = { fish: "생선", wood: "λͺ©μž¬", stone: "돌" }; +const RES_EMOJI: Record = { + fish: "🐟", wood: "πŸͺ΅", stone: "πŸͺ¨", plank: "πŸͺš", block: "🧱", +}; +const RES_LABEL: Record = { + fish: "생선", wood: "λͺ©μž¬", stone: "돌", plank: "판자", block: "μ„μž¬ 블둝", +}; // ---------- νŠœν† λ¦¬μ–Ό ---------- interface TutStep { @@ -292,6 +298,14 @@ export default function Game() { ["🐟", ICON_IMG.fish, Math.floor(state.resources.fish)], ["πŸͺ΅", ICON_IMG.wood, Math.floor(state.resources.wood)], ["πŸͺ¨", ICON_IMG.stone, Math.floor(state.resources.stone)], + ...(state.buildings.some((b) => b.type === "workshop") || + (state.resources.plank ?? 0) > 0 || + (state.resources.block ?? 0) > 0 + ? ([ + ["πŸͺš", ICON_IMG.plank, Math.floor(state.resources.plank ?? 0)], + ["🧱", ICON_IMG.block, Math.floor(state.resources.block ?? 0)], + ] as Array<[string, string | null, string | number]>) + : []), ["πŸ–", ICON_IMG.meal, meals], ["πŸͺ™", ICON_IMG.coin, Math.floor(state.coins ?? 0)], ] as Array<[string, string | null, string | number]> @@ -491,7 +505,11 @@ export default function Game() { onUpgradeKitchen={() => run(() => engine.upgradeKitchen())} highlightAssign={tutTarget === "assign"} onRush={() => run(() => engine.rushConstruction(openedBuilding.id))} - onSetSell={(key) => run(() => engine.setSellKey(openedBuilding.id, key))} + onSetSell={(slot, key) => + run(() => engine.setSellSlot(openedBuilding.id, slot, key)) + } + onUpgradeMarket={() => run(() => engine.upgradeMarket())} + onSetCraft={(key) => run(() => engine.setCraftKey(openedBuilding.id, key))} onMove={() => { setPanel(null); setPlacing({ @@ -827,6 +845,8 @@ function BuildingPanel({ highlightAssign, onRush, onSetSell, + onUpgradeMarket, + onSetCraft, }: { building: Building; state: GameState; @@ -837,7 +857,9 @@ function BuildingPanel({ onMove: () => void; highlightAssign?: boolean; onRush: () => void; - onSetSell: (key: string | null) => void; + onSetSell: (slot: number, key: string | null) => void; + onUpgradeMarket: () => void; + onSetCraft: (key: string | null) => void; }) { const isKitchen = building.type === "kitchen"; const recipe = activeRecipe(state); @@ -897,6 +919,8 @@ function BuildingPanel({ { 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: "plank", label: "πŸͺš 판자", price: SELL_PRICES.plank, stock: Math.floor(state.resources.plank ?? 0) }, + { key: "block", label: "🧱 μ„μž¬ 블둝", price: SELL_PRICES.block, stock: Math.floor(state.resources.block ?? 0) }, { key: "meals", label: `${recipe.emoji} μš”λ¦¬(${recipe.name})`, @@ -906,41 +930,72 @@ function BuildingPanel({ .reduce((sum, b) => sum + Math.floor(b.stored), 0), }, ]; + const sellKeys = building.sellKeys ?? [null]; + const slots = Array.from({ length: building.level }, (_, i) => sellKeys[i] ?? null); + const nextMarket = MARKET_LEVELS[building.level + 1]; return (

- 판맀 ν’ˆλͺ©μ„ μ •ν•˜λ©΄ λΆ„λ‹Ή 5κ°œμ”© μžλ™μœΌλ‘œ νŒ”μ•„ λƒ₯코인을 λ²Œμ–΄μš”. κ°€κ³΅ν•œ - μš”λ¦¬μΌμˆ˜λ‘ 값이 μ’‹λ‹΅λ‹ˆλ‹€! + μŠ¬λ‘―λ§ˆλ‹€ ν’ˆλͺ©μ„ μ •ν•˜λ©΄ μŠ¬λ‘―λ‹Ή λΆ„λ‹Ή 5κ°œμ”© μžλ™μœΌλ‘œ νŒ”μ•„μš”. κ°€κ³΅ν•œ + μžμž¬Β·μš”λ¦¬μΌμˆ˜λ‘ 값이 μ’‹λ‹΅λ‹ˆλ‹€!

-
- {options.map((o) => ( - - ))} -
- {!building.sellKey && ( + {slots.map((sel, slotIdx) => ( +
+
+ πŸ“¦ 판맀 슬둯 {slotIdx + 1} + {sel && ( + + {options.find((o) => o.key === sel)?.label} 판맀 쀑 + + )} +
+
+ {options.map((o) => ( + + ))} +
+
+ ))} + {slots.every((k) => !k) && (

μ§€κΈˆμ€ 아무것도 νŒ”μ§€ μ•Šμ•„μš”

)} + {nextMarket && ( + + )} + {!nextMarket && ( +

졜고 레벨 μž₯ν„°μ˜ˆμš”! (슬둯 3개)

+ )}
); } + if (building.maxSlots === 0) { // νŒ¨μ‹œλΈŒ 건물 β€” 배치 μ—†μŒ const restingHere = state.cats.filter((c) => c.state === "resting"); @@ -993,6 +1048,37 @@ function BuildingPanel({ return (
{/* 정보 */} + {building.type === "workshop" && ( +
+

+ ν’ˆλͺ©μ„ μ •ν•˜λ©΄ 배치된 고양이가 재료λ₯Ό μ†Œλͺ¨ν•˜λ©° 계속 κ°€κ³΅ν•΄μš”. +

+ {CRAFT_RECIPES.map((r) => { + const active = building.craftKey === r.key; + return ( + + ); + })} +
+ )} +
@@ -1026,6 +1112,25 @@ function BuildingPanel({ />
+ ) : building.type === "workshop" ? ( + (() => { + const craft = CRAFT_RECIPES.find((r) => r.key === building.craftKey); + return ( + <> +
가곡 속도 {totalRate.toFixed(2)}/초
+ {craft && ( +
+
+
+ )} + + ); + })() ) : (
생산 속도 {totalRate.toFixed(2)}/초
)} @@ -1073,7 +1178,12 @@ function BuildingPanel({ disabled={stored <= 0} className="amber-btn w-full py-2.5 text-sm disabled:cursor-not-allowed disabled:opacity-40" > - {building.resource === "fish" ? "🐟" : "πŸͺ΅"} {stored}개 회수 + {RES_EMOJI[ + (building.type === "workshop" + ? (building.craftKey ?? "wood") + : (building.resource ?? "wood")) as string + ] ?? "πŸ“¦"}{" "} + {stored}개 회수 )} diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index bd46a37..5e1d6e3 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -24,8 +24,18 @@ import type { ResourceKey, } from "@/lib/game/types"; -const RES_EMOJI: Record = { fish: "🐟", wood: "πŸͺ΅", stone: "πŸͺ¨" }; -const RES_ICON: Record = { fish: "fish", wood: "wood", stone: "stone" }; +const RES_EMOJI: Record = { + fish: "🐟", wood: "πŸͺ΅", stone: "πŸͺ¨", plank: "πŸͺš", block: "🧱", +}; +const RES_ICON: Record = { + fish: "fish", wood: "wood", stone: "stone", plank: "plank", block: "block", +}; +/** 건물이 μ‚°μΆœν•˜λŠ” μžμ› ν‚€ (λͺ©κ³΅μ†ŒλŠ” 선택 ν’ˆλͺ©) */ +const resourceOf = (b: Building): ResourceKey | null => + b.type === "workshop" + ? ((b.craftKey as ResourceKey | null) ?? null) + : (b.resource ?? null); + const EMOJI_TO_NATURE: Record = { "🌼": "daisy", "🌷": "tulip", "🌾": "wheat", "πŸ„": "mushroom", "πŸͺ¨": "rock", }; @@ -426,7 +436,7 @@ export default function VillageView({ if (b && catRate(cat, b.aptitude) > 0) { bubblesRef.current.push({ x: anim.x, y: anim.y - 34, born: now, - icon: b.type === "kitchen" ? "pan" : RES_ICON[b.resource ?? "fish"], + icon: b.type === "kitchen" ? "pan" : RES_ICON[resourceOf(b) ?? "fish"], }); } } @@ -759,7 +769,7 @@ export default function VillageView({ if (stored > 0) { drawPlankBadge( x, y - imgW * 0.55, `${stored}`, - isKitchen ? "meal" : RES_ICON[b.resource ?? "fish"], + isKitchen ? "meal" : RES_ICON[resourceOf(b) ?? "fish"], ); } }, @@ -1006,16 +1016,17 @@ 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); - if (b && b.resource && Math.floor(b.stored) > 0 && s) { - const before = Math.floor(s.resources[b.resource]); + const resKey = b ? resourceOf(b) : null; + if (b && resKey && Math.floor(b.stored) > 0 && s) { + const before = Math.floor(s.resources[resKey]); const err = engine.collect(r.id); engine.save(); - const gained = Math.floor(s.resources[b.resource]) - before; + const gained = Math.floor(s.resources[resKey]) - before; bubblesRef.current.push({ x: r.x + r.w / 2, y: r.y, born: performance.now(), ...(err ? { text: "πŸ“¦ μ°½κ³  가득!" } - : { icon: RES_ICON[b.resource], text: `+${gained}` }), + : { icon: RES_ICON[resKey], text: `+${gained}` }), }); } return; diff --git a/src/lib/game/data.ts b/src/lib/game/data.ts index f7363b7..fc0891b 100644 --- a/src/lib/game/data.ts +++ b/src/lib/game/data.ts @@ -35,6 +35,7 @@ export const BUILDING_IMG: Record = { sun_deck: "/assets/buildings/sun_deck.png", market: "/assets/buildings/market.png", warehouse: "/assets/buildings/warehouse.png", + workshop: "/assets/buildings/workshop.png", }; // μžμ—° μ†Œν’ˆ 일러슀트 (public/assets/nature/) @@ -58,6 +59,8 @@ export const ICON_IMG: Record = { meal: "/assets/icons/meal.png", coin: "/assets/icons/coin.png", hammer: "/assets/icons/hammer.png", + plank: "/assets/icons/plank.png", + block: "/assets/icons/block.png", sweat: "/assets/icons/sweat.png", plate: "/assets/icons/plate.png", sleep: "/assets/icons/sleep.png", @@ -158,6 +161,7 @@ export const FOOTPRINTS: Record = { sun_deck: { w: 2, h: 2 }, market: { w: 2, h: 2 }, warehouse: { w: 2, h: 2 }, + workshop: { w: 2, h: 2 }, }; export const footprintOf = (type: string) => FOOTPRINTS[type] ?? { w: 1, h: 1 }; @@ -192,7 +196,8 @@ export interface BuildDef { | "cat_house" | "sun_deck" | "market" - | "warehouse"; + | "warehouse" + | "workshop"; kind?: "work" | "passive"; // passive: 고양이 배치 μ—†μŒ (κΈ°λ³Έ work) name: string; emoji: string; @@ -246,6 +251,11 @@ export const BUILD_DEFS: BuildDef[] = [ aptitude: "crafting", maxSlots: 0, cost: { wood: 200, stone: 100 }, reqHallLv: 3, buildTime: 120, desc: "μžμ› 보관 μƒν•œμ„ +500 λŠ˜λ €μ€λ‹ˆλ‹€. 꽉 μ°¬ μ°½κ³ λŠ” 생산을 λ©ˆμΆ”κ²Œ ν•΄μš”.", }, + { + id: "workshop_1", type: "workshop", name: "λͺ©κ³΅μ†Œ", emoji: "πŸͺš", + aptitude: "crafting", maxSlots: 2, cost: { wood: 150, stone: 80 }, reqHallLv: 3, buildTime: 120, + desc: "μ œμž‘ 적성 고양이가 λͺ©μž¬Β·λŒμ„ νŒμžμ™€ μ„μž¬ λΈ”λ‘μœΌλ‘œ κ°€κ³΅ν•©λ‹ˆλ‹€. νšŒκ΄€ Lv4의 μ—΄μ‡ !", + }, ]; // ---------- κΎΈλ―ΈκΈ° (λ°”λ‹₯ μž₯식) ---------- @@ -302,6 +312,32 @@ export const mealCap = (kitchenLv: number) => export const EAT_THRESHOLD = 65; // 이 ν¬λ§Œλ„ μ΄ν•˜λ©΄ 먹으러 감 export const FULLNESS_DECAY_PER_SEC = 0.03; // Γ— μ‹μš• 수치 +// ---------- λͺ©κ³΅μ†Œ (μƒμœ„ 자재 가곡) ---------- + +export interface CraftDef { + key: "plank" | "block"; + name: string; + emoji: string; + input: Partial>; + work: number; // 1κ°œλ‹Ή ν•„μš” μž‘μ—…λŸ‰ +} + +export const CRAFT_RECIPES: CraftDef[] = [ + { key: "plank", name: "판자", emoji: "πŸͺš", input: { wood: 5 }, work: 6 }, + { key: "block", name: "μ„μž¬ 블둝", emoji: "🧱", input: { stone: 5 }, work: 8 }, +]; + +// ---------- μž₯ν„° 증좕 (레벨 = 판맀 슬둯 수) ---------- + +export const MARKET_LEVELS: Record< + number, + { cost: Partial> } +> = { + 2: { cost: { wood: 200, stone: 80 } }, + 3: { cost: { plank: 30, block: 15 } }, +}; +export const MARKET_MAX_LEVEL = 3; + // ---------- 경제 (Phase C) ---------- // νŒλ§€κ°€ (κ°œλ‹Ή λƒ₯코인) @@ -309,6 +345,8 @@ export const SELL_PRICES: Record = { fish: 1, wood: 2, stone: 3, + plank: 8, // λͺ©μž¬ 5 β†’ 판자 1: μ›μžμž¬ λŒ€λΉ„ μ••μΆ•Β·λΆ€κ°€κ°€μΉ˜ + block: 12, }; // μš”λ¦¬ νŒλ§€κ°€ (λ ˆμ‹œν”Ό ν‚€ κΈ°μ€€) export const MEAL_PRICES: Record = { @@ -341,9 +379,9 @@ export const STRIKE_RESUME = 40; // 이 이상 회볡되면 볡귀 export interface HallLevelDef { capacity: number; - cost: Partial>; + cost: Partial>; reqCat?: { level: number; count: number }; - reqBuildings?: Array<{ type: "quarry" | "kitchen"; level?: number; label: string }>; + reqBuildings?: Array<{ type: "quarry" | "kitchen" | "workshop"; level?: number; label: string }>; locked?: string; // 아직 κ΅¬ν˜„ μ•ˆ 된 μ„ ν–‰ μš”μ†Œ μ•ˆλ‚΄ } @@ -358,8 +396,10 @@ export const HALL_LEVELS: Record = { ], }, 4: { - capacity: 12, cost: { stone: 400 }, reqCat: { level: 8, count: 3 }, - locked: "λͺ©κ³΅μ†Œ(λͺ©νŒ)κ°€ μΆ”κ°€λœ λ’€ ν•΄κΈˆλ©λ‹ˆλ‹€", + capacity: 12, + cost: { plank: 60, block: 40 }, + reqCat: { level: 8, count: 2 }, + reqBuildings: [{ type: "workshop", label: "λͺ©κ³΅μ†Œ 건섀" }], }, }; diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts index ccae08f..3163119 100644 --- a/src/lib/game/engine.ts +++ b/src/lib/game/engine.ts @@ -37,6 +37,9 @@ import { HALL_TILE, isWaterTile, KITCHEN_UPGRADE_COST, + CRAFT_RECIPES, + MARKET_LEVELS, + MARKET_MAX_LEVEL, mealCap, PASSIVES, PLACEMENT_RULES, @@ -48,7 +51,9 @@ import { import type { BuildingType } from "./types"; const SAVE_KEY = "cat-village-save"; -const RES_LABEL: Record = { fish: "생선", wood: "λͺ©μž¬", stone: "돌" }; +const RES_LABEL: Record = { + fish: "생선", wood: "λͺ©μž¬", stone: "돌", plank: "판자", block: "μ„μž¬ 블둝", +}; const APTITUDE_KEYS: AptitudeKey[] = [ "fishing", "logging", "mining", "foraging", "cooking", "crafting", "grooming", ]; @@ -124,7 +129,7 @@ function createInitialState(): GameState { boardVersion: BOARD_VERSION, tutorialStep: 0, coins: 0, - resources: { fish: 0, wood: 0, stone: 0 }, + resources: { fish: 0, wood: 0, stone: 0, plank: 0, block: 0 }, cats: [], buildings: [], decors: [], @@ -308,6 +313,16 @@ export const engine = { if (!state) state = createInitialState(); // ꡬ버전 μ„Έμ΄λΈŒ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ (ν•„λ“œ 보강) state.resources.stone ??= 0; + state.resources.plank ??= 0; + state.resources.block ??= 0; + for (const b of state.buildings) { + // μž₯ν„°: 단일 sellKey β†’ 슬둯 λ°°μ—΄ (레벨 = 슬둯 수) + if (b.type === "market" && b.sellKeys === undefined) { + b.sellKeys = [b.sellKey ?? null]; + b.sellKey = undefined; + } + if (b.type === "workshop") b.craftKey ??= null; + } for (const cat of state.cats) { cat.aptitudePoints ??= 0; cat.status.energy ??= cat.stats.stamina; @@ -456,42 +471,64 @@ export const engine = { building.stored += 1; building.progress -= recipe.work; } - } else { + } else if (building.type === "workshop") { + // λͺ©κ³΅μ†Œ: 선택 ν’ˆλͺ©μ„ 재료 μ†Œλͺ¨ν•˜λ©° 반볡 가곡 (λΆ€μ—Œκ³Ό 동일 νŒ¨ν„΄) + const recipe = CRAFT_RECIPES.find((r) => r.key === building.craftKey); + if (recipe) { + building.progress = (building.progress ?? 0) + produced; + while (building.progress >= recipe.work) { + if ( + building.stored >= BUILDING_STORE_CAP || + !canConsume(state, recipe.input) + ) { + building.progress = recipe.work; // 재료·적재 λŒ€κΈ° + break; + } + consume(state, recipe.input); + building.stored += 1; + building.progress -= recipe.work; + } + } + } else if (building.type !== "market") { // 건물 적재 ν•œλ„ β€” 가득 μ°¨λ©΄ 생산 μ •μ§€ (회수 μœ λ„) 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; + if (building.type === "market" && !isUnderConstruction(building)) { + const keys = (building.sellKeys ?? []).filter(Boolean) as string[]; + if (keys.length > 0) { + building.progress = (building.progress ?? 0) + MARKET_RATE_PER_SEC * dt; + while (building.progress >= 1) { + // μŠ¬λ‘―λ‹Ή 1κ°œμ”© λ™μ‹œ 판맀 (μŠ¬λ‘―λ³„ λΆ„λ‹Ή 5개) + let soldAny = false; + for (const sellKey of keys) { + if (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); + soldAny = true; + } + } else { + const key = sellKey as keyof GameState["resources"]; + if (state.resources[key] >= 1) { + state.resources[key] -= 1; + state.coins = + (state.coins ?? 0) + (SELL_PRICES[sellKey] ?? 1); + soldAny = 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 (!soldAny) { + building.progress = 1; // νŒ” 것이 μ—†μœΌλ©΄ λŒ€κΈ° + break; } + building.progress -= 1; } - if (!sold) { - building.progress = 1; // νŒ” 것이 μ—†μœΌλ©΄ λŒ€κΈ° - break; - } - building.progress -= 1; } } } @@ -685,12 +722,51 @@ export const engine = { }, /** μž₯ν„° 판맀 ν’ˆλͺ© μ„€μ • */ - setSellKey(marketId: string, key: string | null): string | null { + /** μž₯ν„° μŠ¬λ‘―λ³„ 판맀 ν’ˆλͺ© μ„€μ • */ + setSellSlot(marketId: string, slot: number, 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; + m.sellKeys ??= [null]; + if (slot < 0 || slot >= m.level) return null; + while (m.sellKeys.length < m.level) m.sellKeys.push(null); + m.sellKeys[slot] = key; + return null; + }, + + /** μž₯ν„° 증좕 (레벨 = 판맀 슬둯 수) */ + upgradeMarket(): string | null { + if (!state) return null; + const m = state.buildings.find((b) => b.type === "market"); + if (!m) return "μž₯ν„°κ°€ μ—†μŠ΅λ‹ˆλ‹€"; + if (m.level >= MARKET_MAX_LEVEL) return "μž₯ν„°κ°€ 졜고 λ ˆλ²¨μž…λ‹ˆλ‹€"; + const def = MARKET_LEVELS[m.level + 1]; + if (!def) 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); + m.level += 1; + m.sellKeys ??= [null]; + while (m.sellKeys.length < m.level) m.sellKeys.push(null); + return null; + }, + + /** λͺ©κ³΅μ†Œ 생산 ν’ˆλͺ© μ„€μ • */ + setCraftKey(workshopId: string, key: string | null): string | null { + if (!state) return null; + const w = state.buildings.find((b) => b.id === workshopId); + if (!w || w.type !== "workshop") return null; + if (key !== null && !CRAFT_RECIPES.some((r) => r.key === key)) return null; + if (w.craftKey !== key && Math.floor(w.stored) > 0) + return "λ¨Όμ € μŒ“μΈ μ™„μ„±ν’ˆμ„ μˆ˜κ±°ν•΄μ£Όμ„Έμš”"; + w.craftKey = key; + w.progress = 0; return null; }, @@ -889,14 +965,19 @@ export const engine = { collect(buildingId: string): string | null { if (!state) return null; const building = state.buildings.find((b) => b.id === buildingId); - if (!building || !building.resource) return null; // λΆ€μ—Œμ€ 회수 λŒ€μƒ μ•„λ‹˜ + // λͺ©κ³΅μ†ŒλŠ” 선택 ν’ˆλͺ©μ΄, 생산 건물은 κ³ μ • μžμ›μ΄ 회수 λŒ€μƒ + const resKey = + building?.type === "workshop" + ? (building.craftKey as keyof GameState["resources"] | null) + : (building?.resource ?? null); + if (!building || !resKey) return null; // λΆ€μ—Œμ€ 회수 λŒ€μƒ μ•„λ‹˜ const cap = storageCap(state); - const space = Math.max(0, cap - Math.floor(state.resources[building.resource])); + const space = Math.max(0, cap - Math.floor(state.resources[resKey])); const amount = Math.min(Math.floor(building.stored), space); if (amount <= 0) return space <= 0 ? "μ°½κ³ κ°€ 가득 μ°Όμ–΄μš”! μž₯ν„°μ—μ„œ νŒ”κ±°λ‚˜ μ°½κ³ λ₯Ό μ§€μ–΄μ£Όμ„Έμš”" : null; building.stored -= amount; - state.resources[building.resource] += amount; + state.resources[resKey] += amount; return null; }, diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index d7c68f7..3cb533a 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -7,7 +7,7 @@ export type AptitudeKey = | "crafting" | "grooming"; -export type ResourceKey = "fish" | "wood" | "stone"; +export type ResourceKey = "fish" | "wood" | "stone" | "plank" | "block"; export type BuildingType = | "dock" @@ -17,7 +17,8 @@ export type BuildingType = | "cat_house" | "sun_deck" | "market" - | "warehouse"; + | "warehouse" + | "workshop"; export interface Cat { id: string; @@ -59,7 +60,9 @@ export interface Building { stored: number; // 생산 건물: 회수 λŒ€κΈ° μ‚°μΆœλ¬Ό / λΆ€μ—Œ: 보관 쀑인 μš”λ¦¬ 수 progress?: number; // λΆ€μ—Œ: ν˜„μž¬ μš”λ¦¬ μ§„ν–‰ μž‘μ—…λŸ‰ constructionUntil?: number; // 건섀 μ™„λ£Œ μ˜ˆμ • timestamp (μ—†μœΌλ©΄ 완곡 μƒνƒœ) - sellKey?: string | null; // μž₯ν„°: 판맀 ν’ˆλͺ© (fish/wood/stone/meals) + sellKey?: string | null; // (ꡬ버전) μž₯ν„° 판맀 ν’ˆλͺ© β€” sellKeys둜 λ§ˆμ΄κ·Έλ ˆμ΄μ…˜λ¨ + sellKeys?: (string | null)[]; // μž₯ν„°: μŠ¬λ‘―λ³„ 판맀 ν’ˆλͺ© (레벨 = 슬둯 수) + craftKey?: string | null; // λͺ©κ³΅μ†Œ: 생산 ν’ˆλͺ© (plank/block) } export interface DecoTile {