Phase C: 냥코인 경제 — 장터·창고 상한·입양 비용·즉시 완공
- 냥코인(🪙) 재화 + HUD 표시, 복덩이 패시브 실효과(일하는 중 평균 100초당 1) - 장터 건물(목재 120, 회관 Lv2): 판매 품목 선택 → 분당 5개 자동 판매 가격: 생선 1·목재 2·돌 3·구이 4·훈제 10 — 가공할수록 이득 - 창고 상한: 자원별 기본 500, 창고 건물(+500)로 확장. 가득 차면 회수 차단 (부분 회수·"창고 가득" 피드백), 건물 적재도 150 한도로 생산 정지 - 입양 비용: 3마리째부터 100+(n-2)×50, 모달에 비용 표시. 재추첨 🎲 30코인 - 즉시 완공: 건설 중 패널에서 남은 10초당 1코인 - 요리 판매는 부엌 선반에서 차감 — "먹일까 팔까" 선택 완성 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+114
-5
@@ -8,8 +8,10 @@ import {
|
||||
engine,
|
||||
effectiveRate,
|
||||
activeRecipe,
|
||||
adoptCost,
|
||||
fullnessPenalty,
|
||||
isUnderConstruction,
|
||||
storageCap,
|
||||
} from "@/lib/game/engine";
|
||||
import {
|
||||
APTITUDE_LABEL,
|
||||
@@ -20,7 +22,10 @@ import {
|
||||
DECO_DEFS,
|
||||
HALL_LEVELS,
|
||||
KITCHEN_UPGRADE_COST,
|
||||
MEAL_PRICES,
|
||||
mealCap,
|
||||
REROLL_COST,
|
||||
SELL_PRICES,
|
||||
PASSIVES,
|
||||
RECIPES,
|
||||
TOOL_IMG,
|
||||
@@ -264,6 +269,7 @@ export default function Game() {
|
||||
["🪵", Math.floor(state.resources.wood)],
|
||||
["🪨", Math.floor(state.resources.stone)],
|
||||
["🍖", meals],
|
||||
["🪙", Math.floor(state.coins ?? 0)],
|
||||
].map(([icon, value]) => (
|
||||
<span
|
||||
key={icon as string}
|
||||
@@ -414,6 +420,8 @@ export default function Game() {
|
||||
onCollect={() => run(() => engine.collect(openedBuilding.id))}
|
||||
onUpgradeKitchen={() => run(() => engine.upgradeKitchen())}
|
||||
highlightAssign={tutTarget === "assign"}
|
||||
onRush={() => run(() => engine.rushConstruction(openedBuilding.id))}
|
||||
onSetSell={(key) => run(() => engine.setSellKey(openedBuilding.id, key))}
|
||||
onMove={() => {
|
||||
setPanel(null);
|
||||
setPlacing({
|
||||
@@ -468,9 +476,25 @@ export default function Game() {
|
||||
<AdoptModal
|
||||
candidates={candidates}
|
||||
highlightPick={tutTarget === "adopt-pick"}
|
||||
cost={adoptCost(state)}
|
||||
coins={Math.floor(state.coins ?? 0)}
|
||||
onReroll={() => {
|
||||
const err = run(() => engine.payReroll());
|
||||
if (!err)
|
||||
setCandidates(
|
||||
engine.generateCandidates(
|
||||
3,
|
||||
state.tutorialStep === 1
|
||||
? "fishing"
|
||||
: state.tutorialStep === 4
|
||||
? "logging"
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onPick={(cat) => {
|
||||
run(() => engine.adoptCandidate(cat));
|
||||
setCandidates(null);
|
||||
const err = run(() => engine.adoptCandidate(cat));
|
||||
if (!err) setCandidates(null);
|
||||
}}
|
||||
onClose={() => setCandidates(null)}
|
||||
/>
|
||||
@@ -721,6 +745,8 @@ function BuildingPanel({
|
||||
onUpgradeKitchen,
|
||||
onMove,
|
||||
highlightAssign,
|
||||
onRush,
|
||||
onSetSell,
|
||||
}: {
|
||||
building: Building;
|
||||
state: GameState;
|
||||
@@ -730,6 +756,8 @@ function BuildingPanel({
|
||||
onUpgradeKitchen: () => void;
|
||||
onMove: () => void;
|
||||
highlightAssign?: boolean;
|
||||
onRush: () => void;
|
||||
onSetSell: (key: string | null) => void;
|
||||
}) {
|
||||
const isKitchen = building.type === "kitchen";
|
||||
const recipe = activeRecipe(state);
|
||||
@@ -773,6 +801,62 @@ function BuildingPanel({
|
||||
<p className="font-num text-center text-sm text-[#8a6316]">
|
||||
남은 시간 {Math.floor(remainSec / 60)}:{String(remainSec % 60).padStart(2, "0")}
|
||||
</p>
|
||||
<button
|
||||
onClick={onRush}
|
||||
className="amber-btn w-full py-2 text-sm"
|
||||
>
|
||||
⚡ 즉시 완공 — 🪙 {Math.max(1, Math.ceil(remainMs / 10000))}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (building.type === "market") {
|
||||
// 장터: 판매 품목 선택
|
||||
const options: Array<{ key: string; label: string; price: number; stock: number }> = [
|
||||
{ key: "fish", label: "🐟 생선", price: SELL_PRICES.fish, stock: Math.floor(state.resources.fish) },
|
||||
{ key: "wood", label: "🪵 목재", price: SELL_PRICES.wood, stock: Math.floor(state.resources.wood) },
|
||||
{ key: "stone", label: "🪨 돌", price: SELL_PRICES.stone, stock: Math.floor(state.resources.stone) },
|
||||
{
|
||||
key: "meals",
|
||||
label: `${recipe.emoji} 요리(${recipe.name})`,
|
||||
price: MEAL_PRICES[recipe.key] ?? 4,
|
||||
stock: state.buildings
|
||||
.filter((b) => b.type === "kitchen")
|
||||
.reduce((sum, b) => sum + Math.floor(b.stored), 0),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-[#6f5c39]">
|
||||
판매 품목을 정하면 분당 5개씩 자동으로 팔아 냥코인을 벌어요. 가공한
|
||||
요리일수록 값이 좋답니다!
|
||||
</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>
|
||||
<div className="space-y-2">
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.key}
|
||||
onClick={() => onSetSell(building.sellKey === o.key ? null : o.key)}
|
||||
className={`card-nature flex w-full items-center justify-between px-3.5 py-2.5 text-sm hover:brightness-105 ${
|
||||
building.sellKey === o.key ? "tut-highlight" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="font-jua text-[#3f2c14]">{o.label}</span>
|
||||
<span className="font-num text-xs text-[#8a6316]">
|
||||
개당 🪙{o.price} · 재고 {o.stock}
|
||||
{building.sellKey === o.key && " · 판매 중!"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!building.sellKey && (
|
||||
<p className="text-center text-xs text-[#a3906c]">지금은 아무것도 팔지 않아요</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -793,6 +877,11 @@ function BuildingPanel({
|
||||
📍 이동
|
||||
</button>
|
||||
</div>
|
||||
{building.type === "warehouse" && (
|
||||
<div className="card-nature px-4 py-3 text-center text-sm">
|
||||
현재 보관 상한: <b className="font-num">{storageCap(state)}</b> / 자원
|
||||
</div>
|
||||
)}
|
||||
{building.type === "cat_house" && (
|
||||
<section>
|
||||
<h3 className="font-jua mb-2 text-sm text-[#8a7551]">
|
||||
@@ -1192,11 +1281,17 @@ function AdoptModal({
|
||||
onPick,
|
||||
onClose,
|
||||
highlightPick,
|
||||
cost,
|
||||
onReroll,
|
||||
coins,
|
||||
}: {
|
||||
candidates: Cat[];
|
||||
onPick: (cat: Cat) => void;
|
||||
onClose: () => void;
|
||||
highlightPick?: boolean;
|
||||
cost: number;
|
||||
onReroll: () => void;
|
||||
coins: number;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -1209,7 +1304,12 @@ function AdoptModal({
|
||||
>
|
||||
<div className="panel-paper max-h-[calc(90vh-20px)] overflow-y-auto p-5">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-jua text-xl text-[#4a3418]">😻 입양할 고양이를 선택하세요</h2>
|
||||
<h2 className="font-jua text-xl text-[#4a3418]">
|
||||
😻 입양할 고양이를 선택하세요{" "}
|
||||
<span className="font-num text-sm text-[#8a6316]">
|
||||
{cost > 0 ? `(비용 🪙${cost})` : "(무료!)"}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="grid h-8 w-8 place-items-center rounded-full bg-linear-to-b from-[#e8d6ae] to-[#cdb488] text-sm text-[#6b4a20] shadow hover:brightness-105"
|
||||
@@ -1276,13 +1376,22 @@ function AdoptModal({
|
||||
onClick={() => onPick(cat)}
|
||||
className={"nature-btn mt-auto py-2 text-sm" + (highlightPick ? " tut-highlight" : "")}
|
||||
>
|
||||
이 아이로 할래요
|
||||
이 아이로 할래요{cost > 0 ? ` (🪙${cost})` : ""}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-center text-xs text-[#8a7551]">
|
||||
<div className="mt-4 flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={onReroll}
|
||||
disabled={coins < REROLL_COST}
|
||||
className="wood-plank px-4 py-1.5 text-xs hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
🎲 다른 아이들 보기 (🪙{REROLL_COST})
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-3 text-center text-xs text-[#8a7551]">
|
||||
한 마리만 선택할 수 있어요 · 패시브에 마우스를 올리면 상세 정보가 보입니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
새 이슈에서 참조
사용자 차단