건설 방식 개편: 상점 구매 + 자유 배치 (클래시 오브 클랜 방식)
- 건물 위치가 게임 상태(building.pos)로 이동, 구세이브 자동 마이그레이션 - 🛒 상점 패널: 건물 목록·비용·해금 조건, 구매 → 배치 모드 진입 - 배치 모드: 배치 가능 타일 하이라이트, 호버 타일 초록/빨강 + 고스트, 클릭 확정(비용은 확정 시 차감), ESC/취소 버튼 - 배치 규칙: 물·나무·회관·기존 건물 차단, 부두는 물가 열 전용 - 기존 건물 📍 이동 기능 (건물 패널), 이동 시 배치된 고양이가 새 일터로 걸어감 - 공터(🚧) 방식 제거 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+47
-2
@@ -3,20 +3,27 @@ import {
|
||||
APT_POINT_EVERY,
|
||||
APTITUDE_RATE,
|
||||
BASE_RATE,
|
||||
BOARD_COLS,
|
||||
BOARD_ROWS,
|
||||
BREEDS,
|
||||
BUILD_DEFS,
|
||||
CAT_NAMES,
|
||||
DEFAULT_BUILDING_POS,
|
||||
EAT_THRESHOLD,
|
||||
EXP_PER_LEVEL,
|
||||
FULLNESS_DECAY_PER_SEC,
|
||||
HALL_LEVELS,
|
||||
HALL_TILE,
|
||||
KITCHEN_UPGRADE_COST,
|
||||
mealCap,
|
||||
PASSIVES,
|
||||
PLACEMENT_RULES,
|
||||
RECIPES,
|
||||
TREE_TILES,
|
||||
type BreedDef,
|
||||
type RecipeDef,
|
||||
} from "./data";
|
||||
import type { BuildingType } from "./types";
|
||||
|
||||
const SAVE_KEY = "cat-village-save";
|
||||
const RES_LABEL: Record<string, string> = { fish: "생선", wood: "목재", stone: "돌" };
|
||||
@@ -91,11 +98,13 @@ function createBuildings(): Building[] {
|
||||
return [
|
||||
{
|
||||
id: "dock_1", type: "dock", name: "부두", emoji: "⚓",
|
||||
pos: { ...DEFAULT_BUILDING_POS.dock_1 },
|
||||
level: 1, maxSlots: 3, aptitude: "fishing", resource: "fish",
|
||||
assigned: [], stored: 0,
|
||||
},
|
||||
{
|
||||
id: "lumberyard_1", type: "lumberyard", name: "벌목장", emoji: "🪓",
|
||||
pos: { ...DEFAULT_BUILDING_POS.lumberyard_1 },
|
||||
level: 1, maxSlots: 3, aptitude: "logging", resource: "wood",
|
||||
assigned: [], stored: 0,
|
||||
},
|
||||
@@ -150,6 +159,26 @@ function passiveFoodMul(cat: Cat): number {
|
||||
return mul;
|
||||
}
|
||||
|
||||
/** 해당 타일에 건물을 놓을 수 있는가 (이동 시 excludeId로 자기 자신 제외) */
|
||||
export function canPlaceAt(
|
||||
s: GameState,
|
||||
type: BuildingType,
|
||||
i: number,
|
||||
j: number,
|
||||
excludeId?: string,
|
||||
): boolean {
|
||||
if (!Number.isInteger(i) || !Number.isInteger(j)) return false;
|
||||
// 물 열 제외한 보드 범위
|
||||
if (i < 0 || j < 0 || i >= BOARD_COLS - 1 || j >= BOARD_ROWS) return false;
|
||||
const rule = PLACEMENT_RULES[type] ?? "land";
|
||||
if (rule === "nearWater" && i !== BOARD_COLS - 2) return false; // 부두는 물가 열만
|
||||
if (HALL_TILE.i === i && HALL_TILE.j === j) return false;
|
||||
if (TREE_TILES.some((t) => t.i === i && t.j === j)) return false;
|
||||
if (s.buildings.some((b) => b.id !== excludeId && b.pos.i === i && b.pos.j === j))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 현재 해금된 레시피 중 가장 마지막 것 (부엌이 요리할 메뉴) */
|
||||
export function activeRecipe(s: GameState): RecipeDef {
|
||||
const kitchenLv = s.buildings.find((b) => b.type === "kitchen")?.level ?? 1;
|
||||
@@ -187,6 +216,8 @@ export const engine = {
|
||||
// 구버전 세이브 마이그레이션 (필드 보강)
|
||||
state.resources.stone ??= 0;
|
||||
for (const cat of state.cats) cat.aptitudePoints ??= 0;
|
||||
for (const b of state.buildings)
|
||||
b.pos ??= { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 5 }) };
|
||||
return state;
|
||||
},
|
||||
|
||||
@@ -266,8 +297,8 @@ export const engine = {
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 공통 건설: BUILD_DEFS 기반 (부엌·채석장 …) */
|
||||
build(type: "quarry" | "kitchen"): string | null {
|
||||
/** 공통 건설: 상점 구매 후 선택한 타일에 배치 (비용은 배치 확정 시 차감) */
|
||||
build(type: "quarry" | "kitchen", pos: { i: number; j: number }): string | null {
|
||||
if (!state) return null;
|
||||
const def = BUILD_DEFS.find((d) => d.type === type);
|
||||
if (!def) return null;
|
||||
@@ -275,6 +306,8 @@ export const engine = {
|
||||
return `${def.name}은(는) 이미 지어져 있습니다`;
|
||||
if (state.hall.level < def.reqHallLv)
|
||||
return `회관 Lv${def.reqHallLv}부터 지을 수 있습니다`;
|
||||
if (!canPlaceAt(state, def.type, pos.i, pos.j))
|
||||
return "그곳에는 지을 수 없습니다";
|
||||
const missing = Object.entries(def.cost).filter(
|
||||
([key, amount]) =>
|
||||
state!.resources[key as keyof GameState["resources"]] < (amount ?? 0),
|
||||
@@ -286,12 +319,24 @@ export const engine = {
|
||||
consume(state, def.cost);
|
||||
state.buildings.push({
|
||||
id: def.id, type: def.type, name: def.name, emoji: def.emoji,
|
||||
pos: { i: pos.i, j: pos.j },
|
||||
level: 1, maxSlots: def.maxSlots, aptitude: def.aptitude,
|
||||
resource: def.resource, assigned: [], stored: 0, progress: 0,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 건물 이동 (자유 배치) */
|
||||
moveBuilding(id: string, pos: { i: number; j: number }): string | null {
|
||||
if (!state) return null;
|
||||
const building = state.buildings.find((b) => b.id === id);
|
||||
if (!building) return null;
|
||||
if (!canPlaceAt(state, building.type, pos.i, pos.j, id))
|
||||
return "그곳으로는 옮길 수 없습니다";
|
||||
building.pos = { i: pos.i, j: pos.j };
|
||||
return null;
|
||||
},
|
||||
|
||||
/** 부엌 증축 Lv1→2: 훈제 생선 해금 조건 + 선반 확장 */
|
||||
upgradeKitchen(): string | null {
|
||||
if (!state) return null;
|
||||
|
||||
새 이슈에서 참조
사용자 차단