자연 친화 UI 전면 개편 (디자인 문서 기반)

- 폰트: Jua(제목·버튼)·Gowun Dodum(본문)·JetBrains Mono(숫자) next/font 적용
- 팔레트·공통 클래스: 우드/크림/잎사귀 토큰, wood-plank·wood-pill·nature-btn·
  amber-btn·panel-wood/paper·card-nature·chip-nature·bar-* (globals.css)
- HUD: 어두운 나무 바 + 매달린 판자 타이틀(로프) + 우드 자원 pill + 초록 입양 버튼
- 패널: 나무 프레임(나뭇결 텍스처) + 크림 종이 이중 구조 + 잎사귀 장식
- 카드: 비대칭 잎모양 모서리 + 초록 액센트 바, 연두 적성 칩, 초록/앰버 진행 바
- 입양 모달·상점·토스트·배치 힌트 모두 동일 스타일
- 캔버스: 건물 자원 배지를 나뭇결 판자+초록 글로우로, 라벨은 Jua+크림 외곽선

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 15:33:46 +09:00
co-authored by Claude Fable 5
부모 ecfccfbe70
커밋 e5c58b3d6a
4개의 변경된 파일과 443개의 추가작업 그리고 178개의 파일을 삭제
+200 -12
파일 보기
@@ -1,26 +1,214 @@
@import "tailwindcss"; @import "tailwindcss";
/* ── 자연 친화 팔레트 ── */
:root { :root {
--background: #ffffff; --background: #f6eeda;
--foreground: #171717; --foreground: #3f2c14;
--wood-dark: #6b4220;
--wood: #8a5a2b;
--wood-light: #b57f42;
--cream: #f6eeda;
--cream-bright: #fffdf6;
--ink: #3f2c14;
--ink-soft: #6f5c39;
--gold-line: rgba(236, 215, 166, 0.28);
--leaf: #7fb539;
--leaf-dark: #4f8420;
--leaf-bright: #b6e05e;
} }
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-gowun);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
} }
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: var(--font-gowun), sans-serif;
}
.font-jua {
font-family: var(--font-jua), sans-serif;
}
.font-gowun {
font-family: var(--font-gowun), sans-serif;
}
.font-num {
font-family: var(--font-mono), monospace;
}
/* ── 공통 컴포넌트 스타일 ── */
/* 상단 어두운 나무 바 */
.nature-topbar {
background: linear-gradient(rgba(30, 20, 8, 0.92), rgba(30, 20, 8, 0.72));
border-bottom: 1px solid var(--gold-line);
}
/* 매달린 나무 판자 (타이틀) */
.wood-plank {
border-radius: 14px 20px 14px 20px;
background: linear-gradient(#8a5a2b, #6b4220);
border: 1px solid rgba(48, 30, 12, 0.6);
box-shadow:
inset 0 1px 0 rgba(255, 236, 196, 0.35),
inset 0 -3px 8px rgba(0, 0, 0, 0.35),
0 6px 14px rgba(30, 20, 8, 0.35);
color: #fbf1d9;
}
/* 어두운 우드 pill (자원·보조 버튼) */
.wood-pill {
border-radius: 999px;
background: linear-gradient(rgba(44, 30, 14, 0.86), rgba(28, 19, 8, 0.9));
border: 1px solid var(--gold-line);
box-shadow:
inset 0 1px 0 rgba(255, 240, 205, 0.18),
0 4px 10px rgba(20, 14, 4, 0.3);
color: #f4e9cf;
}
.wood-pill-icon {
border-radius: 50%;
background: radial-gradient(circle at 35% 25%, #f6ecd2, #d8c39a);
box-shadow: inset 0 -2px 4px rgba(120, 95, 50, 0.35);
}
/* 초록 자연 버튼 (주 액션) */
.nature-btn {
border-radius: 999px;
background: linear-gradient(#a3d24f, #5d8f22);
border: 1px solid #3f6a13;
box-shadow:
inset 0 1px 0 rgba(255, 255, 235, 0.5),
0 6px 14px rgba(50, 80, 20, 0.4);
color: #26400c;
font-family: var(--font-jua), sans-serif;
}
.nature-btn:hover:not(:disabled) {
background: linear-gradient(#b0dc60, #6a9e2c);
}
/* 앰버 우드 버튼 (구매·건설) */
.amber-btn {
border-radius: 999px;
background: linear-gradient(#d6a43c, #a87a1e);
border: 1px solid #7a5510;
box-shadow:
inset 0 1px 0 rgba(255, 240, 200, 0.5),
0 5px 12px rgba(90, 60, 10, 0.35);
color: #3d2c07;
font-family: var(--font-jua), sans-serif;
}
.amber-btn:hover:not(:disabled) {
background: linear-gradient(#e2b34e, #b78829);
}
/* 패널: 바깥 나무 프레임 */
.panel-wood {
border-radius: 30px;
background-image:
repeating-linear-gradient(
118deg,
rgba(255, 232, 186, 0.22) 0 3px,
rgba(0, 0, 0, 0) 3px 9px,
rgba(52, 28, 8, 0.4) 9px 15px,
rgba(0, 0, 0, 0) 15px 24px
),
repeating-linear-gradient(
62deg,
rgba(255, 232, 186, 0.1) 0 2px,
rgba(0, 0, 0, 0) 2px 11px
),
linear-gradient(160deg, #b57f42, #8a5a2b 45%, #6d4420);
box-shadow:
inset 0 2px 0 rgba(255, 238, 200, 0.35),
inset 0 -4px 10px rgba(34, 18, 4, 0.4),
0 12px 28px rgba(20, 14, 4, 0.4);
}
/* 패널: 안쪽 크림 종이 */
.panel-paper {
border-radius: 22px;
background-image:
radial-gradient(
140% 90% at 50% 0%,
rgba(255, 252, 240, 0.9),
rgba(246, 238, 218, 0.94)
),
repeating-linear-gradient(
90deg,
rgba(140, 110, 60, 0.05) 0 2px,
rgba(0, 0, 0, 0) 2px 7px
);
background-color: #f6eeda;
box-shadow:
inset 0 0 0 1px rgba(120, 90, 45, 0.25),
inset 0 6px 18px rgba(140, 110, 60, 0.18);
}
/* 크림 카드 (잎모양 비대칭 모서리) */
.card-nature {
position: relative;
border-radius: 6px 22px 6px 22px;
background: linear-gradient(#fffdf6, #f3e9d0);
border: 1px solid rgba(126, 95, 48, 0.3);
box-shadow:
inset 0 1px 0 rgba(255, 255, 250, 0.9),
0 4px 10px rgba(120, 95, 50, 0.14);
}
.card-accent {
position: absolute;
left: 0;
top: 14px;
bottom: 14px;
width: 4px;
border-radius: 0 4px 4px 0;
background: linear-gradient(#9ccc48, #4f8420);
}
/* 연두 칩 (적성 등) */
.chip-nature {
border-radius: 3px 10px 3px 10px;
background: rgba(126, 181, 57, 0.14);
border: 1px solid rgba(94, 140, 42, 0.3);
color: #4d6b21;
}
/* 잎사귀 장식 */
.leaf {
position: absolute;
border-radius: 0 100% 0 100%;
background: linear-gradient(140deg, #a8d95c, #59902a);
pointer-events: none;
}
.leaf-bright {
background: linear-gradient(140deg, #bde36f, #6aa233);
}
/* 진행 바 (포만도 등) */
.bar-track {
border-radius: 999px;
background: rgba(126, 95, 48, 0.16);
box-shadow: inset 0 1px 2px rgba(100, 75, 35, 0.28);
overflow: hidden;
}
.bar-fill-green {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #7fc23a, #b6e05e);
box-shadow: inset 0 1px 0 rgba(255, 255, 230, 0.6);
}
.bar-fill-amber {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #d6a43c, #eec767);
box-shadow: inset 0 1px 0 rgba(255, 255, 230, 0.6);
}
.bar-fill-red {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #d66a4a, #ee9367);
} }
+15 -8
파일 보기
@@ -1,14 +1,21 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Jua, Gowun_Dodum, JetBrains_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const jua = Jua({
variable: "--font-geist-sans", weight: "400",
variable: "--font-jua",
subsets: ["latin"], subsets: ["latin"],
}); });
const geistMono = Geist_Mono({ const gowunDodum = Gowun_Dodum({
variable: "--font-geist-mono", weight: "400",
variable: "--font-gowun",
subsets: ["latin"],
});
const jetbrainsMono = JetBrains_Mono({
variable: "--font-mono",
subsets: ["latin"], subsets: ["latin"],
}); });
@@ -20,10 +27,10 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: LayoutProps<"/">) { export default function RootLayout({ children }: LayoutProps<"/">) {
return ( return (
<html <html
lang="en" lang="ko"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${jua.variable} ${gowunDodum.variable} ${jetbrainsMono.variable} h-full antialiased`}
> >
<body className="min-h-full flex flex-col">{children}</body> <body className="min-h-full flex flex-col font-gowun">{children}</body>
</html> </html>
); );
} }
+122 -103
파일 보기
@@ -57,7 +57,7 @@ export default function Game() {
if (!state) { if (!state) {
return ( return (
<div className="fixed inset-0 flex items-center justify-center bg-emerald-50 text-gray-400 dark:bg-gray-950"> <div className="fixed inset-0 flex items-center justify-center bg-[#f6eeda] font-jua text-lg text-[#8a7551]">
마을 불러오는 중… 마을 불러오는 중…
</div> </div>
); );
@@ -91,52 +91,60 @@ export default function Game() {
{/* ── 배치 모드 힌트 ── */} {/* ── 배치 모드 힌트 ── */}
{placing && ( {placing && (
<div className="absolute bottom-6 left-1/2 z-40 flex -translate-x-1/2 items-center gap-3 rounded-full bg-gray-900/90 px-5 py-2.5 text-sm font-semibold text-white shadow-lg"> <div className="wood-plank font-jua absolute bottom-6 left-1/2 z-40 flex -translate-x-1/2 items-center gap-3 px-5 py-2.5 text-sm">
📍 {placing.moveId ? "옮길" : "배치할"} 위치를 클릭하세요 📍 {placing.moveId ? "옮길" : "배치할"} 위치를 클릭하세요
{placing.type === "dock" && " (부두는 물가에만)"} {placing.type === "dock" && " (부두는 물가에만)"}
<button <button
onClick={() => setPlacing(null)} onClick={() => setPlacing(null)}
className="rounded-full bg-white/20 px-3 py-1 text-xs hover:bg-white/30" className="wood-pill px-3 py-1 text-xs hover:brightness-125"
> >
취소 (ESC) 취소 (ESC)
</button> </button>
</div> </div>
)} )}
{/* ── 상단 HUD ── */} {/* ── 상단 HUD (어두운 나무 바) ── */}
<div className="pointer-events-none absolute inset-x-0 top-0 z-30 flex flex-wrap items-start justify-between gap-2 p-3"> <div className="nature-topbar pointer-events-none absolute inset-x-0 top-0 z-30 h-[52px]">
<div className="pointer-events-auto flex flex-wrap items-center gap-2"> <div className="flex h-full items-start justify-between gap-2 px-4">
{/* 매달린 나무 판자 타이틀 */}
<div className="pointer-events-auto relative mt-0 flex items-start gap-3.5">
<button <button
onClick={() => setPanel({ type: "hall" })} onClick={() => setPanel({ type: "hall" })}
className="rounded-full bg-white/85 px-4 py-2 text-sm font-bold shadow backdrop-blur hover:bg-white dark:bg-gray-900/85 dark:hover:bg-gray-900" className="wood-plank relative mt-1 flex items-center gap-2 px-4 py-2"
> >
<span className="absolute -top-1 left-4 h-4 w-[3px] -rotate-12 bg-linear-to-b from-[#c9b184] to-[#8c7550]" />
<span className="absolute -top-1 right-4 h-4 w-[3px] rotate-12 bg-linear-to-b from-[#c9b184] to-[#8c7550]" />
<span className="font-jua text-[17px] tracking-wide [text-shadow:0_1px_2px_rgba(0,0,0,.45)]">
🏡 고양이 마을 · 회관 Lv{state.hall.level} 🏡 고양이 마을 · 회관 Lv{state.hall.level}
</span>
</button> </button>
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85"> {/* 자원 pill */}
🐱 {state.cats.length}/{state.hall.capacity} <div className="mt-2 flex flex-wrap items-center gap-2">
</span> {[
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85"> ["🐱", `${state.cats.length}/${state.hall.capacity}`],
🐟 {Math.floor(state.resources.fish)} ["🐟", Math.floor(state.resources.fish)],
</span> ["🪵", Math.floor(state.resources.wood)],
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85"> ["🪨", Math.floor(state.resources.stone)],
🪵 {Math.floor(state.resources.wood)} ["🍖", meals],
</span> ].map(([icon, value]) => (
<span className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85">
🪨 {Math.floor(state.resources.stone)}
</span>
<span <span
title="부엌에 보관 중인 요리" key={icon as string}
className="rounded-full bg-white/85 px-3 py-2 text-sm shadow backdrop-blur dark:bg-gray-900/85" className="wood-pill flex items-center gap-1.5 py-1 pl-1 pr-3"
> >
🍖 {meals} <span className="wood-pill-icon grid h-6 w-6 place-items-center text-[13px]">
{icon}
</span> </span>
<span className="font-num text-sm leading-none">{value}</span>
</span>
))}
</div> </div>
<div className="pointer-events-auto flex gap-2"> </div>
<div className="pointer-events-auto mt-2 flex gap-2">
<button <button
onClick={() => onClick={() =>
setPanel(panel?.type === "shop" ? null : { type: "shop" }) setPanel(panel?.type === "shop" ? null : { type: "shop" })
} }
className="rounded-full bg-white/85 px-4 py-2 text-sm font-semibold shadow backdrop-blur hover:bg-white dark:bg-gray-900/85 dark:hover:bg-gray-900" className="wood-pill font-jua flex items-center gap-1.5 px-4 py-2 text-[15px] hover:brightness-125"
> >
🛒 상점 🛒 상점
</button> </button>
@@ -144,23 +152,24 @@ export default function Game() {
onClick={() => onClick={() =>
setPanel(panel?.type === "cats" ? null : { type: "cats" }) setPanel(panel?.type === "cats" ? null : { type: "cats" })
} }
className="rounded-full bg-white/85 px-4 py-2 text-sm font-semibold shadow backdrop-blur hover:bg-white dark:bg-gray-900/85 dark:hover:bg-gray-900" className="wood-pill font-jua flex items-center gap-1.5 px-4 py-2 text-[15px] hover:brightness-125"
> >
🐾 고양이 🐾 고양이
</button> </button>
<button <button
onClick={() => setCandidates(engine.generateCandidates())} onClick={() => setCandidates(engine.generateCandidates())}
disabled={state.cats.length >= state.hall.capacity} disabled={state.cats.length >= state.hall.capacity}
className="rounded-full bg-orange-500 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-orange-600 disabled:cursor-not-allowed disabled:opacity-50" className="nature-btn flex items-center gap-1.5 px-4 py-2 text-[15px] disabled:cursor-not-allowed disabled:opacity-50"
> >
😻 입양 😻 입양
</button> </button>
</div> </div>
</div> </div>
</div>
{/* ── 메시지 토스트 ── */} {/* ── 메시지 토스트 ── */}
{message && ( {message && (
<div className="absolute left-1/2 top-16 z-50 -translate-x-1/2 rounded-full bg-amber-500/95 px-5 py-2 text-sm font-semibold text-white shadow-lg"> <div className="wood-plank font-jua absolute left-1/2 top-16 z-50 -translate-x-1/2 px-5 py-2 text-sm">
⚠️ {message} ⚠️ {message}
</div> </div>
)} )}
@@ -255,9 +264,16 @@ function SidePanel({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<aside className="absolute bottom-3 right-3 top-16 z-40 flex w-full max-w-sm flex-col rounded-2xl border border-gray-200 bg-white/95 shadow-2xl backdrop-blur dark:border-gray-800 dark:bg-gray-900/95"> <aside className="panel-wood absolute bottom-3 right-3 top-15 z-40 w-full max-w-sm p-2.5">
<div className="flex items-center justify-between border-b border-gray-100 px-5 py-4 dark:border-gray-800"> {/* 잎사귀 장식 */}
<h2 className="flex items-center gap-2 text-lg font-bold"> <span className="leaf left-2 top-14 h-6 w-6 rotate-[-28deg]" />
<span className="leaf leaf-bright left-1 top-20 h-4 w-4 rotate-24" />
<span className="leaf right-2 top-52 h-5 w-5 rotate-140" />
<span className="leaf leaf-bright bottom-2 left-24 h-5 w-5 rotate-[-140deg]" />
<span className="leaf leaf-bright bottom-1.5 right-32 h-4 w-4 rotate-[-200deg]" />
<div className="panel-paper relative flex h-full flex-col overflow-hidden">
<div className="flex items-center justify-between border-b border-[rgba(126,95,48,.18)] px-5 py-3.5">
<h2 className="font-jua flex items-center gap-2 text-[19px] text-[#4a3418]">
{icon && ( {icon && (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img src={icon} alt="" className="h-8 w-8 object-contain" /> <img src={icon} alt="" className="h-8 w-8 object-contain" />
@@ -266,12 +282,13 @@ function SidePanel({
</h2> </h2>
<button <button
onClick={onClose} onClick={onClose}
className="rounded-lg px-3 py-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800" 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"
> >
✕ ✕
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto px-5 py-4">{children}</div> <div className="flex-1 overflow-y-auto px-4 py-4">{children}</div>
</div>
</aside> </aside>
); );
} }
@@ -282,25 +299,25 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
const next = HALL_LEVELS[state.hall.level + 1]; const next = HALL_LEVELS[state.hall.level + 1];
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-[#6f5c39]">
마을의 중심. 회관 레벨이 수용 인원과 건물·레시피 해금을 결정합니다. 마을의 중심. 회관 레벨이 수용 인원과 건물·레시피 해금을 결정합니다.
</p> </p>
<div className="rounded-lg bg-gray-50 px-4 py-3 text-sm dark:bg-gray-800"> <div className="card-nature px-4 py-3 text-sm">
현재 수용 인원 <b>{state.cats.length}/{state.hall.capacity}마리</b> 현재 수용 인원 <b>{state.cats.length}/{state.hall.capacity}마리</b>
</div> </div>
{next ? ( {next ? (
<div className="rounded-xl border border-gray-200 p-4 dark:border-gray-700"> <div className="card-nature p-4">
<h3 className="mb-2 font-semibold"> <h3 className="font-jua mb-2 text-[16px] text-[#4a3418]">
Lv{state.hall.level + 1} 증축 → 수용 {next.capacity}마리 Lv{state.hall.level + 1} 증축 → 수용 {next.capacity}마리
</h3> </h3>
<ul className="mb-3 space-y-1 text-sm text-gray-600 dark:text-gray-300"> <ul className="mb-3 space-y-1 text-sm text-[#6f5c39]">
{Object.entries(next.cost).map(([key, amount]) => { {Object.entries(next.cost).map(([key, amount]) => {
const have = Math.floor( const have = Math.floor(
state.resources[key as keyof typeof state.resources], state.resources[key as keyof typeof state.resources],
); );
const ok = have >= (amount ?? 0); const ok = have >= (amount ?? 0);
return ( return (
<li key={key} className={ok ? "text-emerald-600 dark:text-emerald-400" : ""}> <li key={key} className={ok ? "text-[#5c8a2a]" : ""}>
{ok ? "✓" : "·"} {RES_EMOJI[key]} {RES_LABEL[key]} {have}/{amount} {ok ? "✓" : "·"} {RES_EMOJI[key]} {RES_LABEL[key]} {have}/{amount}
</li> </li>
); );
@@ -309,7 +326,7 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
const b = state.buildings.find((x) => x.type === req.type); const b = state.buildings.find((x) => x.type === req.type);
const ok = Boolean(b && b.level >= (req.level ?? 1)); const ok = Boolean(b && b.level >= (req.level ?? 1));
return ( return (
<li key={req.label} className={ok ? "text-emerald-600 dark:text-emerald-400" : ""}> <li key={req.label} className={ok ? "text-[#5c8a2a]" : ""}>
{ok ? "✓" : "·"} {req.label} {ok ? "✓" : "·"} {req.label}
</li> </li>
); );
@@ -334,13 +351,13 @@ function HallPanel({ state, onUpgrade }: { state: GameState; onUpgrade: () => vo
)} )}
</ul> </ul>
{next.locked ? ( {next.locked ? (
<p className="rounded-lg bg-gray-100 px-3 py-2 text-xs text-gray-500 dark:bg-gray-800 dark:text-gray-400"> <p className="chip-nature px-3 py-2 text-xs">
🔒 {next.locked} 🔒 {next.locked}
</p> </p>
) : ( ) : (
<button <button
onClick={onUpgrade} onClick={onUpgrade}
className="w-full rounded-lg bg-emerald-600 py-2 text-sm font-semibold text-white hover:bg-emerald-700" className="nature-btn w-full py-2 text-sm"
> >
🏗️ 증축하기 🏗️ 증축하기
</button> </button>
@@ -364,7 +381,7 @@ function ShopPanel({
}) { }) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs text-gray-400"> <p className="text-xs text-[#8a7551]">
구매 후 마을의 원하는 타일을 클릭해 배치합니다. 비용은 배치 확정 시 차감돼요. 구매 후 마을의 원하는 타일을 클릭해 배치합니다. 비용은 배치 확정 시 차감돼요.
</p> </p>
{BUILD_DEFS.map((def) => { {BUILD_DEFS.map((def) => {
@@ -378,7 +395,7 @@ function ShopPanel({
return ( return (
<div <div
key={def.type} key={def.type}
className="flex items-center gap-3 rounded-xl border border-gray-200 p-3 dark:border-gray-700" className="card-nature flex items-center gap-3 p-3"
> >
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
@@ -387,22 +404,22 @@ function ShopPanel({
className={`h-12 w-12 shrink-0 object-contain ${built || !hallOk ? "opacity-40" : ""}`} className={`h-12 w-12 shrink-0 object-contain ${built || !hallOk ? "opacity-40" : ""}`}
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-sm font-semibold">{def.name}</div> <div className="font-jua text-[15px] text-[#3f2c14]">{def.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="font-num text-xs text-[#8a6316]">
{costEntries.map(([k, v]) => `${RES_EMOJI[k]} ${v}`).join(" + ")} {costEntries.map(([k, v]) => `${RES_EMOJI[k]} ${v}`).join(" + ")}
{def.reqHallLv > 1 && ` · 회관 Lv${def.reqHallLv}`} {def.reqHallLv > 1 && ` · 회관 Lv${def.reqHallLv}`}
</div> </div>
<div className="mt-0.5 line-clamp-2 text-[11px] text-gray-400">{def.desc}</div> <div className="mt-0.5 line-clamp-2 text-[11px] text-[#8a7551]">{def.desc}</div>
</div> </div>
{built ? ( {built ? (
<span className="shrink-0 rounded-lg bg-gray-100 px-3 py-1.5 text-xs text-gray-400 dark:bg-gray-800"> <span className="chip-nature shrink-0 px-3 py-1.5 text-xs">
보유 중 보유 중
</span> </span>
) : ( ) : (
<button <button
onClick={() => onBuy(def)} onClick={() => onBuy(def)}
disabled={!hallOk || !affordable} disabled={!hallOk || !affordable}
className="shrink-0 rounded-lg bg-amber-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-amber-700 disabled:cursor-not-allowed disabled:opacity-40" className="amber-btn shrink-0 px-3.5 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-40"
> >
{!hallOk ? `회관 Lv${def.reqHallLv}` : !affordable ? "자원 부족" : "구매"} {!hallOk ? `회관 Lv${def.reqHallLv}` : !affordable ? "자원 부족" : "구매"}
</button> </button>
@@ -455,7 +472,7 @@ function BuildingPanel({
return ( return (
<div className="space-y-5"> <div className="space-y-5">
{/* 정보 */} {/* 정보 */}
<div className="rounded-lg bg-gray-50 px-4 py-3 text-sm leading-relaxed dark:bg-gray-800"> <div className="card-nature px-4 py-3 text-sm leading-relaxed">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span> <span>
필요 적성: <b>{APTITUDE_LABEL[building.aptitude]}</b> · 슬롯{" "} 필요 적성: <b>{APTITUDE_LABEL[building.aptitude]}</b> · 슬롯{" "}
@@ -464,7 +481,7 @@ function BuildingPanel({
<button <button
onClick={onMove} onClick={onMove}
title="건물 위치 옮기기" title="건물 위치 옮기기"
className="shrink-0 rounded-lg bg-gray-200 px-2.5 py-1 text-xs font-semibold text-gray-600 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" className="wood-plank shrink-0 px-2.5 py-1 text-xs hover:brightness-110"
> >
📍 이동 📍 이동
</button> </button>
@@ -479,9 +496,9 @@ function BuildingPanel({
→ 포만도 +{recipe.fullness}) → 포만도 +{recipe.fullness})
</div> </div>
<div>요리 속도 {totalRate.toFixed(2)}/초</div> <div>요리 속도 {totalRate.toFixed(2)}/초</div>
<div className="mt-1 h-1.5 w-full overflow-hidden rounded bg-gray-200 dark:bg-gray-700"> <div className="bar-track mt-1 h-2 w-full">
<div <div
className="h-full bg-amber-400" className="bar-fill-amber"
style={{ style={{
width: `${Math.min(100, ((building.progress ?? 0) / recipe.work) * 100)}%`, width: `${Math.min(100, ((building.progress ?? 0) / recipe.work) * 100)}%`,
}} }}
@@ -496,18 +513,18 @@ function BuildingPanel({
{/* 산출/선반 */} {/* 산출/선반 */}
{isKitchen ? ( {isKitchen ? (
<> <>
<div className="rounded-lg bg-amber-50 px-4 py-2.5 text-center text-sm font-semibold text-amber-700 dark:bg-amber-950 dark:text-amber-300"> <div className="card-nature px-4 py-2.5 text-center text-sm font-semibold text-[#8a6316]">
{recipe.emoji} 선반 {stored}/{mealCap(building.level)} — 배고픈 고양이가 자동으로 먹습니다 {recipe.emoji} 선반 {stored}/{mealCap(building.level)} — 배고픈 고양이가 자동으로 먹습니다
</div> </div>
{building.level < 2 && ( {building.level < 2 && (
<div className="rounded-xl border border-gray-200 p-3 text-sm dark:border-gray-700"> <div className="card-nature p-3 text-sm">
<div className="mb-1 font-semibold">⬆️ 부엌 Lv2 증축</div> <div className="font-jua mb-1 text-[15px] text-[#3f2c14]">⬆️ 부엌 Lv2 증축</div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400"> <p className="mb-2 text-xs text-[#6f5c39]">
선반 15칸으로 확장 · 회관 Lv3 달성 시 🐟 훈제 생선 해금 선반 15칸으로 확장 · 회관 Lv3 달성 시 🐟 훈제 생선 해금
</p> </p>
<button <button
onClick={onUpgradeKitchen} onClick={onUpgradeKitchen}
className="w-full rounded-lg bg-emerald-600 py-2 text-xs font-semibold text-white hover:bg-emerald-700" className="nature-btn w-full py-2 text-xs"
> >
{Object.entries(KITCHEN_UPGRADE_COST) {Object.entries(KITCHEN_UPGRADE_COST)
.map(([k, v]) => `${RES_EMOJI[k]} ${v}`) .map(([k, v]) => `${RES_EMOJI[k]} ${v}`)
@@ -522,7 +539,7 @@ function BuildingPanel({
(r) => r.reqHallLv > state.hall.level || r.reqKitchenLv > kitchenLv, (r) => r.reqHallLv > state.hall.level || r.reqKitchenLv > kitchenLv,
); );
return lockedRecipe ? ( return lockedRecipe ? (
<p className="text-xs text-gray-400"> <p className="text-xs text-[#8a7551]">
🔒 {lockedRecipe.emoji} {lockedRecipe.name} — 회관 Lv 🔒 {lockedRecipe.emoji} {lockedRecipe.name} — 회관 Lv
{lockedRecipe.reqHallLv} · 부엌 Lv{lockedRecipe.reqKitchenLv} 해금 {lockedRecipe.reqHallLv} · 부엌 Lv{lockedRecipe.reqKitchenLv} 해금
</p> </p>
@@ -533,7 +550,7 @@ function BuildingPanel({
<button <button
onClick={onCollect} onClick={onCollect}
disabled={stored <= 0} disabled={stored <= 0}
className="w-full rounded-lg bg-sky-500 py-2.5 text-sm font-semibold text-white hover:bg-sky-600 disabled:cursor-not-allowed disabled:opacity-40" className="amber-btn w-full py-2.5 text-sm disabled:cursor-not-allowed disabled:opacity-40"
> >
{building.resource === "fish" ? "🐟" : "🪵"} {stored}개 회수 {building.resource === "fish" ? "🐟" : "🪵"} {stored}개 회수
</button> </button>
@@ -541,11 +558,11 @@ function BuildingPanel({
{/* 배치된 고양이 */} {/* 배치된 고양이 */}
<section> <section>
<h3 className="mb-2 text-sm font-bold text-gray-500 dark:text-gray-400"> <h3 className="font-jua mb-2 text-sm text-[#8a7551]">
일하는 중 ({assignedCats.length}/{building.maxSlots}) 일하는 중 ({assignedCats.length}/{building.maxSlots})
</h3> </h3>
{assignedCats.length === 0 && ( {assignedCats.length === 0 && (
<p className="text-xs text-gray-400">아직 아무도 없어요</p> <p className="text-xs text-[#8a7551]">아직 아무도 없어요</p>
)} )}
<div className="space-y-2"> <div className="space-y-2">
{assignedCats.map((cat) => ( {assignedCats.map((cat) => (
@@ -562,11 +579,11 @@ function BuildingPanel({
{/* 배치 가능한 고양이 */} {/* 배치 가능한 고양이 */}
<section> <section>
<h3 className="mb-2 text-sm font-bold text-gray-500 dark:text-gray-400"> <h3 className="font-jua mb-2 text-sm text-[#8a7551]">
배치 가능한 고양이 배치 가능한 고양이
</h3> </h3>
{candidates.length === 0 && ( {candidates.length === 0 && (
<p className="text-xs text-gray-400"> <p className="text-xs text-[#8a7551]">
{APTITUDE_LABEL[building.aptitude]} 적성이 있는 고양이가 없어요 {APTITUDE_LABEL[building.aptitude]} 적성이 있는 고양이가 없어요
</p> </p>
)} )}
@@ -590,7 +607,7 @@ function BuildingPanel({
})} })}
</div> </div>
{slotsLeft <= 0 && candidates.length > 0 && ( {slotsLeft <= 0 && candidates.length > 0 && (
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400"> <p className="mt-2 text-xs text-[#8a6316]">
슬롯이 가득 찼어요 — 먼저 해제하거나 건물을 증축하세요 슬롯이 가득 찼어요 — 먼저 해제하거나 건물을 증축하세요
</p> </p>
)} )}
@@ -617,7 +634,8 @@ function CatRow({
}; };
}) { }) {
return ( return (
<div className="flex items-center gap-3 rounded-xl border border-gray-200 px-3 py-2 dark:border-gray-700"> <div className="card-nature flex items-center gap-3 px-3 py-2">
<span className="card-accent" />
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={catSprite(cat.breed, "idle")} src={catSprite(cat.breed, "idle")}
@@ -625,26 +643,26 @@ function CatRow({
className="h-10 w-10 shrink-0 object-contain" className="h-10 w-10 shrink-0 object-contain"
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold"> <div className="font-jua truncate text-sm text-[#3f2c14]">
{cat.name}{" "} {cat.name}{" "}
<span className="text-xs font-normal text-gray-400">Lv{cat.level}</span> <span className="font-num text-xs text-[#8a7551]">Lv{cat.level}</span>
</div> </div>
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="text-xs text-[#6f5c39]">
{APTITUDE_LABEL[aptitude]} {cat.aptitude[aptitude]} ·{" "} {APTITUDE_LABEL[aptitude]} {cat.aptitude[aptitude]} ·{" "}
{effectiveRate(cat, aptitude).toFixed(2)}/초 {effectiveRate(cat, aptitude).toFixed(2)}/초
{fullnessPenalty(cat) < 1 && ( {fullnessPenalty(cat) < 1 && (
<span className="ml-1 text-red-500">배고픔</span> <span className="ml-1 text-[#b0472a]">배고픔</span>
)} )}
</div> </div>
{tag && <div className="text-[11px] text-gray-400">{tag}</div>} {tag && <div className="text-[11px] text-[#a3906c]">{tag}</div>}
</div> </div>
<button <button
onClick={action.onClick} onClick={action.onClick}
disabled={action.disabled} disabled={action.disabled}
className={`shrink-0 rounded-lg px-3 py-1.5 text-xs font-semibold disabled:cursor-not-allowed disabled:opacity-40 ${ className={`shrink-0 px-3 py-1.5 text-xs disabled:cursor-not-allowed disabled:opacity-40 ${
action.style === "primary" action.style === "primary"
? "bg-orange-500 text-white hover:bg-orange-600" ? "nature-btn"
: "bg-gray-100 text-gray-600 hover:bg-red-100 hover:text-red-600 dark:bg-gray-800 dark:text-gray-300" : "wood-plank hover:brightness-110"
}`} }`}
> >
{action.label} {action.label}
@@ -696,9 +714,10 @@ function CatCard({
const cap = BREEDS.find((b) => b.key === cat.breed)?.aptitudeCap ?? 5; const cap = BREEDS.find((b) => b.key === cat.breed)?.aptitudeCap ?? 5;
return ( return (
<div className="rounded-xl border border-gray-200 p-4 dark:border-gray-700"> <div className="card-nature p-4">
<span className="card-accent" />
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
<span className="flex items-center gap-1.5 font-semibold"> <span className="font-jua flex items-center gap-1.5 text-[16px] text-[#3f2c14]">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={catSprite(cat.breed, "idle")} src={catSprite(cat.breed, "idle")}
@@ -706,14 +725,14 @@ function CatCard({
className="h-7 w-7 object-contain" className="h-7 w-7 object-contain"
/> />
{cat.name}{" "} {cat.name}{" "}
<span className="text-xs font-normal text-gray-400">Lv{cat.level}</span> <span className="font-num text-xs text-[#8a7551]">Lv{cat.level}</span>
</span> </span>
<span className="text-xs text-gray-400">{cat.breedName}</span> <span className="text-xs text-[#a3906c]">{cat.breedName}</span>
</div> </div>
<div className="mb-2 h-1.5 w-full overflow-hidden rounded bg-gray-100 dark:bg-gray-800"> <div className="bar-track mb-2 h-1.5 w-full">
<div <div
className="h-full bg-orange-400" className="bar-fill-amber"
style={{ width: `${Math.min(100, (cat.exp / expNeeded) * 100)}%` }} style={{ width: `${Math.min(100, (cat.exp / expNeeded) * 100)}%` }}
/> />
</div> </div>
@@ -724,14 +743,14 @@ function CatCard({
.map((k) => ( .map((k) => (
<span <span
key={k} key={k}
className="flex items-center gap-0.5 rounded bg-sky-50 px-1.5 py-0.5 text-[11px] text-sky-700 dark:bg-sky-950 dark:text-sky-300" className="chip-nature flex items-center gap-0.5 px-1.5 py-0.5 text-[11px]"
> >
{APTITUDE_LABEL[k]} {cat.aptitude[k]} {APTITUDE_LABEL[k]} {cat.aptitude[k]}
{cat.aptitudePoints > 0 && cat.aptitude[k] < cap && ( {cat.aptitudePoints > 0 && cat.aptitude[k] < cap && (
<button <button
onClick={() => onInvest(cat.id, k)} onClick={() => onInvest(cat.id, k)}
title="적성 포인트 투자" title="적성 포인트 투자"
className="ml-0.5 rounded bg-sky-600 px-1 text-[10px] font-bold text-white hover:bg-sky-700" className="ml-0.5 rounded bg-[#5d8f22] px-1 text-[10px] font-bold text-white hover:bg-[#4f8420]"
> >
+ +
</button> </button>
@@ -739,29 +758,27 @@ function CatCard({
</span> </span>
))} ))}
</div> </div>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p className="mt-1 text-xs text-[#6f5c39]">
💪 근면 {cat.stats.diligence} 💪 근면 {cat.stats.diligence}
{cat.aptitudePoints > 0 && ( {cat.aptitudePoints > 0 && (
<span className="ml-2 rounded bg-orange-100 px-1.5 py-0.5 text-[11px] font-semibold text-orange-600 dark:bg-orange-950 dark:text-orange-300"> <span className="ml-2 rounded-full bg-[rgba(214,164,60,.2)] px-2 py-0.5 text-[11px] font-semibold text-[#8a6316]">
✨ 적성 포인트 {cat.aptitudePoints} ✨ 적성 포인트 {cat.aptitudePoints}
</span> </span>
)} )}
</p> </p>
<div className="mt-2 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2">
<div className="h-1.5 flex-1 overflow-hidden rounded bg-gray-100 dark:bg-gray-800"> <div className="bar-track h-2 flex-1">
<div <div
className={`h-full ${ className={cat.status.fullness < 25 ? "bar-fill-red" : "bar-fill-green"}
cat.status.fullness < 25 ? "bg-red-400" : "bg-emerald-400"
}`}
style={{ width: `${cat.status.fullness}%` }} style={{ width: `${cat.status.fullness}%` }}
/> />
</div> </div>
<span className="text-[11px] text-gray-400"> <span className="font-num text-[11px] text-[#8a7a58]">
🍽️ {Math.floor(cat.status.fullness)} (식욕 {cat.stats.appetite}) 🍽️ {Math.floor(cat.status.fullness)} (식욕 {cat.stats.appetite})
</span> </span>
{fullnessPenalty(cat) < 1 && ( {fullnessPenalty(cat) < 1 && (
<span className="rounded bg-red-50 px-1.5 py-0.5 text-[11px] text-red-600 dark:bg-red-950 dark:text-red-300"> <span className="rounded-full bg-[rgba(214,106,74,.16)] px-2 py-0.5 text-[11px] text-[#b0472a]">
배고픔 배고픔
</span> </span>
)} )}
@@ -777,11 +794,11 @@ function CatCard({
<p className="mt-2 text-xs"> <p className="mt-2 text-xs">
{workplace ? ( {workplace ? (
<span className="text-sky-600 dark:text-sky-400"> <span className="text-[#5c8a2a]">
{workplace.emoji} {workplace.name}에서 일하는 중 {workplace.emoji} {workplace.name}에서 일하는 중
</span> </span>
) : ( ) : (
<span className="text-gray-400">쉬는 중</span> <span className="text-[#a3906c]">쉬는 중</span>
)} )}
</p> </p>
</div> </div>
@@ -805,14 +822,15 @@ function AdoptModal({
onClick={onClose} onClick={onClose}
> >
<div <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" className="panel-wood max-h-[90vh] w-full max-w-3xl overflow-hidden p-2.5"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="panel-paper max-h-[calc(90vh-20px)] overflow-y-auto p-5">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-bold">😻 입양할 고양이를 선택하세요</h2> <h2 className="font-jua text-xl text-[#4a3418]">😻 입양할 고양이를 선택하세요</h2>
<button <button
onClick={onClose} onClick={onClose}
className="rounded-lg px-3 py-1 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800" 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"
> >
✕ ✕
</button> </button>
@@ -826,7 +844,7 @@ function AdoptModal({
return ( return (
<div <div
key={cat.id} key={cat.id}
className="flex flex-col rounded-xl border border-gray-200 p-4 dark:border-gray-700" className="card-nature flex flex-col p-4"
> >
<div className="mb-2 text-center"> <div className="mb-2 text-center">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -835,8 +853,8 @@ function AdoptModal({
alt={cat.name} alt={cat.name}
className="mx-auto h-20 w-auto object-contain drop-shadow-sm" className="mx-auto h-20 w-auto object-contain drop-shadow-sm"
/> />
<div className="mt-1 font-bold">{cat.name}</div> <div className="font-jua mt-1 text-[17px] text-[#3f2c14]">{cat.name}</div>
<div className="text-xs text-gray-400"> <div className="text-xs text-[#a3906c]">
{cat.breedName}{" "} {cat.breedName}{" "}
<span className="text-amber-400"> <span className="text-amber-400">
{"★".repeat(breed?.rarity ?? 1)} {"★".repeat(breed?.rarity ?? 1)}
@@ -844,7 +862,7 @@ function AdoptModal({
</div> </div>
</div> </div>
<div className="mb-2 rounded-lg bg-gray-50 px-3 py-2 text-xs leading-relaxed dark:bg-gray-800"> <div className="chip-nature mb-2 px-3 py-2 text-xs leading-relaxed">
💪 근면 {cat.stats.diligence} · ❤️ 체력 {cat.stats.stamina} · 🍽️ 💪 근면 {cat.stats.diligence} · ❤️ 체력 {cat.stats.stamina} · 🍽️
식욕 {cat.stats.appetite} 식욕 {cat.stats.appetite}
</div> </div>
@@ -854,13 +872,13 @@ function AdoptModal({
aptitudes.map((k) => ( aptitudes.map((k) => (
<span <span
key={k} 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" className="chip-nature px-1.5 py-0.5 text-[11px]"
> >
{APTITUDE_LABEL[k]} {cat.aptitude[k]} {APTITUDE_LABEL[k]} {cat.aptitude[k]}
</span> </span>
)) ))
) : ( ) : (
<span className="text-[11px] text-gray-400">적성 없음…</span> <span className="text-[11px] text-[#a3906c]">적성 없음…</span>
)} )}
</div> </div>
@@ -868,13 +886,13 @@ function AdoptModal({
{cat.passives.length > 0 ? ( {cat.passives.length > 0 ? (
cat.passives.map((key) => <PassiveBadge key={key} passiveKey={key} />) cat.passives.map((key) => <PassiveBadge key={key} passiveKey={key} />)
) : ( ) : (
<span className="text-[11px] text-gray-400">패시브 없음</span> <span className="text-[11px] text-[#a3906c]">패시브 없음</span>
)} )}
</div> </div>
<button <button
onClick={() => onPick(cat)} onClick={() => onPick(cat)}
className="mt-auto rounded-lg bg-orange-500 py-2 text-sm font-semibold text-white hover:bg-orange-600" className="nature-btn mt-auto py-2 text-sm"
> >
이 아이로 할래요 이 아이로 할래요
</button> </button>
@@ -882,10 +900,11 @@ function AdoptModal({
); );
})} })}
</div> </div>
<p className="mt-4 text-center text-xs text-gray-400"> <p className="mt-4 text-center text-xs text-[#8a7551]">
한 마리만 선택할 수 있어요 · 패시브에 마우스를 올리면 상세 정보가 보입니다 한 마리만 선택할 수 있어요 · 패시브에 마우스를 올리면 상세 정보가 보입니다
</p> </p>
</div> </div>
</div> </div>
</div>
); );
} }
+66 -15
파일 보기
@@ -210,6 +210,52 @@ export default function VillageView({
let raf = 0; let raf = 0;
let last = performance.now(); let last = performance.now();
// next/font가 생성한 실제 폰트 패밀리명 (캔버스 텍스트용)
const juaFamily =
getComputedStyle(document.documentElement)
.getPropertyValue("--font-jua")
.trim() || "sans-serif";
// 나무 판자 배지 그리기 (자원 카운트)
const drawPlankBadge = (cx: number, cy: number, label: string) => {
ctx.font = `bold 13px ${juaFamily}`;
const w = ctx.measureText(label).width + 18;
const h = 22;
const x0 = cx - w / 2;
const y0 = cy - h;
const grad = ctx.createLinearGradient(0, y0, 0, y0 + h);
grad.addColorStop(0, "#a97640");
grad.addColorStop(0.55, "#7d4e24");
grad.addColorStop(1, "#5f3a19");
ctx.beginPath();
ctx.roundRect(x0, y0, w, h, [11, 4, 11, 4]);
ctx.fillStyle = grad;
ctx.fill();
ctx.strokeStyle = "rgba(44,26,8,0.75)";
ctx.stroke();
// 상단 하이라이트·하단 초록 글로우
ctx.beginPath();
ctx.moveTo(x0 + 4, y0 + 2);
ctx.lineTo(x0 + w - 4, y0 + 2);
ctx.strokeStyle = "rgba(255,238,200,0.35)";
ctx.stroke();
const glow = ctx.createLinearGradient(x0, 0, x0 + w, 0);
glow.addColorStop(0, "rgba(126,181,57,0)");
glow.addColorStop(0.5, "rgba(126,181,57,0.75)");
glow.addColorStop(1, "rgba(126,181,57,0)");
ctx.beginPath();
ctx.moveTo(x0 + 5, y0 + h - 2.5);
ctx.lineTo(x0 + w - 5, y0 + h - 2.5);
ctx.strokeStyle = glow;
ctx.lineWidth = 2;
ctx.stroke();
ctx.lineWidth = 1;
// 텍스트
ctx.fillStyle = "#fbf1d9";
ctx.textAlign = "center";
ctx.fillText(label, cx, y0 + 16);
};
const draw = (now: number) => { const draw = (now: number) => {
const dt = Math.min((now - last) / 1000, 0.1); const dt = Math.min((now - last) / 1000, 0.1);
last = now; last = now;
@@ -468,9 +514,13 @@ export default function VillageView({
draw: () => { draw: () => {
shadow(x, y + 6, 26); shadow(x, y + 6, 26);
drawEmoji("🏛️", x, y + 8, 48); drawEmoji("🏛️", x, y + 8, 48);
ctx.font = "bold 12px sans-serif"; ctx.font = `13px ${juaFamily}`;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.fillStyle = "#374151"; ctx.strokeStyle = "rgba(255,252,238,0.75)";
ctx.lineWidth = 3;
ctx.strokeText(`마을 회관 Lv${state.hall.level}`, x, y + 24);
ctx.lineWidth = 1;
ctx.fillStyle = "#4a3418";
ctx.fillText(`마을 회관 Lv${state.hall.level}`, x, y + 24); ctx.fillText(`마을 회관 Lv${state.hall.level}`, x, y + 24);
}, },
}); });
@@ -503,20 +553,17 @@ export default function VillageView({
} else { } else {
drawEmoji(b.emoji, x, y + 8, 42); drawEmoji(b.emoji, x, y + 8, 42);
} }
ctx.font = "bold 12px sans-serif"; ctx.font = `13px ${juaFamily}`;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.fillStyle = "#374151"; ctx.strokeStyle = "rgba(255,252,238,0.75)";
ctx.lineWidth = 3;
ctx.strokeText(b.name, x, y + 24);
ctx.lineWidth = 1;
ctx.fillStyle = "#4a3418";
ctx.fillText(b.name, x, y + 24); ctx.fillText(b.name, x, y + 24);
if (stored > 0) { if (stored > 0) {
const label = `${isKitchen ? "🍖" : RES_EMOJI[b.resource ?? "fish"]}${stored}`; const label = `${isKitchen ? "🍖" : RES_EMOJI[b.resource ?? "fish"]} ${stored}`;
ctx.font = "bold 12px sans-serif"; drawPlankBadge(x, y - 40, label);
const w = ctx.measureText(label).width + 12;
ctx.beginPath();
ctx.roundRect(x - w / 2, y - 58, w, 20, 10);
ctx.fillStyle = "#f97316";
ctx.fill();
ctx.fillStyle = "#fff";
ctx.fillText(label, x, y - 43);
} }
}, },
}); });
@@ -569,9 +616,13 @@ export default function VillageView({
} else { } else {
drawEmoji(cat.emoji, anim.x, anim.y + bob, 30, anim.flip); drawEmoji(cat.emoji, anim.x, anim.y + bob, 30, anim.flip);
} }
ctx.font = "10px sans-serif"; ctx.font = `10px ${juaFamily}`;
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.fillStyle = "rgba(55,65,81,0.85)"; ctx.strokeStyle = "rgba(255,252,238,0.7)";
ctx.lineWidth = 2.5;
ctx.strokeText(cat.name, anim.x, anim.y + 14);
ctx.lineWidth = 1;
ctx.fillStyle = "rgba(74,52,24,0.9)";
ctx.fillText(cat.name, anim.x, anim.y + 14); ctx.fillText(cat.name, anim.x, anim.y + 14);
if (working) drawEmoji("💦", anim.x + 14, anim.y - 30 + bob, 11); if (working) drawEmoji("💦", anim.x + 14, anim.y - 30 + bob, 11);
}, },