From 9de2abfd85b7bf03e3a465dddb5dd216ee74925a Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Aug 2026 15:01:12 +0900 Subject: [PATCH] =?UTF-8?q?Phase=20A:=20=EC=B1=84=EC=84=9D=EC=9E=A5=C2=B7?= =?UTF-8?q?=EB=8F=8C=20=EC=9E=90=EC=9B=90=C2=B7=ED=9A=8C=EA=B4=80=20Lv3?= =?UTF-8?q?=C2=B7=EB=B6=80=EC=97=8C=20=EC=A6=9D=EC=B6=95=C2=B7=ED=9B=88?= =?UTF-8?q?=EC=A0=9C=20=EC=83=9D=EC=84=A0=C2=B7=EC=A0=81=EC=84=B1=20?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 돌 자원 + 채석장(목재 120, 회관 Lv2 해금, 채굴 3슬롯) - 건설 시스템 공통화 (BUILD_DEFS): 공터 표시·건설 패널 데이터 기반 - 부엌 Lv2 증축 (목재 150+돌 60): 선반 15, 회관 Lv3와 함께 훈제 생선 해금 - 회관 Lv3: 목재 250+돌 150, 채석장·부엌 Lv2·Lv5↑ 2마리 → 수용 8 (Lv4는 목공소 대기 잠금 예고) - 적성 포인트: 3레벨마다 1 지급, 카드에서 원하는 적성에 투자 (품종 상한) - 경험치 필요량 레벨×60 → ×90, 세이브 마이그레이션(stone·aptitudePoints) - HUD 돌 칩, 자원 이모지/라벨 공통화 Co-Authored-By: Claude Fable 5 --- src/components/game/Game.tsx | 193 +++++++++++++++++++++++----- src/components/game/VillageView.tsx | 26 ++-- src/lib/game/data.ts | 57 +++++++- src/lib/game/engine.ts | 86 +++++++++++-- src/lib/game/types.ts | 5 +- 5 files changed, 304 insertions(+), 63 deletions(-) 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, 작업 속도 배율