- 보드 11×11 → 15×15, 강을 동쪽 2열로 확장 (흐름·반짝임도 2열) - 건물 풋프린트: 회관 3×3, 부두·벌목장·채석장·부엌 2×2 (pos=좌상단 앵커) 렌더·클릭 영역·작업 위치(반경)·산책 충돌 전부 풋프린트 기반 - 배치: canPlaceAt이 풋프린트 전 타일 검사 + 사각 겹침, 호버 시 풋프린트 전체 하이라이트 + 실제 크기 고스트 - 부두: onWater 규칙(물 타일 위에만), 이미지 좌우 반전(강 방향) - 보드 버전(v2) 마이그레이션: 기존 세이브 건물을 새 기본 위치로 재배치 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
234 라인
9.2 KiB
TypeScript
234 라인
9.2 KiB
TypeScript
import type { AptitudeKey } from "./types";
|
||
|
||
// 적성 레벨별 초당 작업량 (기획서 4.4) — BASE_RATE로 전체 속도 스케일 조정
|
||
export const APTITUDE_RATE = [0, 1.0, 2.2, 4.8, 10.0, 21.0, 45.0];
|
||
export const BASE_RATE = 0.2;
|
||
|
||
// 품종별 스프라이트 폴더 (public/assets/cats/{폴더}/). 미등록 품종은 기본(오렌지 태비) 사용
|
||
export const CAT_SPRITE_DIR: Record<string, string> = {
|
||
korean_shorthair: "korean_shorthair",
|
||
munchkin: "munchkin",
|
||
norwegian_forest: "norwegian_forest",
|
||
bengal: "bengal",
|
||
russian_blue: "russian_blue",
|
||
persian: "persian",
|
||
sphynx: "sphynx",
|
||
maine_coon: "maine_coon",
|
||
};
|
||
|
||
export type CatFrame = "idle" | "walk1" | "walk2" | "work" | "cook" | "sleep";
|
||
|
||
export function catSprite(breed: string, frame: CatFrame): string {
|
||
const dir = CAT_SPRITE_DIR[breed];
|
||
return dir ? `/assets/cats/${dir}/${frame}.png` : `/assets/cats/${frame}.png`;
|
||
}
|
||
|
||
// 건물 일러스트 (public/assets/buildings/)
|
||
export const BUILDING_IMG: Record<string, string> = {
|
||
hall: "/assets/buildings/hall.png",
|
||
dock: "/assets/buildings/dock.png",
|
||
lumberyard: "/assets/buildings/lumberyard.png",
|
||
quarry: "/assets/buildings/quarry.png",
|
||
kitchen: "/assets/buildings/kitchen.png",
|
||
};
|
||
|
||
// 적성별 도구 일러스트 (public/assets/tools/)
|
||
export const TOOL_IMG: Record<AptitudeKey, string> = {
|
||
fishing: "/assets/tools/fishing.png",
|
||
logging: "/assets/tools/logging.png",
|
||
mining: "/assets/tools/mining.png",
|
||
foraging: "/assets/tools/foraging.png",
|
||
cooking: "/assets/tools/cooking.png",
|
||
crafting: "/assets/tools/crafting.png",
|
||
grooming: "/assets/tools/grooming.png",
|
||
};
|
||
|
||
export const APTITUDE_LABEL: Record<AptitudeKey, string> = {
|
||
fishing: "낚시",
|
||
logging: "벌목",
|
||
mining: "채굴",
|
||
foraging: "채집",
|
||
cooking: "요리",
|
||
crafting: "제작",
|
||
grooming: "손질",
|
||
};
|
||
|
||
export interface BreedDef {
|
||
key: string;
|
||
name: string;
|
||
emoji: string;
|
||
rarity: number; // 별 개수
|
||
weight: number; // 추첨 가중치
|
||
bias: AptitudeKey[]; // 적성 편향
|
||
aptitudeCap: number;
|
||
appetiteMod: number;
|
||
}
|
||
|
||
// 기획서 4.2 품종표
|
||
export const BREEDS: BreedDef[] = [
|
||
{ key: "korean_shorthair", name: "코리안 숏헤어", emoji: "🐱", rarity: 1, weight: 35, bias: [], aptitudeCap: 5, appetiteMod: -1 },
|
||
{ key: "munchkin", name: "먼치킨", emoji: "😺", rarity: 1, weight: 25, bias: ["foraging", "grooming"], aptitudeCap: 5, appetiteMod: 0 },
|
||
{ key: "norwegian_forest", name: "노르웨이 숲", emoji: "🐈", rarity: 2, weight: 15, bias: ["logging", "mining"], aptitudeCap: 5, appetiteMod: 1 },
|
||
{ key: "bengal", name: "뱅갈", emoji: "🐆", rarity: 2, weight: 10, bias: ["fishing", "foraging"], aptitudeCap: 5, appetiteMod: 0 },
|
||
{ key: "russian_blue", name: "러시안 블루", emoji: "🐈⬛", rarity: 2, weight: 8, bias: ["crafting", "cooking"], aptitudeCap: 5, appetiteMod: 0 },
|
||
{ key: "persian", name: "페르시안", emoji: "😸", rarity: 3, weight: 4, bias: ["cooking", "grooming"], aptitudeCap: 5, appetiteMod: 0 },
|
||
{ key: "sphynx", name: "스핑크스", emoji: "🐯", rarity: 3, weight: 2, bias: ["crafting", "mining"], aptitudeCap: 5, appetiteMod: 0 },
|
||
{ key: "maine_coon", name: "메인쿤", emoji: "🦁", rarity: 4, weight: 1, bias: ["logging", "mining"], aptitudeCap: 6, appetiteMod: 2 },
|
||
];
|
||
|
||
export interface PassiveDef {
|
||
key: string;
|
||
name: string;
|
||
kind: "positive" | "negative" | "tradeoff";
|
||
desc: string;
|
||
speedMul: number; // 1단계에서는 작업 속도 효과만 적용
|
||
}
|
||
|
||
// 기획서 5장 중 1단계에서 의미 있는 패시브 위주
|
||
export const PASSIVES: PassiveDef[] = [
|
||
{ key: "diligent", name: "부지런함", kind: "positive", desc: "작업 속도 +25%", speedMul: 1.25 },
|
||
{ key: "sturdy", name: "튼튼함", kind: "positive", desc: "체력 소모 -30%", speedMul: 1 },
|
||
{ key: "small_eater", name: "소식가", kind: "positive", desc: "식량 소모 -30%", speedMul: 1 },
|
||
{ key: "lucky", name: "복덩이", kind: "positive", desc: "확률적으로 냥코인 획득", speedMul: 1 },
|
||
{ key: "lazy", name: "게으름뱅이", kind: "negative", desc: "작업 속도 -30%", speedMul: 0.7 },
|
||
{ key: "big_eater", name: "대식가", kind: "negative", desc: "식량 소모 +50%", speedMul: 1 },
|
||
{ key: "workaholic", name: "일중독", kind: "tradeoff", desc: "작업 속도 +60%, 행복도 감소 +40%", speedMul: 1.6 },
|
||
];
|
||
|
||
// ---------- 마을 보드 ----------
|
||
|
||
import type { BuildingType } from "./types";
|
||
|
||
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: 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 },
|
||
];
|
||
// 강은 동쪽 2열
|
||
export const isWaterTile = (i: number) => i >= BOARD_COLS - 2;
|
||
|
||
// 건물 풋프린트 (타일 단위, 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: "onWater",
|
||
};
|
||
|
||
// 보드 v2 기본 위치 (마이그레이션 시 재배치)
|
||
export const DEFAULT_BUILDING_POS: Record<string, { i: number; j: number }> = {
|
||
dock_1: { i: 13, j: 6 },
|
||
lumberyard_1: { i: 2, j: 9 },
|
||
kitchen_1: { i: 8, j: 8 },
|
||
quarry_1: { i: 1, j: 2 },
|
||
};
|
||
|
||
// ---------- 성장 ----------
|
||
|
||
export const EXP_PER_LEVEL = 90; // 레벨업 필요 경험치 = 레벨 × 90
|
||
export const APT_POINT_EVERY = 3; // 3레벨마다 적성 포인트 1
|
||
|
||
// ---------- 건설 가능한 건물 ----------
|
||
|
||
export interface BuildDef {
|
||
id: string; // 건물 인스턴스 id (단일 건설)
|
||
type: "quarry" | "kitchen";
|
||
name: string;
|
||
emoji: string;
|
||
aptitude: AptitudeKey;
|
||
resource?: "fish" | "wood" | "stone";
|
||
maxSlots: number;
|
||
cost: Partial<Record<"fish" | "wood" | "stone", number>>;
|
||
reqHallLv: number;
|
||
desc: string;
|
||
}
|
||
|
||
export const BUILD_DEFS: BuildDef[] = [
|
||
{
|
||
id: "kitchen_1", type: "kitchen", name: "부엌", emoji: "🍳",
|
||
aptitude: "cooking", maxSlots: 2, cost: { wood: 60 }, reqHallLv: 1,
|
||
desc: "요리 적성 고양이가 생선으로 요리를 만들고, 배고픈 고양이들이 알아서 먹으러 옵니다.",
|
||
},
|
||
{
|
||
id: "quarry_1", type: "quarry", name: "채석장", emoji: "⛏️",
|
||
aptitude: "mining", resource: "stone", maxSlots: 3, cost: { wood: 120 }, reqHallLv: 2,
|
||
desc: "채굴 적성 고양이가 돌을 캡니다. 돌은 회관 증축과 중급 건물에 필요합니다.",
|
||
},
|
||
];
|
||
|
||
// ---------- 요리 (부엌) ----------
|
||
|
||
export interface RecipeDef {
|
||
key: string;
|
||
name: string;
|
||
emoji: string;
|
||
input: Partial<Record<"fish" | "wood" | "stone", number>>; // 재료 (추후 획득처 확장)
|
||
work: number; // 요리 1개당 필요 작업량
|
||
fullness: number; // 먹었을 때 포만도 회복량
|
||
reqHallLv: number; // 해금 조건: 회관 레벨
|
||
reqKitchenLv: number; // 해금 조건: 부엌 레벨
|
||
}
|
||
|
||
export const RECIPES: RecipeDef[] = [
|
||
{
|
||
key: "grilled_fish", name: "물고기 구이", emoji: "🍖",
|
||
input: { fish: 2 }, work: 3, fullness: 40,
|
||
reqHallLv: 1, reqKitchenLv: 1,
|
||
},
|
||
{
|
||
key: "smoked_fish", name: "훈제 생선", emoji: "🐟",
|
||
input: { fish: 3 }, work: 6, fullness: 70,
|
||
reqHallLv: 3, reqKitchenLv: 2, // 아직 잠김 — 회관·부엌 증축으로 해금 예정
|
||
},
|
||
];
|
||
|
||
export const KITCHEN_MEAL_CAPS = [10, 15]; // 부엌 레벨별 선반 보관 수
|
||
export const KITCHEN_UPGRADE_COST = { wood: 150, stone: 60 }; // Lv1→2
|
||
export const mealCap = (kitchenLv: number) =>
|
||
KITCHEN_MEAL_CAPS[kitchenLv - 1] ?? KITCHEN_MEAL_CAPS[KITCHEN_MEAL_CAPS.length - 1];
|
||
export const EAT_THRESHOLD = 65; // 이 포만도 이하면 먹으러 감
|
||
export const FULLNESS_DECAY_PER_SEC = 0.03; // × 식욕 수치
|
||
|
||
// ---------- 마을 회관 증축 ----------
|
||
|
||
export interface HallLevelDef {
|
||
capacity: number;
|
||
cost: Partial<Record<"fish" | "wood" | "stone", number>>;
|
||
reqCat?: { level: number; count: number };
|
||
reqBuildings?: Array<{ type: "quarry" | "kitchen"; level?: number; label: string }>;
|
||
locked?: string; // 아직 구현 안 된 선행 요소 안내
|
||
}
|
||
|
||
export const HALL_LEVELS: Record<number, HallLevelDef> = {
|
||
2: { capacity: 5, cost: { wood: 100, fish: 80 }, reqCat: { level: 3, count: 1 } },
|
||
3: {
|
||
capacity: 8, cost: { wood: 250, stone: 150 },
|
||
reqCat: { level: 5, count: 2 },
|
||
reqBuildings: [
|
||
{ type: "quarry", label: "채석장 건설" },
|
||
{ type: "kitchen", level: 2, label: "부엌 Lv2" },
|
||
],
|
||
},
|
||
4: {
|
||
capacity: 12, cost: { stone: 400 }, reqCat: { level: 8, count: 3 },
|
||
locked: "목공소(목판)가 추가된 뒤 해금됩니다",
|
||
},
|
||
};
|
||
|
||
export const CAT_NAMES = [
|
||
"치즈", "나비", "콩이", "보리", "두부", "마루", "모찌", "호두",
|
||
"감자", "앙꼬", "밤이", "구름", "초코", "우유", "깨비", "솜이",
|
||
"달이", "별이", "쿠키", "만두", "생강", "참깨", "누룽지", "조약돌",
|
||
];
|