From 1ff8d0b8ca4326b5f54ee1a3f9f88eb1d50ba0f1 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Aug 2026 16:20:37 +0900 Subject: [PATCH] =?UTF-8?q?Phase=20B:=20=EC=B2=B4=EB=A0=A5=C2=B7=EA=B0=95?= =?UTF-8?q?=EC=A0=9C=20=ED=9C=B4=EC=8B=9D=C2=B7=ED=96=89=EB=B3=B5=EB=8F=84?= =?UTF-8?q?=C2=B7=ED=8C=8C=EC=97=85=C2=B7=EA=B3=A0=EC=96=91=EC=9D=B4=20?= =?UTF-8?q?=EC=A7=91=C2=B7=ED=96=87=EB=B3=95=20=EB=8D=B0=ED=81=AC=C2=B7?= =?UTF-8?q?=EC=98=A4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=A0=95=EC=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 체력(energy): 작업 중 분당 -2(튼튼함 -30%), 소진 시 강제 휴식 — 배치 해제 후 수면 포즈로 회복(분당 +20, 고양이 집 +30), 완충 시 자동 복귀 - 행복도: 포만 80↑ +1/분, 굶주림 -4/분, 고양이 집 부족 -2/분(바닥 25), 휴식 +3/분, 햇볕 데크 회복 1.5배, 인기쟁이 동료 버프 효과: 80↑ 속도 +10%, 50 미만 -30%, 20 미만 파업(배치 해제·재배치 불가) - 파업: 💢 배회, 행복도 40 회복 시 이전 일터로 자동 복귀 - 새 패시브: 낙천적(-40%)·까다로움(+50%)·인기쟁이 / 튼튼함·일중독 실효과화 - 새 건물: 고양이 집(정원 4, 목재100+돌20)·햇볕 데크(목재120+돌40) — 배치 없는 패시브 건물(전용 패널), 회관 Lv3 해금, 이모지 렌더(일러스트 대기) - 오프라인 정산: 최대 8시간을 60초 단위 시뮬레이션, 복귀 시 토스트 - UI: 고양이 카드에 체력(⚡)·행복도(💗) 바, 휴식/파업 상태 표시 - 뷰: 휴식 시 고양이 집으로 이동 후 수면 포즈+💤, 파업 시 💢 Co-Authored-By: Claude Fable 5 --- src/components/game/Game.tsx | 113 +++++++++++++++++-- src/components/game/VillageView.tsx | 79 +++++++++---- src/lib/game/data.ts | 28 ++++- src/lib/game/engine.ts | 169 ++++++++++++++++++++++++++-- src/lib/game/types.ts | 11 +- 5 files changed, 355 insertions(+), 45 deletions(-) diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index 4de9673..9487075 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -57,6 +57,20 @@ export default function Game() { if (msgTimer.current) clearTimeout(msgTimer.current); }, []); + // 오프라인 정산 알림 (1회) + const offlineShown = useRef(false); + useEffect(() => { + if (!state || offlineShown.current) return; + offlineShown.current = true; + const mins = engine.consumeOfflineReport(); + if (mins) { + setMessage( + `🌙 ${mins >= 60 ? `${Math.floor(mins / 60)}시간 ${mins % 60}분` : `${mins}분`} 동안 자리를 비웠어요 — 마을은 계속 돌아갔답니다!`, + ); + msgTimer.current = setTimeout(() => setMessage(null), 6000); + } + }, [state]); + if (!state) { return (
@@ -85,7 +99,7 @@ export default function Game() { if (!placing) return; const error = placing.moveId ? run(() => engine.moveBuilding(placing.moveId!, { i, j })) - : run(() => engine.build(placing.type as "quarry" | "kitchen", { i, j })); + : run(() => engine.build(placing.type as BuildDef["type"], { i, j })); if (!error) setPlacing(null); }} onCancelPlace={() => setPlacing(null)} @@ -200,8 +214,12 @@ export default function Game() { )} {openedBuilding && ( setPanel(null)} > {/* eslint-disable-next-line @next/next/no-img-element */} - {def.name} + {BUILDING_IMG[def.type] ? ( + /* eslint-disable-next-line @next/next/no-img-element */ + {def.name} + ) : ( + + {def.emoji} + + )}
{def.name}
@@ -499,6 +524,50 @@ function BuildingPanel({ ); } + if (building.maxSlots === 0) { + // 패시브 건물 — 배치 없음 + const restingHere = state.cats.filter((c) => c.state === "resting"); + return ( +
+

+ {BUILD_DEFS.find((d) => d.type === building.type)?.desc} +

+
+ +
+ {building.type === "cat_house" && ( +
+

+ 💤 쉬고 있는 고양이 ({restingHere.length}) +

+ {restingHere.length === 0 && ( +

지금은 모두 기운이 넘쳐요!

+ )} +
+ {restingHere.map((cat) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
{cat.name}
+
+ ⚡ {Math.floor(cat.status.energy)}/{cat.stats.stamina} +
+
+
+ ))} +
+
+ )} +
+ ); + } + return (
{/* 정보 */} @@ -807,6 +876,28 @@ function CatCard({ 🍽️ {Math.floor(cat.status.fullness)} (식욕 {cat.stats.appetite}) +
+
+
+
+
+ + ⚡ {Math.floor(cat.status.energy)}/{cat.stats.stamina} + +
+
+
+
= 20 ? "linear-gradient(90deg,#e88bb8,#f7b7d4)" : undefined }} + /> +
+ + 💗 {Math.floor(cat.status.happiness)} + {fullnessPenalty(cat) < 1 && ( 배고픔 @@ -823,7 +914,11 @@ function CatCard({ )}

- {workplace ? ( + {cat.state === "resting" ? ( + 💤 지쳐서 휴식 중 (회복되면 복귀) + ) : cat.state === "striking" ? ( + 💢 파업 중! 행복도를 올려주세요 + ) : workplace ? ( {workplace.emoji} {workplace.name}에서 일하는 중 diff --git a/src/components/game/VillageView.tsx b/src/components/game/VillageView.tsx index a01d499..fd73ca0 100644 --- a/src/components/game/VillageView.tsx +++ b/src/components/game/VillageView.tsx @@ -303,7 +303,40 @@ export default function VillageView({ } anim.lastFullness = cat.status.fullness; - if (cat.assignedTo) { + if (cat.state === "resting") { + // 고양이 집이 있으면 그 곁에서, 없으면 그 자리에서 잠 + const house = state.buildings.find( + (b) => b.type === "cat_house" && b.constructionUntil === undefined, + ); + if (house) { + const spot = workSpot(house, 0); + if (Math.hypot(anim.tx - spot.x, anim.ty - spot.y) > 4) { + anim.tx = spot.x; + anim.ty = spot.y; + anim.mode = "toWork"; + } + } else if (anim.mode !== "working") { + anim.tx = anim.x; + anim.ty = anim.y; + anim.mode = "working"; // 제자리 수면 (working 모드 재활용: 이동 정지) + } + anim.workBuildingId = null; + // 💤 버블 + if (Math.random() < dt / 4) { + bubblesRef.current.push({ x: anim.x + 10, y: anim.y - 36, born: now, text: "💤" }); + } + } else if (cat.state === "striking") { + anim.workBuildingId = null; + if (anim.mode !== "wander") { + anim.mode = "wander"; + const t = randomWalkTarget(state); + anim.tx = t.x; + anim.ty = t.y; + } + if (Math.random() < dt / 5) { + bubblesRef.current.push({ x: anim.x + 10, y: anim.y - 36, born: now, text: "💢" }); + } + } else if (cat.assignedTo) { const building = state.buildings.find((b) => b.id === cat.assignedTo); const slot = building ? building.assigned.indexOf(cat.id) : 0; const spot = workSpot(building, Math.max(0, slot)); @@ -340,7 +373,7 @@ export default function VillageView({ anim.mode = "working"; } else if (anim.mode === "working") { // 일하는 중 — 생산 말풍선 (약 2.5초에 한 번) - if (Math.random() < dt / 2.5 && cat.assignedTo) { + if (Math.random() < dt / 2.5 && cat.assignedTo && !cat.state) { const b = state.buildings.find((x) => x.id === cat.assignedTo); if (b && catRate(cat, b.aptitude) > 0) { bubblesRef.current.push({ @@ -599,21 +632,16 @@ export default function VillageView({ ctx.fillText(`${b.name} 건설 중`, x, baseY + 17); return; } - const bImg = getImage(BUILDING_IMG[b.type]); + const bImg = BUILDING_IMG[b.type] + ? getImage(BUILDING_IMG[b.type]) + : undefined; if (bImg) { // 건물 일러스트 (자체 잔디 베이스 포함 — 그림자 불필요) const h = (bImg.height / bImg.width) * imgW; ctx.drawImage(bImg, x - imgW / 2, baseY + 4 - h, imgW, h); } else { shadow(x, y + 6, 24); - const img = toolImagesRef.current.get(b.aptitude); - if (img) { - const h = 56; - const w = (img.width / img.height) * h; - ctx.drawImage(img, x - w / 2, y + 8 - h, w, h); - } else { - drawEmoji(b.emoji, x, y + 8, 42); - } + drawEmoji(b.emoji, x, y + 8, TW * fp.w * 0.55); } ctx.font = `13px ${juaFamily}`; ctx.textAlign = "center"; @@ -650,15 +678,18 @@ export default function VillageView({ const atKitchen = working && state.buildings.find((b) => b.id === cat.assignedTo)?.type === "kitchen"; - const frameName: CatFrame = working - ? atKitchen - ? "cook" - : "work" - : moving - ? Math.floor(now / 200) % 2 === 0 - ? "walk1" - : "walk2" - : "idle"; + const frameName: CatFrame = + cat.state === "resting" && !moving + ? "sleep" + : working && !cat.state + ? atKitchen + ? "cook" + : "work" + : moving + ? Math.floor(now / 200) % 2 === 0 + ? "walk1" + : "walk2" + : "idle"; // 품종 스프라이트 → (cook 없으면 work) → 이모지 순 폴백 let sprite = getImage(catSprite(cat.breed, frameName)); if (!sprite && frameName === "cook") @@ -682,7 +713,7 @@ export default function VillageView({ ctx.lineWidth = 1; ctx.fillStyle = "rgba(74,52,24,0.9)"; ctx.fillText(cat.name, anim.x, anim.y + 14); - if (working) drawEmoji("💦", anim.x + 14, anim.y - 30 + bob, 11); + if (working && !cat.state) drawEmoji("💦", anim.x + 14, anim.y - 30 + bob, 11); }, }); } @@ -721,9 +752,9 @@ export default function VillageView({ const cj = hover.j + fp.h / 2 - 0.5; const gx = isoX(ci, cj); const gBase = isoY(ci, cj) + TH / 2 + (fp.h * TH) / 2; - const gImg = - getImage(BUILDING_IMG[placing.type]) ?? - toolImagesRef.current.get(placing.aptitude); + const gImg = BUILDING_IMG[placing.type] + ? getImage(BUILDING_IMG[placing.type]) + : undefined; ctx.globalAlpha = 0.75; if (gImg) { const w = TW * fp.w * 0.94; diff --git a/src/lib/game/data.ts b/src/lib/game/data.ts index dc26527..ec7888e 100644 --- a/src/lib/game/data.ts +++ b/src/lib/game/data.ts @@ -94,6 +94,9 @@ export const PASSIVES: PassiveDef[] = [ { 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 }, + { key: "optimist", name: "낙천적", kind: "positive", desc: "행복도 감소 속도 -40%", speedMul: 1 }, + { key: "picky", name: "까다로움", kind: "negative", desc: "행복도 감소 속도 +50%", speedMul: 1 }, + { key: "popular", name: "인기쟁이", kind: "positive", desc: "같은 건물 동료의 행복도가 서서히 상승", speedMul: 1 }, ]; // ---------- 마을 보드 ---------- @@ -118,6 +121,8 @@ export const FOOTPRINTS: Record = { lumberyard: { w: 2, h: 2 }, quarry: { w: 2, h: 2 }, kitchen: { w: 2, h: 2 }, + cat_house: { w: 2, h: 2 }, + sun_deck: { w: 2, h: 2 }, }; export const footprintOf = (type: string) => FOOTPRINTS[type] ?? { w: 1, h: 1 }; @@ -144,7 +149,8 @@ export const APT_POINT_EVERY = 3; // 3레벨마다 적성 포인트 1 export interface BuildDef { id: string; // 건물 인스턴스 id (단일 건설) - type: "quarry" | "kitchen"; + type: "quarry" | "kitchen" | "cat_house" | "sun_deck"; + kind?: "work" | "passive"; // passive: 고양이 배치 없음 (기본 work) name: string; emoji: string; aptitude: AptitudeKey; @@ -167,6 +173,16 @@ export const BUILD_DEFS: BuildDef[] = [ aptitude: "mining", resource: "stone", maxSlots: 3, cost: { wood: 120 }, reqHallLv: 2, buildTime: 120, desc: "채굴 적성 고양이가 돌을 캡니다. 돌은 회관 증축과 중급 건물에 필요합니다.", }, + { + id: "cat_house_1", type: "cat_house", name: "고양이 집", emoji: "🛖", kind: "passive", + aptitude: "grooming", maxSlots: 0, cost: { wood: 100, stone: 20 }, reqHallLv: 3, buildTime: 90, + desc: "지친 고양이가 푹 쉬는 보금자리. 휴식 회복이 빨라지고(분당 +30) 행복도 하락을 막아줍니다.", + }, + { + id: "sun_deck_1", type: "sun_deck", name: "햇볕 데크", emoji: "🌞", kind: "passive", + aptitude: "grooming", maxSlots: 0, cost: { wood: 120, stone: 40 }, reqHallLv: 3, buildTime: 90, + desc: "따끈한 햇살 아래 뒹굴뒹굴. 마을 전체의 행복도 회복이 1.5배 빨라집니다.", + }, ]; // ---------- 요리 (부엌) ---------- @@ -202,6 +218,16 @@ export const mealCap = (kitchenLv: number) => export const EAT_THRESHOLD = 65; // 이 포만도 이하면 먹으러 감 export const FULLNESS_DECAY_PER_SEC = 0.03; // × 식욕 수치 +// ---------- 체력·행복도 (Phase B) ---------- + +export const ENERGY_DRAIN_PER_SEC = 2 / 60; // 작업 중 체력 소모 (분당 2) +export const REST_RECOVER_PER_SEC = 20 / 60; // 휴식 회복 (분당 20) +export const HOUSE_RECOVER_PER_SEC = 30 / 60; // 고양이 집 휴식 회복 (분당 30) +export const IDLE_RECOVER_PER_SEC = 10 / 60; // 배회 중 자연 회복 (분당 10) +export const CAT_HOUSE_CAPACITY = 4; // 고양이 집 1채당 휴식 정원 +export const STRIKE_THRESHOLD = 20; // 행복도가 이 밑이면 파업 +export const STRIKE_RESUME = 40; // 이 이상 회복되면 복귀 + // ---------- 마을 회관 증축 ---------- export interface HallLevelDef { diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts index d6cc3cf..3d27626 100644 --- a/src/lib/game/engine.ts +++ b/src/lib/game/engine.ts @@ -3,6 +3,13 @@ import { APT_POINT_EVERY, APTITUDE_RATE, BASE_RATE, + CAT_HOUSE_CAPACITY, + ENERGY_DRAIN_PER_SEC, + HOUSE_RECOVER_PER_SEC, + IDLE_RECOVER_PER_SEC, + REST_RECOVER_PER_SEC, + STRIKE_RESUME, + STRIKE_THRESHOLD, BOARD_COLS, BOARD_ROWS, BOARD_VERSION, @@ -74,6 +81,11 @@ function generateCat(forceAptitude?: AptitudeKey): Cat { passives.push(pool.splice(idx, 1)[0].key); } + const stats = { + diligence: rand(50, 150), + stamina: rand(50, 150), + appetite: Math.min(5, Math.max(1, rand(2, 4) + breed.appetiteMod)), + }; return { id: nextId("cat"), name: CAT_NAMES[rand(0, CAT_NAMES.length - 1)], @@ -84,12 +96,8 @@ function generateCat(forceAptitude?: AptitudeKey): Cat { exp: 0, aptitudePoints: 0, aptitude, - stats: { - diligence: rand(50, 150), - stamina: rand(50, 150), - appetite: Math.min(5, Math.max(1, rand(2, 4) + breed.appetiteMod)), - }, - status: { fullness: 100, happiness: 80 }, + stats, + status: { fullness: 100, happiness: 80, energy: stats.stamina }, passives, assignedTo: null, }; @@ -129,6 +137,7 @@ function createInitialState(): GameState { // ---------- 엔진 (모듈 싱글턴, React 밖에서 틱 계산) ---------- let state: GameState | null = null; +let offlineMinutes: number | null = null; function passiveSpeedMul(cat: Cat): number { return cat.passives.reduce((mul, key) => { @@ -150,9 +159,17 @@ export function fullnessPenalty(cat: Cat): number { return 1; } -/** 실효 산출량 (배고픔 페널티 반영) */ +/** 행복도 배율: 80↑ +10%, 20~49 -30% */ +export function happinessMul(cat: Cat): number { + const h = cat.status.happiness; + if (h >= 80) return 1.1; + if (h < 50) return 0.7; + return 1; +} + +/** 실효 산출량 (배고픔·행복도 반영) */ export function effectiveRate(cat: Cat, aptitudeKey: AptitudeKey): number { - return catRate(cat, aptitudeKey) * fullnessPenalty(cat); + return catRate(cat, aptitudeKey) * fullnessPenalty(cat) * happinessMul(cat); } function passiveFoodMul(cat: Cat): number { @@ -246,7 +263,10 @@ export const engine = { if (!state) state = createInitialState(); // 구버전 세이브 마이그레이션 (필드 보강) state.resources.stone ??= 0; - for (const cat of state.cats) cat.aptitudePoints ??= 0; + for (const cat of state.cats) { + cat.aptitudePoints ??= 0; + cat.status.energy ??= cat.stats.stamina; + } for (const b of state.buildings) b.pos ??= { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 5 }) }; // 보드 개편(크기·풋프린트 변경) 시 건물 위치를 새 기본값으로 재배치 @@ -255,9 +275,26 @@ export const engine = { b.pos = { ...(DEFAULT_BUILDING_POS[b.id] ?? { i: 4, j: 6 }) }; state.boardVersion = BOARD_VERSION; } + // 오프라인 정산: 최대 8시간, 60초 단위로 시뮬레이션 + const offlineSec = Math.min( + Math.max((Date.now() - state.updatedAt) / 1000, 0), + 8 * 3600, + ); + if (offlineSec > 120) { + const chunks = Math.floor(offlineSec / 60); + for (let k = 0; k < chunks; k++) this.tick(60); + offlineMinutes = Math.floor(offlineSec / 60); + } return state; }, + /** 오프라인 정산 리포트 (1회성) */ + consumeOfflineReport(): number | null { + const r = offlineMinutes; + offlineMinutes = null; + return r; + }, + tick(dt: number) { if (!state) return; for (const building of state.buildings) { @@ -325,6 +362,108 @@ export const engine = { } } + // ---------- 체력·휴식·행복도 (Phase B) ---------- + const houses = state.buildings.filter( + (b) => b.type === "cat_house" && !isUnderConstruction(b), + ); + const houseCapacity = houses.length * CAT_HOUSE_CAPACITY; + const hasDeck = state.buildings.some( + (b) => b.type === "sun_deck" && !isUnderConstruction(b), + ); + const gainMul = hasDeck ? 1.5 : 1; + const restingCats = state.cats.filter((c) => c.state === "resting"); + + const tryReturn = (cat: Cat) => { + // 휴식·파업 종료 → 이전 일터로 자동 복귀 (자리가 남아 있으면) + const target = cat.returnTo + ? state!.buildings.find((b) => b.id === cat.returnTo) + : undefined; + cat.state = null; + cat.returnTo = null; + if ( + target && + !isUnderConstruction(target) && + target.assigned.length < target.maxSlots && + cat.aptitude[target.aptitude] > 0 + ) { + target.assigned.push(cat.id); + cat.assignedTo = target.id; + } + }; + + for (const cat of state.cats) { + const st = cat.status; + const decayMul = + (cat.passives.includes("optimist") ? 0.6 : 1) * + (cat.passives.includes("picky") ? 1.5 : 1) * + (cat.passives.includes("workaholic") ? 1.4 : 1); + const sturdyMul = cat.passives.includes("sturdy") ? 0.7 : 1; + + // 체력 + if (cat.state === "resting") { + const idx = restingCats.indexOf(cat); + const inHouse = houses.length > 0 && idx >= 0 && idx < houseCapacity; + const recover = inHouse ? HOUSE_RECOVER_PER_SEC : REST_RECOVER_PER_SEC; + st.energy = Math.min(cat.stats.stamina, st.energy + recover * dt); + st.happiness = Math.min(100, st.happiness + (3 / 60) * gainMul * dt); + if (st.energy >= cat.stats.stamina) tryReturn(cat); + } else if (cat.assignedTo) { + st.energy = Math.max(0, st.energy - ENERGY_DRAIN_PER_SEC * sturdyMul * dt); + if (st.energy <= 0) { + // 강제 휴식: 배치 해제 후 회복 + cat.returnTo = cat.assignedTo; + for (const b of state.buildings) + b.assigned = b.assigned.filter((id) => id !== cat.id); + cat.assignedTo = null; + cat.state = "resting"; + } + } else { + st.energy = Math.min( + cat.stats.stamina, + st.energy + IDLE_RECOVER_PER_SEC * dt, + ); + } + + // 행복도 증감 + if (st.fullness >= 80) + st.happiness = Math.min(100, st.happiness + (1 / 60) * gainMul * dt); + if (st.fullness <= 0) + st.happiness = Math.max(0, st.happiness - (4 / 60) * decayMul * dt); + // 고양이 집 부족: 하락하되 이 요인만으로는 파업까지 가지 않도록 바닥 25 유지 + if ( + state.hall.level >= 3 && + state.cats.length > houseCapacity && + st.happiness > 25 + ) + st.happiness = Math.max(25, st.happiness - (2 / 60) * decayMul * dt); + // 인기쟁이: 같은 건물 동료 행복도 상승 + if (cat.assignedTo) { + const mates = state.cats.filter( + (c) => + c.id !== cat.id && + c.assignedTo === cat.assignedTo && + c.passives.includes("popular"), + ); + if (mates.length > 0) + st.happiness = Math.min(100, st.happiness + (0.5 / 60) * dt); + } + + // 파업 + if ( + cat.state !== "striking" && + cat.state !== "resting" && + st.happiness < STRIKE_THRESHOLD + ) { + cat.returnTo = cat.assignedTo; + for (const b of state.buildings) + b.assigned = b.assigned.filter((id) => id !== cat.id); + cat.assignedTo = null; + cat.state = "striking"; + } + if (cat.state === "striking" && st.happiness >= STRIKE_RESUME) + tryReturn(cat); + } + state.updatedAt = Date.now(); }, @@ -343,7 +482,10 @@ export const engine = { }, /** 공통 건설: 상점 구매 후 선택한 타일에 배치 (비용은 배치 확정 시 차감) */ - build(type: "quarry" | "kitchen", pos: { i: number; j: number }): string | null { + build( + type: "quarry" | "kitchen" | "cat_house" | "sun_deck", + pos: { i: number; j: number }, + ): string | null { if (!state) return null; const def = BUILD_DEFS.find((d) => d.type === type); if (!def) return null; @@ -451,6 +593,13 @@ export const engine = { if (!cat || !building) return null; if (isUnderConstruction(building)) return `${building.name}은(는) 아직 건설 중입니다`; + if (cat.state === "striking") + return `${cat.name}은(는) 파업 중이에요 (행복도를 올려주세요)`; + if (building.maxSlots === 0) return `${building.name}에는 배치할 수 없습니다`; + if (cat.state === "resting") { + cat.state = null; + cat.returnTo = null; + } if (cat.aptitude[building.aptitude] === 0) return `${cat.name}은(는) ${building.name} 적성이 없습니다`; if (building.assigned.length >= building.maxSlots) diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index df33c89..d9d6c01 100644 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -9,7 +9,13 @@ export type AptitudeKey = export type ResourceKey = "fish" | "wood" | "stone"; -export type BuildingType = "dock" | "lumberyard" | "kitchen" | "quarry"; +export type BuildingType = + | "dock" + | "lumberyard" + | "kitchen" + | "quarry" + | "cat_house" + | "sun_deck"; export interface Cat { id: string; @@ -29,9 +35,12 @@ export interface Cat { status: { fullness: number; happiness: number; + energy: number; // 현재 체력 (최대 = stats.stamina) }; passives: string[]; assignedTo: string | null; + state?: "resting" | "striking" | null; // 강제 휴식 / 파업 + returnTo?: string | null; // 휴식·파업 종료 후 복귀할 건물 } export interface Building {