입양 모달: 랜덤 후보 3마리 중 선택
- 입양하기 → 모달에 후보 3마리 (능력치·품종 희귀도·적성·패시브 전부 랜덤) - 패시브 배지 마우스오버 시 상세 툴팁 (이름·긍정/부정/복합 구분·효과) - PassiveBadge 컴포넌트로 통합, 고양이 카드에서도 동일 툴팁 사용 - 모달 밖 클릭/✕로 취소 가능 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
+140
-20
@@ -6,18 +6,21 @@ import VillageView from "@/components/game/VillageView";
|
||||
import { engine, effectiveRate, activeRecipe, fullnessPenalty } from "@/lib/game/engine";
|
||||
import {
|
||||
APTITUDE_LABEL,
|
||||
BREEDS,
|
||||
HALL_LEVELS,
|
||||
KITCHEN_BUILD_COST,
|
||||
KITCHEN_MEAL_CAP,
|
||||
PASSIVES,
|
||||
TOOL_IMG,
|
||||
} from "@/lib/game/data";
|
||||
import type { AptitudeKey } from "@/lib/game/types";
|
||||
import type { Building, Cat, GameState } from "@/lib/game/types";
|
||||
|
||||
export default function Game() {
|
||||
const { state, dispatch } = useGame();
|
||||
const [selectedCatId, setSelectedCatId] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [candidates, setCandidates] = useState<Cat[] | null>(null);
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
@@ -36,10 +39,21 @@ export default function Game() {
|
||||
<div className="space-y-8">
|
||||
<TopBar
|
||||
state={state}
|
||||
onAdopt={() => run(() => engine.adopt())}
|
||||
onAdopt={() => setCandidates(engine.generateCandidates())}
|
||||
onUpgrade={() => run(() => engine.upgradeHall())}
|
||||
/>
|
||||
|
||||
{candidates && (
|
||||
<AdoptModal
|
||||
candidates={candidates}
|
||||
onPick={(cat) => {
|
||||
run(() => engine.adoptCandidate(cat));
|
||||
setCandidates(null);
|
||||
}}
|
||||
onClose={() => setCandidates(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<VillageView />
|
||||
|
||||
{message && (
|
||||
@@ -96,6 +110,128 @@ export default function Game() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 패시브 스킬 배지 — 마우스오버 시 상세 툴팁 */
|
||||
function PassiveBadge({ passiveKey }: { passiveKey: string }) {
|
||||
const def = PASSIVES.find((p) => p.key === passiveKey);
|
||||
if (!def) return null;
|
||||
const color =
|
||||
def.kind === "positive"
|
||||
? "bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300"
|
||||
: def.kind === "negative"
|
||||
? "bg-red-50 text-red-600 dark:bg-red-950 dark:text-red-300"
|
||||
: "bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300";
|
||||
const kindLabel =
|
||||
def.kind === "positive" ? "긍정" : def.kind === "negative" ? "부정" : "복합";
|
||||
return (
|
||||
<span className={`group relative cursor-help rounded px-1.5 py-0.5 text-[11px] ${color}`}>
|
||||
{def.name}
|
||||
<span className="pointer-events-none absolute bottom-full left-0 z-20 mb-1.5 hidden w-48 rounded-lg bg-gray-900 px-3 py-2 text-left text-[11px] leading-relaxed text-white shadow-lg group-hover:block dark:bg-gray-700">
|
||||
<span className="mb-0.5 block font-semibold">
|
||||
{def.name} <span className="font-normal opacity-70">({kindLabel})</span>
|
||||
</span>
|
||||
{def.desc}
|
||||
<span className="absolute left-3 top-full border-4 border-transparent border-t-gray-900 dark:border-t-gray-700" />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** 입양 모달 — 후보 3마리 중 1마리 선택 */
|
||||
function AdoptModal({
|
||||
candidates,
|
||||
onPick,
|
||||
onClose,
|
||||
}: {
|
||||
candidates: Cat[];
|
||||
onPick: (cat: Cat) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl dark:bg-gray-900"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">😻 입양할 고양이를 선택하세요</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg px-3 py-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{candidates.map((cat) => {
|
||||
const breed = BREEDS.find((b) => b.key === cat.breed);
|
||||
const aptitudes = (Object.keys(APTITUDE_LABEL) as AptitudeKey[]).filter(
|
||||
(k) => cat.aptitude[k] > 0,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={cat.id}
|
||||
className="flex flex-col rounded-xl border border-gray-200 p-4 dark:border-gray-700"
|
||||
>
|
||||
<div className="mb-2 text-center">
|
||||
<div className="text-4xl">{cat.emoji}</div>
|
||||
<div className="mt-1 font-bold">{cat.name}</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{cat.breedName}{" "}
|
||||
<span className="text-amber-400">
|
||||
{"★".repeat(breed?.rarity ?? 1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 rounded-lg bg-gray-50 px-3 py-2 text-xs leading-relaxed dark:bg-gray-800">
|
||||
💪 근면 {cat.stats.diligence} · ❤️ 체력 {cat.stats.stamina} · 🍽️
|
||||
식욕 {cat.stats.appetite}
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex flex-wrap gap-1">
|
||||
{aptitudes.length > 0 ? (
|
||||
aptitudes.map((k) => (
|
||||
<span
|
||||
key={k}
|
||||
className="rounded bg-sky-50 px-1.5 py-0.5 text-[11px] text-sky-700 dark:bg-sky-950 dark:text-sky-300"
|
||||
>
|
||||
{APTITUDE_LABEL[k]} {cat.aptitude[k]}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[11px] text-gray-400">적성 없음…</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex min-h-6 flex-wrap gap-1">
|
||||
{cat.passives.length > 0 ? (
|
||||
cat.passives.map((key) => <PassiveBadge key={key} passiveKey={key} />)
|
||||
) : (
|
||||
<span className="text-[11px] text-gray-400">패시브 없음</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onPick(cat)}
|
||||
className="mt-auto rounded-lg bg-orange-500 py-2 text-sm font-semibold text-white hover:bg-orange-600"
|
||||
>
|
||||
이 아이로 할래요
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-center text-xs text-gray-400">
|
||||
한 마리만 선택할 수 있어요 · 능력치와 패시브에 마우스를 올리면 상세 정보가 보입니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopBar({
|
||||
state,
|
||||
onAdopt,
|
||||
@@ -335,25 +471,9 @@ function CatCard({
|
||||
|
||||
{cat.passives.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{cat.passives.map((key) => {
|
||||
const def = PASSIVES.find((p) => p.key === key);
|
||||
if (!def) return null;
|
||||
const color =
|
||||
def.kind === "positive"
|
||||
? "bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300"
|
||||
: def.kind === "negative"
|
||||
? "bg-red-50 text-red-600 dark:bg-red-950 dark:text-red-300"
|
||||
: "bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300";
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
title={def.desc}
|
||||
className={`rounded px-1.5 py-0.5 text-[11px] ${color}`}
|
||||
>
|
||||
{def.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{cat.passives.map((key) => (
|
||||
<PassiveBadge key={key} passiveKey={key} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+10
-3
@@ -243,10 +243,17 @@ export const engine = {
|
||||
state.updatedAt = Date.now();
|
||||
},
|
||||
|
||||
adopt(): string | null {
|
||||
/** 입양 후보 생성 (아직 마을에 합류하지 않음) */
|
||||
generateCandidates(count = 3): Cat[] {
|
||||
return Array.from({ length: count }, () => generateCat());
|
||||
},
|
||||
|
||||
/** 후보 중 한 마리 입양 확정 */
|
||||
adoptCandidate(cat: Cat): string | null {
|
||||
if (!state) return null;
|
||||
if (state.cats.length >= state.hall.capacity) return "수용 인원이 가득 찼습니다 (회관 증축 필요)";
|
||||
state.cats.push(generateCat());
|
||||
if (state.cats.length >= state.hall.capacity)
|
||||
return "수용 인원이 가득 찼습니다 (회관 증축 필요)";
|
||||
state.cats.push(cat);
|
||||
return null;
|
||||
},
|
||||
|
||||
|
||||
새 이슈에서 참조
사용자 차단