보드 확장 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>
이 커밋은 다음에 포함됨:
+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;
|
||||
},
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface Building {
|
||||
|
||||
export interface GameState {
|
||||
version: 1;
|
||||
boardVersion?: number; // 보드 개편 마이그레이션 마커
|
||||
resources: Record<ResourceKey, number>;
|
||||
cats: Cat[];
|
||||
buildings: Building[];
|
||||
|
||||
새 이슈에서 참조
사용자 차단