diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 9c3e76b..7502813 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -13,19 +13,25 @@ import { import { APTITUDE_LABEL, BREEDS, + BUILD_DEFS, catSprite, HALL_LEVELS, - KITCHEN_BUILD_COST, - KITCHEN_MEAL_CAP, + KITCHEN_UPGRADE_COST, + mealCap, PASSIVES, + RECIPES, TOOL_IMG, + type BuildDef, } 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: "돌" }; + type Panel = | { type: "building"; id: string } | { type: "hall" } - | { type: "kitchen_lot" } + | { type: "lot"; def: BuildDef } | { type: "cats" } | null; @@ -68,8 +74,10 @@ export default function Game() { { if (id === "hall") setPanel({ type: "hall" }); - else if (id === "kitchen_lot") setPanel({ type: "kitchen_lot" }); - else setPanel({ type: "building", id }); + else if (id.startsWith("lot_")) { + const def = BUILD_DEFS.find((d) => d.type === id.slice(4)); + if (def) setPanel({ type: "lot", def }); + } else setPanel({ type: "building", id }); }} /> @@ -91,6 +99,9 @@ export default function Game() { πŸͺ΅ {Math.floor(state.resources.wood)} + + πŸͺ¨ {Math.floor(state.resources.stone)} + run(() => engine.upgradeHall())} /> )} - {panel?.type === "kitchen_lot" && ( - setPanel(null)}> - setPanel(null)}> + { - run(() => engine.buildKitchen()); - setPanel({ type: "building", id: "kitchen_1" }); + const err = dispatch(() => engine.build(panel.def.type)); + if (err) { + setMessage(err); + if (msgTimer.current) clearTimeout(msgTimer.current); + msgTimer.current = setTimeout(() => setMessage(null), 3500); + } else { + setPanel({ type: "building", id: panel.def.id }); + } }} /> @@ -158,6 +176,7 @@ export default function Game() { onAssign={(catId) => run(() => engine.assign(catId, openedBuilding.id))} onUnassign={(catId) => run(() => engine.unassign(catId))} onCollect={() => run(() => engine.collect(openedBuilding.id))} + onUpgradeKitchen={() => run(() => engine.upgradeKitchen())} /> )} @@ -165,7 +184,12 @@ export default function Game() { setPanel(null)}>
{state.cats.map((cat) => ( - + run(() => engine.investAptitude(catId, key))} + /> ))}
@@ -246,7 +270,16 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo const ok = have >= (amount ?? 0); return (
  • - {ok ? "βœ“" : "Β·"} {key === "wood" ? "πŸͺ΅ λͺ©μž¬" : "🐟 생선"} {have}/{amount} + {ok ? "βœ“" : "Β·"} {RES_EMOJI[key]} {RES_LABEL[key]} {have}/{amount} +
  • + ); + })} + {(next.reqBuildings ?? []).map((req) => { + const b = state.buildings.find((x) => x.type === req.type); + const ok = Boolean(b && b.level >= (req.level ?? 1)); + return ( +
  • + {ok ? "βœ“" : "Β·"} {req.label}
  • ); })} @@ -289,25 +322,54 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo ); } -/* ──────────────── λΆ€μ—Œ 곡터 νŒ¨λ„ ──────────────── */ +/* ──────────────── 곡터 νŒ¨λ„ (건섀 곡톡) ──────────────── */ -function KitchenLotPanel({ wood, onBuild }: { wood: number; onBuild: () => void }) { - const canBuild = wood >= KITCHEN_BUILD_COST.wood; +function LotPanel({ + def, + state, + onBuild, +}: { + def: BuildDef; + state: GameState; + onBuild: () => void; +}) { + const costEntries = Object.entries(def.cost) as Array<[string, number]>; + const canBuild = + state.hall.level >= def.reqHallLv && + costEntries.every( + ([key, amount]) => + state.resources[key as keyof GameState["resources"]] >= amount, + ); return (
    {/* eslint-disable-next-line @next/next/no-img-element */} - λΆ€μ—Œ -

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

    + {def.name} +

    {def.desc}

    +
      + {def.reqHallLv > 1 && ( +
    • = def.reqHallLv ? "text-emerald-600 dark:text-emerald-400" : ""}> + {state.hall.level >= def.reqHallLv ? "βœ“" : "Β·"} νšŒκ΄€ Lv{def.reqHallLv} ν•„μš” +
    • + )} + {costEntries.map(([key, amount]) => { + const have = Math.floor(state.resources[key as keyof GameState["resources"]]); + return ( +
    • = amount ? "text-emerald-600 dark:text-emerald-400" : ""}> + {have >= amount ? "βœ“" : "Β·"} {RES_EMOJI[key]} {RES_LABEL[key]} {have}/{amount} +
    • + ); + })} +
    ); @@ -321,12 +383,14 @@ function BuildingPanel({ onAssign, onUnassign, onCollect, + onUpgradeKitchen, }: { building: Building; state: GameState; onAssign: (catId: string) => void; onUnassign: (catId: string) => void; onCollect: () => void; + onUpgradeKitchen: () => void; }) { const isKitchen = building.type === "kitchen"; const recipe = activeRecipe(state); @@ -360,7 +424,7 @@ function BuildingPanel({
    {recipe.emoji} {recipe.name} ( {Object.entries(recipe.input) - .map(([k, v]) => `${k === "fish" ? "생선" : "λͺ©μž¬"} ${v}`) + .map(([k, v]) => `${RES_LABEL[k] ?? k} ${v}`) .join(" + ")} β†’ ν¬λ§Œλ„ +{recipe.fullness})
    @@ -381,9 +445,40 @@ function BuildingPanel({ {/* μ‚°μΆœ/μ„ λ°˜ */} {isKitchen ? ( -
    - {recipe.emoji} μ„ λ°˜ {stored}/{KITCHEN_MEAL_CAP} β€” λ°°κ³ ν”ˆ 고양이가 μžλ™μœΌλ‘œ λ¨ΉμŠ΅λ‹ˆλ‹€ -
    + <> +
    + {recipe.emoji} μ„ λ°˜ {stored}/{mealCap(building.level)} β€” λ°°κ³ ν”ˆ 고양이가 μžλ™μœΌλ‘œ λ¨ΉμŠ΅λ‹ˆλ‹€ +
    + {building.level < 2 && ( +
    +
    ⬆️ λΆ€μ—Œ Lv2 증좕
    +

    + μ„ λ°˜ 15칸으둜 ν™•μž₯ Β· νšŒκ΄€ Lv3 달성 μ‹œ 🐟 ν›ˆμ œ 생선 ν•΄κΈˆ +

    + +
    + )} + {(() => { + const kitchenLv = building.level; + const lockedRecipe = RECIPES.find( + (r) => r.reqHallLv > state.hall.level || r.reqKitchenLv > kitchenLv, + ); + return lockedRecipe ? ( +

    + πŸ”’ {lockedRecipe.emoji} {lockedRecipe.name} β€” νšŒκ΄€ Lv + {lockedRecipe.reqHallLv} Β· λΆ€μ—Œ Lv{lockedRecipe.reqKitchenLv} ν•΄κΈˆ +

    + ) : null; + })()} + ) : ( + )} +
    + ))} + +

    + πŸ’ͺ κ·Όλ©΄ {cat.stats.diligence} + {cat.aptitudePoints > 0 && ( + + ✨ 적성 포인트 {cat.aptitudePoints} + + )}

    diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index 982ed76..47511ff 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -2,8 +2,10 @@ 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"; +import { BUILD_DEFS, catSprite, TOOL_IMG, type CatFrame } from "@/lib/game/data"; +import type { AptitudeKey, ResourceKey } from "@/lib/game/types"; + +const RES_EMOJI: Record = { fish: "🐟", wood: "πŸͺ΅", stone: "πŸͺ¨" }; // ---------- μ•„μ΄μ†Œλ©”νŠΈλ¦­ κ·Έλ¦¬λ“œ ---------- const COLS = 11; @@ -30,6 +32,7 @@ const BUILDING_TILES: Record = { dock_1: { i: 9, j: 5 }, lumberyard_1: { i: 2, j: 8 }, kitchen_1: { i: 6, j: 7 }, + quarry_1: { i: 1, j: 2 }, }; const TREES: Array<{ i: number; j: number }> = [ @@ -260,7 +263,7 @@ export default function VillageView({ bubblesRef.current.push({ x: anim.x, y: anim.y - 34, born: now, text: - b.type === "kitchen" ? "🍳" : b.resource === "fish" ? "🐟" : "πŸͺ΅", + b.type === "kitchen" ? "🍳" : RES_EMOJI[b.resource ?? "fish"], }); } } @@ -451,12 +454,15 @@ export default function VillageView({ }); } - // λΆ€μ—Œ 미건섀 μ‹œ 곡터 ν‘œμ‹œ - if (!state.buildings.some((b) => b.type === "kitchen")) { - const t = BUILDING_TILES.kitchen_1; + // 미건섀 건물 곡터 ν‘œμ‹œ (νšŒκ΄€ 레벨 μΆ©μ‘± μ‹œ) + for (const def of BUILD_DEFS) { + if (state.hall.level < def.reqHallLv) continue; + if (state.buildings.some((b) => b.type === def.type)) continue; + const t = BUILDING_TILES[def.id]; + if (!t) continue; 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 }); + hitRectsRef.current.push({ id: `lot_${def.type}`, kind: "open", x: x - 30, y: y - 34, w: 60, h: 56 }); sprites.push({ y, draw: () => { @@ -464,7 +470,7 @@ export default function VillageView({ ctx.font = "11px sans-serif"; ctx.textAlign = "center"; ctx.fillStyle = "rgba(107,114,128,0.9)"; - ctx.fillText("λΆ€μ—Œ 곡터", x, y + 16); + ctx.fillText(`${def.name} 곡터`, x, y + 16); }, }); } @@ -500,7 +506,7 @@ export default function VillageView({ ctx.fillStyle = "#374151"; ctx.fillText(b.name, x, y + 24); if (stored > 0) { - const label = `${isKitchen ? "πŸ–" : b.resource === "fish" ? "🐟" : "πŸͺ΅"}${stored}`; + const label = `${isKitchen ? "πŸ–" : RES_EMOJI[b.resource ?? "fish"]}${stored}`; ctx.font = "bold 12px sans-serif"; const w = ctx.measureText(label).width + 12; ctx.beginPath(); @@ -610,7 +616,7 @@ export default function VillageView({ engine.save(); bubblesRef.current.push({ x: r.x + r.w / 2, y: r.y, born: performance.now(), - text: `+${amount} ${b.resource === "fish" ? "🐟" : "πŸͺ΅"}`, + text: `+${amount} ${RES_EMOJI[b.resource]}`, }); } return; diff --git a/src/lib/game/data.ts b/src/lib/game/data.ts index 1833b5f..0c94e03 100644 --- a/src/lib/game/data.ts +++ b/src/lib/game/data.ts @@ -86,13 +86,46 @@ export const PASSIVES: PassiveDef[] = [ { key: "workaholic", name: "일쀑독", kind: "tradeoff", desc: "μž‘μ—… 속도 +60%, 행볡도 κ°μ†Œ +40%", speedMul: 1.6 }, ]; +// ---------- μ„±μž₯ ---------- + +export const EXP_PER_LEVEL = 90; // λ ˆλ²¨μ—… ν•„μš” κ²½ν—˜μΉ˜ = 레벨 Γ— 90 +export const APT_POINT_EVERY = 3; // 3λ ˆλ²¨λ§ˆλ‹€ 적성 포인트 1 + +// ---------- 건섀 κ°€λŠ₯ν•œ 건물 ---------- + +export interface BuildDef { + id: string; // 건물 μΈμŠ€ν„΄μŠ€ id (단일 건섀) + type: "quarry" | "kitchen"; + name: string; + emoji: string; + aptitude: AptitudeKey; + resource?: "fish" | "wood" | "stone"; + maxSlots: number; + cost: Partial>; + reqHallLv: number; + desc: string; +} + +export const BUILD_DEFS: BuildDef[] = [ + { + id: "kitchen_1", type: "kitchen", name: "λΆ€μ—Œ", emoji: "🍳", + aptitude: "cooking", maxSlots: 2, cost: { wood: 60 }, reqHallLv: 1, + desc: "μš”λ¦¬ 적성 고양이가 μƒμ„ μœΌλ‘œ μš”λ¦¬λ₯Ό λ§Œλ“€κ³ , λ°°κ³ ν”ˆ 고양이듀이 μ•Œμ•„μ„œ 먹으러 μ˜΅λ‹ˆλ‹€.", + }, + { + id: "quarry_1", type: "quarry", name: "채석μž₯", emoji: "⛏️", + aptitude: "mining", resource: "stone", maxSlots: 3, cost: { wood: 120 }, reqHallLv: 2, + desc: "채꡴ 적성 고양이가 λŒμ„ μΊ‘λ‹ˆλ‹€. λŒμ€ νšŒκ΄€ 증좕과 쀑급 건물에 ν•„μš”ν•©λ‹ˆλ‹€.", + }, +]; + // ---------- μš”λ¦¬ (λΆ€μ—Œ) ---------- export interface RecipeDef { key: string; name: string; emoji: string; - input: Partial>; // 재료 (μΆ”ν›„ νšλ“μ²˜ ν™•μž₯) + input: Partial>; // 재료 (μΆ”ν›„ νšλ“μ²˜ ν™•μž₯) work: number; // μš”λ¦¬ 1κ°œλ‹Ή ν•„μš” μž‘μ—…λŸ‰ fullness: number; // λ¨Ήμ—ˆμ„ λ•Œ ν¬λ§Œλ„ νšŒλ³΅λŸ‰ reqHallLv: number; // ν•΄κΈˆ 쑰건: νšŒκ΄€ 레벨 @@ -112,8 +145,10 @@ export const RECIPES: RecipeDef[] = [ }, ]; -export const KITCHEN_BUILD_COST = { wood: 60 }; -export const KITCHEN_MEAL_CAP = 10; // μš”λ¦¬ μ„ λ°˜ μ΅œλŒ€ 보관 수 +export const KITCHEN_MEAL_CAPS = [10, 15]; // λΆ€μ—Œ λ ˆλ²¨λ³„ μ„ λ°˜ 보관 수 +export const KITCHEN_UPGRADE_COST = { wood: 150, stone: 60 }; // Lv1β†’2 +export const mealCap = (kitchenLv: number) => + KITCHEN_MEAL_CAPS[kitchenLv - 1] ?? KITCHEN_MEAL_CAPS[KITCHEN_MEAL_CAPS.length - 1]; export const EAT_THRESHOLD = 65; // 이 ν¬λ§Œλ„ μ΄ν•˜λ©΄ 먹으러 감 export const FULLNESS_DECAY_PER_SEC = 0.03; // Γ— μ‹μš• 수치 @@ -121,14 +156,26 @@ export const FULLNESS_DECAY_PER_SEC = 0.03; // Γ— μ‹μš• 수치 export interface HallLevelDef { capacity: number; - cost: Partial>; + cost: Partial>; reqCat?: { level: number; count: number }; + reqBuildings?: Array<{ type: "quarry" | "kitchen"; level?: number; label: string }>; locked?: string; // 아직 κ΅¬ν˜„ μ•ˆ 된 μ„ ν–‰ μš”μ†Œ μ•ˆλ‚΄ } export const HALL_LEVELS: Record = { 2: { capacity: 5, cost: { wood: 100, fish: 80 }, reqCat: { level: 3, count: 1 } }, - 3: { capacity: 8, cost: { wood: 400 }, reqCat: { level: 5, count: 2 }, locked: "돌 μžμ›(채석μž₯)이 μΆ”κ°€λœ λ’€ ν•΄κΈˆλ©λ‹ˆλ‹€" }, + 3: { + capacity: 8, cost: { wood: 250, stone: 150 }, + reqCat: { level: 5, count: 2 }, + reqBuildings: [ + { type: "quarry", label: "채석μž₯ 건섀" }, + { type: "kitchen", level: 2, label: "λΆ€μ—Œ Lv2" }, + ], + }, + 4: { + capacity: 12, cost: { stone: 400 }, reqCat: { level: 8, count: 3 }, + locked: "λͺ©κ³΅μ†Œ(λͺ©νŒ)κ°€ μΆ”κ°€λœ λ’€ ν•΄κΈˆλ©λ‹ˆλ‹€", + }, }; export const CAT_NAMES = [ diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts index 9c45dea..6ab2b30 100644 --- a/src/lib/game/engine.ts +++ b/src/lib/game/engine.ts @@ -1,14 +1,17 @@ import type { AptitudeKey, Building, Cat, GameState } from "./types"; import { + APT_POINT_EVERY, APTITUDE_RATE, BASE_RATE, BREEDS, + BUILD_DEFS, CAT_NAMES, EAT_THRESHOLD, + EXP_PER_LEVEL, FULLNESS_DECAY_PER_SEC, HALL_LEVELS, - KITCHEN_BUILD_COST, - KITCHEN_MEAL_CAP, + KITCHEN_UPGRADE_COST, + mealCap, PASSIVES, RECIPES, type BreedDef, @@ -16,6 +19,7 @@ import { } from "./data"; const SAVE_KEY = "cat-village-save"; +const RES_LABEL: Record = { fish: "생선", wood: "λͺ©μž¬", stone: "돌" }; const APTITUDE_KEYS: AptitudeKey[] = [ "fishing", "logging", "mining", "foraging", "cooking", "crafting", "grooming", ]; @@ -68,6 +72,7 @@ function generateCat(forceAptitude?: AptitudeKey): Cat { emoji: breed.emoji, level: 1, exp: 0, + aptitudePoints: 0, aptitude, stats: { diligence: rand(50, 150), @@ -101,7 +106,7 @@ function createInitialState(): GameState { // μ‹œμž‘ 고양이 2λ§ˆλ¦¬λŠ” 각각 λ‚šμ‹œΒ·λ²Œλͺ©μ΄ κ°€λŠ₯ν•˜λ„λ‘ 보정 (초반 λ§‰νž˜ λ°©μ§€) return { version: 1, - resources: { fish: 0, wood: 0 }, + resources: { fish: 0, wood: 0, stone: 0 }, cats: [generateCat("fishing"), generateCat("logging")], buildings: createBuildings(), hall: { level: 1, capacity: 3 }, @@ -179,6 +184,9 @@ export const engine = { // μ €μž₯ 데이터가 깨쑌으면 μƒˆλ‘œ μ‹œμž‘ } if (!state) state = createInitialState(); + // ꡬ버전 μ„Έμ΄λΈŒ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ (ν•„λ“œ 보강) + state.resources.stone ??= 0; + for (const cat of state.cats) cat.aptitudePoints ??= 0; return state; }, @@ -192,11 +200,12 @@ export const engine = { produced += effectiveRate(cat, building.aptitude) * dt; // 배치 쀑 κ²½ν—˜μΉ˜: μ΄ˆλ‹Ή 1 cat.exp += dt; - const needed = cat.level * 60; + const needed = cat.level * EXP_PER_LEVEL; if (cat.exp >= needed) { cat.exp -= needed; cat.level += 1; cat.stats.diligence = Math.min(150, cat.stats.diligence + rand(1, 3)); + if (cat.level % APT_POINT_EVERY === 0) cat.aptitudePoints += 1; } } if (building.type === "kitchen") { @@ -204,7 +213,7 @@ export const engine = { const recipe = activeRecipe(state); building.progress = (building.progress ?? 0) + produced; while (building.progress >= recipe.work) { - if (building.stored >= KITCHEN_MEAL_CAP || !canConsume(state, recipe.input)) { + if (building.stored >= mealCap(building.level) || !canConsume(state, recipe.input)) { building.progress = recipe.work; // μž¬λ£ŒΒ·μ„ λ°˜ λŒ€κΈ° μƒνƒœλ‘œ μΊ‘ break; } @@ -257,20 +266,65 @@ export const engine = { return null; }, - buildKitchen(): string | null { + /** 곡톡 건섀: BUILD_DEFS 기반 (λΆ€μ—ŒΒ·μ±„μ„μž₯ …) */ + build(type: "quarry" | "kitchen"): string | null { if (!state) return null; - if (state.buildings.some((b) => b.type === "kitchen")) return "λΆ€μ—Œμ€ 이미 μ§€μ–΄μ Έ μžˆμŠ΅λ‹ˆλ‹€"; - if (state.resources.wood < KITCHEN_BUILD_COST.wood) - return `λͺ©μž¬κ°€ λΆ€μ‘±ν•©λ‹ˆλ‹€ (ν•„μš”: ${KITCHEN_BUILD_COST.wood}, 보유: ${Math.floor(state.resources.wood)})`; - state.resources.wood -= KITCHEN_BUILD_COST.wood; + const def = BUILD_DEFS.find((d) => d.type === type); + if (!def) return null; + if (state.buildings.some((b) => b.type === type)) + return `${def.name}은(λŠ”) 이미 μ§€μ–΄μ Έ μžˆμŠ΅λ‹ˆλ‹€`; + if (state.hall.level < def.reqHallLv) + return `νšŒκ΄€ Lv${def.reqHallLv}λΆ€ν„° 지을 수 μžˆμŠ΅λ‹ˆλ‹€`; + 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); state.buildings.push({ - id: "kitchen_1", type: "kitchen", name: "λΆ€μ—Œ", emoji: "🍳", - level: 1, maxSlots: 2, aptitude: "cooking", - assigned: [], stored: 0, progress: 0, + id: def.id, type: def.type, name: def.name, emoji: def.emoji, + level: 1, maxSlots: def.maxSlots, aptitude: def.aptitude, + resource: def.resource, assigned: [], stored: 0, progress: 0, }); return null; }, + /** λΆ€μ—Œ 증좕 Lv1β†’2: ν›ˆμ œ 생선 ν•΄κΈˆ 쑰건 + μ„ λ°˜ ν™•μž₯ */ + upgradeKitchen(): string | null { + if (!state) return null; + const kitchen = state.buildings.find((b) => b.type === "kitchen"); + if (!kitchen) return "λΆ€μ—Œμ΄ μ—†μŠ΅λ‹ˆλ‹€"; + if (kitchen.level >= 2) return "λΆ€μ—Œμ΄ 졜고 λ ˆλ²¨μž…λ‹ˆλ‹€"; + const missing = Object.entries(KITCHEN_UPGRADE_COST).filter( + ([key, amount]) => + state!.resources[key as keyof GameState["resources"]] < amount, + ); + if (missing.length > 0) + return `μžμ›μ΄ λΆ€μ‘±ν•©λ‹ˆλ‹€ (${missing + .map(([k, v]) => `${RES_LABEL[k] ?? k} ${v}`) + .join(", ")})`; + consume(state, KITCHEN_UPGRADE_COST); + kitchen.level = 2; + return null; + }, + + /** 적성 포인트 투자 (ν’ˆμ’… μƒν•œκΉŒμ§€) */ + investAptitude(catId: string, key: AptitudeKey): string | null { + if (!state) return null; + const cat = state.cats.find((c) => c.id === catId); + if (!cat) return null; + if (cat.aptitudePoints <= 0) return "적성 ν¬μΈνŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€"; + const cap = BREEDS.find((b) => b.key === cat.breed)?.aptitudeCap ?? 5; + if (cat.aptitude[key] >= cap) + return `${cat.breedName}의 적성 μƒν•œ(${cap})에 λ„λ‹¬ν–ˆμŠ΅λ‹ˆλ‹€`; + cat.aptitude[key] += 1; + cat.aptitudePoints -= 1; + return null; + }, + upgradeHall(): string | null { if (!state) return null; const next = HALL_LEVELS[state.hall.level + 1]; @@ -280,7 +334,11 @@ export const engine = { for (const [key, amount] of Object.entries(next.cost)) { const have = state.resources[key as keyof typeof state.resources]; if (have < (amount ?? 0)) - missing.push(`${key === "wood" ? "λͺ©μž¬" : "생선"} ${Math.floor(have)}/${amount}`); + missing.push(`${RES_LABEL[key] ?? key} ${Math.floor(have)}/${amount}`); + } + for (const req of next.reqBuildings ?? []) { + const b = state.buildings.find((x) => x.type === req.type); + if (!b || b.level < (req.level ?? 1)) missing.push(req.label); } if (next.reqCat) { const count = state.cats.filter((c) => c.level >= next.reqCat!.level).length; diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index fcc758f..18af9a4 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -7,9 +7,9 @@ export type AptitudeKey = | "crafting" | "grooming"; -export type ResourceKey = "fish" | "wood"; +export type ResourceKey = "fish" | "wood" | "stone"; -export type BuildingType = "dock" | "lumberyard" | "kitchen"; +export type BuildingType = "dock" | "lumberyard" | "kitchen" | "quarry"; export interface Cat { id: string; @@ -19,6 +19,7 @@ export interface Cat { emoji: string; level: number; exp: number; + aptitudePoints: number; // 3λ ˆλ²¨λ§ˆλ‹€ 1 μ§€κΈ‰, μ›ν•˜λŠ” 적성에 투자 aptitude: Record; stats: { diligence: number; // 50~150, μž‘μ—… 속도 배율