Phase B: 체력·강제 휴식·행복도·파업·고양이 집·햇볕 데크·오프라인 정산
- 체력(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 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+104
-9
@@ -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 (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-[#f6eeda] font-jua text-lg text-[#8a7551]">
|
||||
@@ -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 && (
|
||||
<SidePanel
|
||||
title={`${openedBuilding.name} Lv${openedBuilding.level}`}
|
||||
icon={BUILDING_IMG[openedBuilding.type] ?? TOOL_IMG[openedBuilding.aptitude]}
|
||||
title={
|
||||
BUILDING_IMG[openedBuilding.type]
|
||||
? `${openedBuilding.name} Lv${openedBuilding.level}`
|
||||
: `${openedBuilding.emoji} ${openedBuilding.name} Lv${openedBuilding.level}`
|
||||
}
|
||||
icon={BUILDING_IMG[openedBuilding.type]}
|
||||
onClose={() => setPanel(null)}
|
||||
>
|
||||
<BuildingPanel
|
||||
@@ -400,11 +418,18 @@ function ShopPanel({
|
||||
className="card-nature flex items-center gap-3 p-3"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={BUILDING_IMG[def.type] ?? TOOL_IMG[def.aptitude]}
|
||||
alt={def.name}
|
||||
className={`h-14 w-14 shrink-0 object-contain ${built || !hallOk ? "opacity-40" : ""}`}
|
||||
/>
|
||||
{BUILDING_IMG[def.type] ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img
|
||||
src={BUILDING_IMG[def.type]}
|
||||
alt={def.name}
|
||||
className={`h-14 w-14 shrink-0 object-contain ${built || !hallOk ? "opacity-40" : ""}`}
|
||||
/>
|
||||
) : (
|
||||
<span className={`grid h-14 w-14 shrink-0 place-items-center text-4xl ${built || !hallOk ? "opacity-40" : ""}`}>
|
||||
{def.emoji}
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-jua text-[15px] text-[#3f2c14]">{def.name}</div>
|
||||
<div className="font-num text-xs text-[#8a6316]">
|
||||
@@ -499,6 +524,50 @@ function BuildingPanel({
|
||||
);
|
||||
}
|
||||
|
||||
if (building.maxSlots === 0) {
|
||||
// 패시브 건물 — 배치 없음
|
||||
const restingHere = state.cats.filter((c) => c.state === "resting");
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-[#6f5c39]">
|
||||
{BUILD_DEFS.find((d) => d.type === building.type)?.desc}
|
||||
</p>
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
onClick={onMove}
|
||||
className="wood-plank shrink-0 px-2.5 py-1 text-xs hover:brightness-110"
|
||||
>
|
||||
📍 이동
|
||||
</button>
|
||||
</div>
|
||||
{building.type === "cat_house" && (
|
||||
<section>
|
||||
<h3 className="font-jua mb-2 text-sm text-[#8a7551]">
|
||||
💤 쉬고 있는 고양이 ({restingHere.length})
|
||||
</h3>
|
||||
{restingHere.length === 0 && (
|
||||
<p className="text-xs text-[#8a7551]">지금은 모두 기운이 넘쳐요!</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{restingHere.map((cat) => (
|
||||
<div key={cat.id} className="card-nature flex items-center gap-3 px-3 py-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={catSprite(cat.breed, "sleep")} alt="" className="h-10 w-10 shrink-0 object-contain" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-jua truncate text-sm text-[#3f2c14]">{cat.name}</div>
|
||||
<div className="font-num text-xs text-[#8a7551]">
|
||||
⚡ {Math.floor(cat.status.energy)}/{cat.stats.stamina}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* 정보 */}
|
||||
@@ -807,6 +876,28 @@ function CatCard({
|
||||
<span className="font-num text-[11px] text-[#8a7a58]">
|
||||
🍽️ {Math.floor(cat.status.fullness)} (식욕 {cat.stats.appetite})
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<div className="bar-track h-2 flex-1">
|
||||
<div
|
||||
className="bar-fill-amber"
|
||||
style={{ width: `${(cat.status.energy / cat.stats.stamina) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-num text-[11px] text-[#8a7a58]">
|
||||
⚡ {Math.floor(cat.status.energy)}/{cat.stats.stamina}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<div className="bar-track h-2 flex-1">
|
||||
<div
|
||||
className={cat.status.happiness < 20 ? "bar-fill-red" : "bar-fill-green"}
|
||||
style={{ width: `${cat.status.happiness}%`, background: cat.status.happiness >= 20 ? "linear-gradient(90deg,#e88bb8,#f7b7d4)" : undefined }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-num text-[11px] text-[#8a7a58]">
|
||||
💗 {Math.floor(cat.status.happiness)}
|
||||
</span>
|
||||
{fullnessPenalty(cat) < 1 && (
|
||||
<span className="rounded-full bg-[rgba(214,106,74,.16)] px-2 py-0.5 text-[11px] text-[#b0472a]">
|
||||
배고픔
|
||||
@@ -823,7 +914,11 @@ function CatCard({
|
||||
)}
|
||||
|
||||
<p className="mt-2 text-xs">
|
||||
{workplace ? (
|
||||
{cat.state === "resting" ? (
|
||||
<span className="text-[#8a6316]">💤 지쳐서 휴식 중 (회복되면 복귀)</span>
|
||||
) : cat.state === "striking" ? (
|
||||
<span className="text-[#b0472a]">💢 파업 중! 행복도를 올려주세요</span>
|
||||
) : workplace ? (
|
||||
<span className="text-[#5c8a2a]">
|
||||
{workplace.emoji} {workplace.name}에서 일하는 중
|
||||
</span>
|
||||
|
||||
새 이슈에서 참조
사용자 차단