Phase A: 채석장·돌 자원·회관 Lv3·부엌 증축·훈제 생선·적성 포인트
- 돌 자원 + 채석장(목재 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 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+72
-14
@@ -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<string, string> = { 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;
|
||||
|
||||
새 이슈에서 참조
사용자 차단