보드 확장 15×15 + 건물 풋프린트 + 부두 물 위 배치

- 보드 11×11 → 15×15, 강을 동쪽 2열로 확장 (흐름·반짝임도 2열)
- 건물 풋프린트: 회관 3×3, 부두·벌목장·채석장·부엌 2×2 (pos=좌상단 앵커)
  렌더·클릭 영역·작업 위치(반경)·산책 충돌 전부 풋프린트 기반
- 배치: canPlaceAt이 풋프린트 전 타일 검사 + 사각 겹침, 호버 시 풋프린트
  전체 하이라이트 + 실제 크기 고스트
- 부두: onWater 규칙(물 타일 위에만), 이미지 좌우 반전(강 방향)
- 보드 버전(v2) 마이그레이션: 기존 세이브 건물을 새 기본 위치로 재배치

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 16:01:32 +09:00
co-authored by Claude Fable 5
부모 256d367bcd
커밋 1f858352a0
5개의 변경된 파일과 160개의 추가작업 그리고 86개의 파일을 삭제
+25 -13
파일 보기
@@ -99,26 +99,38 @@ export const PASSIVES: PassiveDef[] = [
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 BOARD_COLS = 15;
export const BOARD_ROWS = 15;
export const BOARD_VERSION = 2; // 보드 개편 시 증가 → 세이브 위치 리셋
export const HALL_TILE = { i: 6, 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 },
{ i: 0, j: 9 }, { i: 1, j: 11 }, { i: 3, j: 12 }, { i: 0, j: 13 },
{ i: 1, j: 8 }, { i: 4, j: 13 }, { i: 11, j: 1 }, { i: 2, j: 0 },
];
export const isWaterTile = (i: number) => i >= BOARD_COLS - 1;
// 강은 동쪽 2열
export const isWaterTile = (i: number) => i >= BOARD_COLS - 2;
// 배치 규칙: 부두는 물가(강 바로 옆 열)에만
export type PlacementRule = "land" | "nearWater";
// 건물 풋프린트 (타일 단위, pos는 좌상단 앵커)
export const FOOTPRINTS: Record<string, { w: number; h: number }> = {
hall: { w: 3, h: 3 },
dock: { w: 2, h: 2 },
lumberyard: { w: 2, h: 2 },
quarry: { w: 2, h: 2 },
kitchen: { w: 2, h: 2 },
};
export const footprintOf = (type: string) => FOOTPRINTS[type] ?? { w: 1, h: 1 };
// 배치 규칙: 부두는 물 위에만
export type PlacementRule = "land" | "onWater";
export const PLACEMENT_RULES: Partial<Record<BuildingType, PlacementRule>> = {
dock: "nearWater",
dock: "onWater",
};
// 구버전 세이브(위치 없음) 마이그레이션용 기본 위치
// 보드 v2 기본 위치 (마이그레이션 시 재배치)
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 },
dock_1: { i: 13, j: 6 },
lumberyard_1: { i: 2, j: 9 },
kitchen_1: { i: 8, j: 8 },
quarry_1: { i: 1, j: 2 },
};
+40 -8
파일 보기
@@ -5,6 +5,7 @@ import {
BASE_RATE,
BOARD_COLS,
BOARD_ROWS,
BOARD_VERSION,
BREEDS,
BUILD_DEFS,
CAT_NAMES,
@@ -13,7 +14,9 @@ import {
EXP_PER_LEVEL,
FULLNESS_DECAY_PER_SEC,
HALL_LEVELS,
footprintOf,
HALL_TILE,
isWaterTile,
KITCHEN_UPGRADE_COST,
mealCap,
PASSIVES,
@@ -159,7 +162,17 @@ function passiveFoodMul(cat: Cat): number {
return mul;
}
/** 해당 타일에 건물을 놓을 수 있는가 (이동 시 excludeId로 자기 자신 제외) */
/** 사각 풋프린트 겹침 검사 */
function rectsOverlap(
a: { i: number; j: number; w: number; h: number },
b: { i: number; j: number; w: number; h: number },
): boolean {
return (
a.i < b.i + b.w && b.i < a.i + a.w && a.j < b.j + b.h && b.j < a.j + a.h
);
}
/** 해당 앵커(좌상단)에 건물 풋프린트를 놓을 수 있는가 */
export function canPlaceAt(
s: GameState,
type: BuildingType,
@@ -168,14 +181,27 @@ export function canPlaceAt(
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 fp = footprintOf(type);
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;
// 풋프린트 전 타일 검사
for (let di = 0; di < fp.w; di++) {
for (let dj = 0; dj < fp.h; dj++) {
const x = i + di;
const y = j + dj;
if (x < 0 || y < 0 || x >= BOARD_COLS || y >= BOARD_ROWS) return false;
const water = isWaterTile(x);
if (rule === "onWater" ? !water : water) return false;
if (TREE_TILES.some((t) => t.i === x && t.j === y)) return false;
}
}
const rect = { i, j, w: fp.w, h: fp.h };
const hallFp = footprintOf("hall");
if (rectsOverlap(rect, { ...HALL_TILE, w: hallFp.w, h: hallFp.h })) return false;
for (const b of s.buildings) {
if (b.id === excludeId) continue;
const bfp = footprintOf(b.type);
if (rectsOverlap(rect, { ...b.pos, w: bfp.w, h: bfp.h })) return false;
}
return true;
}
@@ -218,6 +244,12 @@ export const engine = {
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 }) };
// 보드 개편(크기·풋프린트 변경) 시 건물 위치를 새 기본값으로 재배치
if (state.boardVersion !== BOARD_VERSION) {
for (const b of state.buildings)
b.pos = { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 6 }) };
state.boardVersion = BOARD_VERSION;
}
return state;
},
+1
파일 보기
@@ -51,6 +51,7 @@ export interface Building {
export interface GameState {
version: 1;
boardVersion?: number; // 보드 개편 마이그레이션 마커
resources: Record<ResourceKey, number>;
cats: Cat[];
buildings: Building[];