건설 방식 개편: 상점 구매 + 자유 배치 (클래시 오브 클랜 방식)
- 건물 위치가 게임 상태(building.pos)로 이동, 구세이브 자동 마이그레이션 - 🛒 상점 패널: 건물 목록·비용·해금 조건, 구매 → 배치 모드 진입 - 배치 모드: 배치 가능 타일 하이라이트, 호버 타일 초록/빨강 + 고스트, 클릭 확정(비용은 확정 시 차감), ESC/취소 버튼 - 배치 규칙: 물·나무·회관·기존 건물 차단, 부두는 물가 열 전용 - 기존 건물 📍 이동 기능 (건물 패널), 이동 시 배치된 고양이가 새 일터로 걸어감 - 공터(🚧) 방식 제거 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
@@ -86,6 +86,33 @@ export const PASSIVES: PassiveDef[] = [
|
||||
{ key: "workaholic", name: "일중독", kind: "tradeoff", desc: "작업 속도 +60%, 행복도 감소 +40%", speedMul: 1.6 },
|
||||
];
|
||||
|
||||
// ---------- 마을 보드 ----------
|
||||
|
||||
import type { BuildingType } from "./types";
|
||||
|
||||
export const BOARD_COLS = 11;
|
||||
export const BOARD_ROWS = 11;
|
||||
export const HALL_TILE = { i: 5, j: 3 };
|
||||
export const TREE_TILES: Array<{ i: number; j: number }> = [
|
||||
{ i: 0, j: 7 }, { i: 1, j: 9 }, { i: 3, j: 9 }, { i: 0, j: 9 },
|
||||
{ i: 1, j: 6 }, { i: 3, j: 7 },
|
||||
];
|
||||
export const isWaterTile = (i: number) => i >= BOARD_COLS - 1;
|
||||
|
||||
// 배치 규칙: 부두는 물가(강 바로 옆 열)에만
|
||||
export type PlacementRule = "land" | "nearWater";
|
||||
export const PLACEMENT_RULES: Partial<Record<BuildingType, PlacementRule>> = {
|
||||
dock: "nearWater",
|
||||
};
|
||||
|
||||
// 구버전 세이브(위치 없음) 마이그레이션용 기본 위치
|
||||
export const DEFAULT_BUILDING_POS: Record<string, { i: number; j: number }> = {
|
||||
dock_1: { i: 9, j: 5 },
|
||||
lumberyard_1: { i: 2, j: 8 },
|
||||
kitchen_1: { i: 6, j: 7 },
|
||||
quarry_1: { i: 1, j: 2 },
|
||||
};
|
||||
|
||||
// ---------- 성장 ----------
|
||||
|
||||
export const EXP_PER_LEVEL = 90; // 레벨업 필요 경험치 = 레벨 × 90
|
||||
|
||||
+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;
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface Building {
|
||||
type: BuildingType;
|
||||
name: string;
|
||||
emoji: string;
|
||||
pos: { i: number; j: number }; // 보드 타일 좌표 (자유 배치)
|
||||
level: number;
|
||||
maxSlots: number;
|
||||
aptitude: AptitudeKey; // 필요 적성
|
||||
|
||||
새 이슈에서 참조
사용자 차단