2단계: 부엌·요리·포만도·자동 식사·회관 증축
- 부엌 건설(목재 60), 요리 적성 배치, 물고기 구이(생선 2→구이 1, 포만도 +40) - 레시피 해금 구조: 회관/부엌 레벨 조건 (훈제 생선은 회관 Lv3 예고) - 포만도: 식욕×0.03/초 감소(소식가/대식가 반영), 배고프면 부엌에서 자동 식사 - 배고픔 페널티: 포만도 25 미만 0.7배, 0이면 0.3배 - 회관 증축 Lv2(목재100+생선80+Lv3 고양이) → 수용 5마리, Lv3은 채석장 대기 - UI: 증축 버튼·요건 표시, 부엌 카드(레시피·선반), 고양이 포만도 바 - 마을 뷰: 부엌 타일(냄비), 🍖 배지, 식사 🍽️ 버블, 재배치 시 이동 버그 수정 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+126
-3
@@ -4,8 +4,15 @@ import {
|
||||
BASE_RATE,
|
||||
BREEDS,
|
||||
CAT_NAMES,
|
||||
EAT_THRESHOLD,
|
||||
FULLNESS_DECAY_PER_SEC,
|
||||
HALL_LEVELS,
|
||||
KITCHEN_BUILD_COST,
|
||||
KITCHEN_MEAL_CAP,
|
||||
PASSIVES,
|
||||
RECIPES,
|
||||
type BreedDef,
|
||||
type RecipeDef,
|
||||
} from "./data";
|
||||
|
||||
const SAVE_KEY = "cat-village-save";
|
||||
@@ -119,6 +126,46 @@ export function catRate(cat: Cat, aptitudeKey: AptitudeKey): number {
|
||||
return apt * (cat.stats.diligence / 100) * passiveSpeedMul(cat) * BASE_RATE;
|
||||
}
|
||||
|
||||
/** 배고픔 페널티: 0이면 0.3배, 25 미만이면 0.7배 */
|
||||
export function fullnessPenalty(cat: Cat): number {
|
||||
if (cat.status.fullness <= 0) return 0.3;
|
||||
if (cat.status.fullness < 25) return 0.7;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** 실효 산출량 (배고픔 페널티 반영) */
|
||||
export function effectiveRate(cat: Cat, aptitudeKey: AptitudeKey): number {
|
||||
return catRate(cat, aptitudeKey) * fullnessPenalty(cat);
|
||||
}
|
||||
|
||||
function passiveFoodMul(cat: Cat): number {
|
||||
let mul = 1;
|
||||
if (cat.passives.includes("small_eater")) mul *= 0.7;
|
||||
if (cat.passives.includes("big_eater")) mul *= 1.5;
|
||||
return mul;
|
||||
}
|
||||
|
||||
/** 현재 해금된 레시피 중 가장 마지막 것 (부엌이 요리할 메뉴) */
|
||||
export function activeRecipe(s: GameState): RecipeDef {
|
||||
const kitchenLv = s.buildings.find((b) => b.type === "kitchen")?.level ?? 1;
|
||||
const unlocked = RECIPES.filter(
|
||||
(r) => r.reqHallLv <= s.hall.level && r.reqKitchenLv <= kitchenLv,
|
||||
);
|
||||
return unlocked[unlocked.length - 1] ?? RECIPES[0];
|
||||
}
|
||||
|
||||
function canConsume(s: GameState, input: RecipeDef["input"]): boolean {
|
||||
return Object.entries(input).every(
|
||||
([key, amount]) => s.resources[key as keyof typeof s.resources] >= (amount ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
function consume(s: GameState, input: RecipeDef["input"]) {
|
||||
for (const [key, amount] of Object.entries(input)) {
|
||||
s.resources[key as keyof typeof s.resources] -= amount ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const engine = {
|
||||
init(): GameState {
|
||||
if (state) return state;
|
||||
@@ -142,7 +189,7 @@ export const engine = {
|
||||
for (const catId of building.assigned) {
|
||||
const cat = state.cats.find((c) => c.id === catId);
|
||||
if (!cat) continue;
|
||||
produced += catRate(cat, building.aptitude) * dt;
|
||||
produced += effectiveRate(cat, building.aptitude) * dt;
|
||||
// 배치 중 경험치: 초당 1
|
||||
cat.exp += dt;
|
||||
const needed = cat.level * 60;
|
||||
@@ -152,8 +199,47 @@ export const engine = {
|
||||
cat.stats.diligence = Math.min(150, cat.stats.diligence + rand(1, 3));
|
||||
}
|
||||
}
|
||||
building.stored += produced;
|
||||
if (building.type === "kitchen") {
|
||||
// 요리: 재료가 있고 선반에 자리가 있을 때만 진행
|
||||
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)) {
|
||||
building.progress = recipe.work; // 재료·선반 대기 상태로 캡
|
||||
break;
|
||||
}
|
||||
consume(state, recipe.input);
|
||||
building.stored += 1;
|
||||
building.progress -= recipe.work;
|
||||
}
|
||||
} else {
|
||||
building.stored += produced;
|
||||
}
|
||||
}
|
||||
|
||||
// 포만도 감소 (식욕 × 패시브)
|
||||
for (const cat of state.cats) {
|
||||
const decay = cat.stats.appetite * FULLNESS_DECAY_PER_SEC * passiveFoodMul(cat);
|
||||
cat.status.fullness = Math.max(0, cat.status.fullness - decay * dt);
|
||||
}
|
||||
|
||||
// 배고픈 고양이가 부엌에서 자동 식사 (배고픈 순서대로)
|
||||
const kitchens = state.buildings.filter(
|
||||
(b) => b.type === "kitchen" && b.stored >= 1,
|
||||
);
|
||||
if (kitchens.length > 0) {
|
||||
const recipe = activeRecipe(state);
|
||||
const hungry = [...state.cats]
|
||||
.filter((c) => c.status.fullness <= EAT_THRESHOLD)
|
||||
.sort((a, b) => a.status.fullness - b.status.fullness);
|
||||
for (const cat of hungry) {
|
||||
const kitchen = kitchens.find((k) => k.stored >= 1);
|
||||
if (!kitchen) break;
|
||||
kitchen.stored -= 1;
|
||||
cat.status.fullness = Math.min(100, cat.status.fullness + recipe.fullness);
|
||||
}
|
||||
}
|
||||
|
||||
state.updatedAt = Date.now();
|
||||
},
|
||||
|
||||
@@ -164,6 +250,43 @@ export const engine = {
|
||||
return null;
|
||||
},
|
||||
|
||||
buildKitchen(): 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;
|
||||
state.buildings.push({
|
||||
id: "kitchen_1", type: "kitchen", name: "부엌", emoji: "🍳",
|
||||
level: 1, maxSlots: 2, aptitude: "cooking",
|
||||
assigned: [], stored: 0, progress: 0,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
|
||||
upgradeHall(): string | null {
|
||||
if (!state) return null;
|
||||
const next = HALL_LEVELS[state.hall.level + 1];
|
||||
if (!next) return "회관이 최고 레벨입니다";
|
||||
if (next.locked) return `회관 Lv${state.hall.level + 1}: ${next.locked}`;
|
||||
const missing: string[] = [];
|
||||
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}`);
|
||||
}
|
||||
if (next.reqCat) {
|
||||
const count = state.cats.filter((c) => c.level >= next.reqCat!.level).length;
|
||||
if (count < next.reqCat.count)
|
||||
missing.push(`Lv${next.reqCat.level} 이상 고양이 ${count}/${next.reqCat.count}마리`);
|
||||
}
|
||||
if (missing.length > 0) return `증축 조건 부족 — ${missing.join(", ")}`;
|
||||
consume(state, next.cost);
|
||||
state.hall.level += 1;
|
||||
state.hall.capacity = next.capacity;
|
||||
return null;
|
||||
},
|
||||
|
||||
assign(catId: string, buildingId: string): string | null {
|
||||
if (!state) return null;
|
||||
const cat = state.cats.find((c) => c.id === catId);
|
||||
@@ -193,7 +316,7 @@ export const engine = {
|
||||
collect(buildingId: string) {
|
||||
if (!state) return;
|
||||
const building = state.buildings.find((b) => b.id === buildingId);
|
||||
if (!building) return;
|
||||
if (!building || !building.resource) return; // 부엌은 회수 대상 아님 (고양이가 직접 먹음)
|
||||
const amount = Math.floor(building.stored);
|
||||
if (amount <= 0) return;
|
||||
building.stored -= amount;
|
||||
|
||||
새 이슈에서 참조
사용자 차단