목공소·상위 자재·장터 다중 슬롯 (경제 확장)
- 신규 자원: 판자(목재5→1, 판매 8코인)·석재 블록(돌5→1, 판매 12코인) - 목공소(회관 Lv3, 제작 적성 2슬롯): 품목 선택 후 재료 소모하며 자동 반복 가공 — 부엌과 동일한 조작 문법, 수거는 생산 건물과 동일 - 장터 증축: Lv1~3 = 판매 슬롯 1~3개, 슬롯별 품목 독립 판매(분당 5개) Lv2 비용 목재200+돌80, Lv3 비용 판자30+블록15 - 회관 Lv4 해금: 목공소 건설 + 판자60·블록40 + Lv8 고양이 2 (수용 12) - HUD 판자·블록 필(목공소 보유 시), 채석장 회수 버튼 돌 이모지 수정 - 마이그레이션: sellKey→sellKeys[], resources.plank/block, craftKey - 목공소 건물 이미지·아이콘은 임시(창고 변형) — 전용 에셋 교체 예정 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+118
-37
@@ -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<string, string> = { fish: "생선", wood: "목재", stone: "돌" };
|
||||
const RES_LABEL: Record<string, string> = {
|
||||
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;
|
||||
},
|
||||
|
||||
|
||||
새 이슈에서 참조
사용자 차단