Phase C: 냥코인 경제 — 장터·창고 상한·입양 비용·즉시 완공
- 냥코인(🪙) 재화 + HUD 표시, 복덩이 패시브 실효과(일하는 중 평균 100초당 1) - 장터 건물(목재 120, 회관 Lv2): 판매 품목 선택 → 분당 5개 자동 판매 가격: 생선 1·목재 2·돌 3·구이 4·훈제 10 — 가공할수록 이득 - 창고 상한: 자원별 기본 500, 창고 건물(+500)로 확장. 가득 차면 회수 차단 (부분 회수·"창고 가득" 피드백), 건물 적재도 150 한도로 생산 정지 - 입양 비용: 3마리째부터 100+(n-2)×50, 모달에 비용 표시. 재추첨 🎲 30코인 - 즉시 완공: 건설 중 패널에서 남은 10초당 1코인 - 요리 판매는 부엌 선반에서 차감 — "먹일까 팔까" 선택 완성 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+116
-8
@@ -16,8 +16,19 @@ import {
|
||||
BREEDS,
|
||||
BUILD_DEFS,
|
||||
CAT_NAMES,
|
||||
ADOPT_BASE_COST,
|
||||
ADOPT_COST_PER_CAT,
|
||||
ADOPT_FREE_CATS,
|
||||
BASE_STORAGE,
|
||||
BUILDING_STORE_CAP,
|
||||
DECO_DEFS,
|
||||
DEFAULT_BUILDING_POS,
|
||||
MARKET_RATE_PER_SEC,
|
||||
MEAL_PRICES,
|
||||
REROLL_COST,
|
||||
RUSH_COST_PER_10S,
|
||||
SELL_PRICES,
|
||||
WAREHOUSE_STORAGE,
|
||||
EAT_THRESHOLD,
|
||||
EXP_PER_LEVEL,
|
||||
FULLNESS_DECAY_PER_SEC,
|
||||
@@ -112,6 +123,7 @@ function createInitialState(): GameState {
|
||||
version: 1,
|
||||
boardVersion: BOARD_VERSION,
|
||||
tutorialStep: 0,
|
||||
coins: 0,
|
||||
resources: { fish: 0, wood: 0, stone: 0 },
|
||||
cats: [],
|
||||
buildings: [],
|
||||
@@ -214,6 +226,21 @@ export function canPlaceAt(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 자원별 보관 상한 (창고로 확장) */
|
||||
export function storageCap(s: GameState): number {
|
||||
const warehouses = s.buildings.filter(
|
||||
(b) => b.type === "warehouse" && !isUnderConstruction(b),
|
||||
).length;
|
||||
return BASE_STORAGE + warehouses * WAREHOUSE_STORAGE;
|
||||
}
|
||||
|
||||
/** 다음 입양 비용 (처음 2마리 무료) */
|
||||
export function adoptCost(s: GameState): number {
|
||||
const n = s.cats.length;
|
||||
if (n < ADOPT_FREE_CATS) return 0;
|
||||
return ADOPT_BASE_COST + (n - ADOPT_FREE_CATS) * ADOPT_COST_PER_CAT;
|
||||
}
|
||||
|
||||
/** 데코를 놓을 수 있는 타일인가 (잔디 위, 건물·나무·물 제외) */
|
||||
export function canPlaceDecoAt(s: GameState, i: number, j: number): boolean {
|
||||
if (!Number.isInteger(i) || !Number.isInteger(j)) return false;
|
||||
@@ -278,6 +305,7 @@ export const engine = {
|
||||
b.pos ??= { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 5 }) };
|
||||
state.decors ??= [];
|
||||
state.tutorialStep ??= 99; // 기존 마을은 튜토리얼 생략
|
||||
state.coins ??= 0;
|
||||
// 보드 개편(크기·풋프린트 변경) 시 건물 위치를 새 기본값으로 재배치
|
||||
if (state.boardVersion !== BOARD_VERSION) {
|
||||
for (const b of state.buildings)
|
||||
@@ -349,6 +377,9 @@ export const engine = {
|
||||
cat.stats.diligence = Math.min(150, cat.stats.diligence + rand(1, 3));
|
||||
if (cat.level % APT_POINT_EVERY === 0) cat.aptitudePoints += 1;
|
||||
}
|
||||
// 복덩이: 일하는 중 평균 100초에 1냥코인
|
||||
if (cat.passives.includes("lucky") && Math.random() < dt / 100)
|
||||
state.coins = (state.coins ?? 0) + 1;
|
||||
}
|
||||
if (building.type === "kitchen") {
|
||||
// 요리: 재료가 있고 선반에 자리가 있을 때만 진행
|
||||
@@ -364,7 +395,42 @@ export const engine = {
|
||||
building.progress -= recipe.work;
|
||||
}
|
||||
} else {
|
||||
building.stored += produced;
|
||||
// 건물 적재 한도 — 가득 차면 생산 정지 (회수 유도)
|
||||
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;
|
||||
}
|
||||
} 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 (!sold) {
|
||||
building.progress = 1; // 팔 것이 없으면 대기
|
||||
break;
|
||||
}
|
||||
building.progress -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,18 +567,55 @@ export const engine = {
|
||||
return Array.from({ length: count }, () => generateCat(forceApt));
|
||||
},
|
||||
|
||||
/** 후보 중 한 마리 입양 확정 */
|
||||
/** 후보 중 한 마리 입양 확정 (3마리째부터 냥코인 소모) */
|
||||
adoptCandidate(cat: Cat): string | null {
|
||||
if (!state) return null;
|
||||
if (state.cats.length >= state.hall.capacity)
|
||||
return "수용 인원이 가득 찼습니다 (회관 증축 필요)";
|
||||
const cost = adoptCost(state);
|
||||
if ((state.coins ?? 0) < cost)
|
||||
return `냥코인이 부족합니다 (필요: 🪙${cost})`;
|
||||
state.coins = (state.coins ?? 0) - cost;
|
||||
state.cats.push(cat);
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 입양 재추첨 비용 지불 */
|
||||
payReroll(): string | null {
|
||||
if (!state) return null;
|
||||
if ((state.coins ?? 0) < REROLL_COST)
|
||||
return `냥코인이 부족합니다 (필요: 🪙${REROLL_COST})`;
|
||||
state.coins = (state.coins ?? 0) - REROLL_COST;
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 즉시 완공 (남은 10초당 1냥코인) */
|
||||
rushConstruction(buildingId: string): string | null {
|
||||
if (!state) return null;
|
||||
const b = state.buildings.find((x) => x.id === buildingId);
|
||||
if (!b || b.constructionUntil === undefined) return null;
|
||||
const remain = Math.max(0, b.constructionUntil - Date.now());
|
||||
const cost = Math.max(1, Math.ceil(remain / 10000) * RUSH_COST_PER_10S);
|
||||
if ((state.coins ?? 0) < cost)
|
||||
return `냥코인이 부족합니다 (필요: 🪙${cost})`;
|
||||
state.coins = (state.coins ?? 0) - cost;
|
||||
b.constructionUntil = Date.now() - 1;
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 장터 판매 품목 설정 */
|
||||
setSellKey(marketId: string, 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;
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 공통 건설: 상점 구매 후 선택한 타일에 배치 (비용은 배치 확정 시 차감) */
|
||||
build(
|
||||
type: "dock" | "lumberyard" | "quarry" | "kitchen" | "cat_house" | "sun_deck",
|
||||
type: BuildingType,
|
||||
pos: { i: number; j: number },
|
||||
): string | null {
|
||||
if (!state) return null;
|
||||
@@ -571,6 +674,7 @@ export const engine = {
|
||||
consume(state, def.cost);
|
||||
state.decors ??= [];
|
||||
state.tutorialStep ??= 99; // 기존 마을은 튜토리얼 생략
|
||||
state.coins ??= 0;
|
||||
// 같은 타일의 기존 장식 제거 후 배치
|
||||
state.decors = state.decors.filter(
|
||||
(d) => !(d.pos.i === pos.i && d.pos.j === pos.j),
|
||||
@@ -682,14 +786,18 @@ export const engine = {
|
||||
cat.assignedTo = null;
|
||||
},
|
||||
|
||||
collect(buildingId: string) {
|
||||
if (!state) return;
|
||||
collect(buildingId: string): string | null {
|
||||
if (!state) return null;
|
||||
const building = state.buildings.find((b) => b.id === buildingId);
|
||||
if (!building || !building.resource) return; // 부엌은 회수 대상 아님 (고양이가 직접 먹음)
|
||||
const amount = Math.floor(building.stored);
|
||||
if (amount <= 0) return;
|
||||
if (!building || !building.resource) return null; // 부엌은 회수 대상 아님
|
||||
const cap = storageCap(state);
|
||||
const space = Math.max(0, cap - Math.floor(state.resources[building.resource]));
|
||||
const amount = Math.min(Math.floor(building.stored), space);
|
||||
if (amount <= 0)
|
||||
return space <= 0 ? "창고가 가득 찼어요! 장터에서 팔거나 창고를 지어주세요" : null;
|
||||
building.stored -= amount;
|
||||
state.resources[building.resource] += amount;
|
||||
return null;
|
||||
},
|
||||
|
||||
save() {
|
||||
|
||||
새 이슈에서 참조
사용자 차단