이모지 전면 이미지화: 건물 4종·자연 소품 9종·아이콘 12종

- build2.png → 고양이 집·햇볕 데크·장터·창고 일러스트 적용
- nature.png → 나무 3종(보드=소나무, 외곽=3종 순환)·데이지·튤립·밀·
  버섯·바위·꽃덤불 — 캔버스 나무/장식 이모지 제거
- icons.png → 생선·목재·돌·구이·발바닥 코인·망치·땀·접시·잠·화남·팬·하트
  판자 배지(아이콘+숫자), 말풍선(생산·식사·수면·파업·회수), HUD pill 적용
- 캔버스 이모지 렌더링이 브라우저/OS 폰트에 따라 깨지던 문제 해결
- 원본 3장 assets-src 보관, 이모지는 이미지 로드 실패 시 폴백으로 유지

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-14 10:08:30 +09:00
co-authored by Claude Fable 5
부모 3cbad820a3
커밋 401db0cb9e
31개의 변경된 파일과 175개의 추가작업 그리고 36개의 파일을 삭제
+18 -10
파일 보기
@@ -21,6 +21,7 @@ import {
catSprite,
DECO_DEFS,
HALL_LEVELS,
ICON_IMG,
KITCHEN_UPGRADE_COST,
MEAL_PRICES,
mealCap,
@@ -263,20 +264,27 @@ export default function Game() {
</button>
{/* 자원 pill */}
<div className="mt-2 flex flex-wrap items-center gap-2">
{[
["🐱", `${state.cats.length}/${state.hall.capacity}`],
["🐟", Math.floor(state.resources.fish)],
["🪵", Math.floor(state.resources.wood)],
["🪨", Math.floor(state.resources.stone)],
["🍖", meals],
["🪙", Math.floor(state.coins ?? 0)],
].map(([icon, value]) => (
{(
[
["🐱", null, `${state.cats.length}/${state.hall.capacity}`],
["🐟", ICON_IMG.fish, Math.floor(state.resources.fish)],
["🪵", ICON_IMG.wood, Math.floor(state.resources.wood)],
["🪨", ICON_IMG.stone, Math.floor(state.resources.stone)],
["🍖", ICON_IMG.meal, meals],
["🪙", ICON_IMG.coin, Math.floor(state.coins ?? 0)],
] as Array<[string, string | null, string | number]>
).map(([emoji, img, value]) => (
<span
key={icon as string}
key={emoji}
className="wood-pill flex items-center gap-1.5 py-1 pl-1 pr-3"
>
<span className="wood-pill-icon grid h-6 w-6 place-items-center text-[13px]">
{icon}
{img ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img src={img} alt="" className="h-4 w-4 object-contain" />
) : (
emoji
)}
</span>
<span className="font-num text-sm leading-none">{value}</span>
</span>
+124 -26
파일 보기
@@ -8,6 +8,8 @@ import {
BUILDING_IMG,
catSprite,
DECO_DEFS,
ICON_IMG,
NATURE_IMG,
footprintOf,
HALL_TILE,
TOOL_IMG,
@@ -23,6 +25,10 @@ import type {
} from "@/lib/game/types";
const RES_EMOJI: Record<ResourceKey, string> = { fish: "🐟", wood: "🪵", stone: "🪨" };
const RES_ICON: Record<ResourceKey, string> = { fish: "fish", wood: "wood", stone: "stone" };
const EMOJI_TO_NATURE: Record<string, string> = {
"🌼": "daisy", "🌷": "tulip", "🌾": "wheat", "🍄": "mushroom", "🪨": "rock",
};
export interface PlacingState {
type?: BuildingType; // 건물 배치·이동
@@ -100,7 +106,8 @@ interface Bubble {
x: number;
y: number;
born: number;
text: string;
text?: string; // 이모지·텍스트
icon?: string; // ICON_IMG 키 (있으면 이미지로)
}
const WALK_SPEED = 42; // px/초
@@ -260,10 +267,17 @@ export default function VillageView({
.getPropertyValue("--font-jua")
.trim() || "sans-serif";
// 나무 판자 배지 그리기 (자원 카운트)
const drawPlankBadge = (cx: number, cy: number, label: string) => {
// 나무 판자 배지 그리기 (아이콘 + 텍스트)
const drawPlankBadge = (
cx: number,
cy: number,
label: string,
iconKey?: string,
) => {
const iconImg = iconKey ? getImage(ICON_IMG[iconKey]) : undefined;
const iconW = iconImg ? 16 : 0;
ctx.font = `bold 13px ${juaFamily}`;
const w = ctx.measureText(label).width + 18;
const w = ctx.measureText(label).width + 18 + iconW;
const h = 22;
const x0 = cx - w / 2;
const y0 = cy - h;
@@ -294,10 +308,15 @@ export default function VillageView({
ctx.lineWidth = 2;
ctx.stroke();
ctx.lineWidth = 1;
// 텍스트
// 아이콘 + 텍스트
if (iconImg) {
const ih = 15;
const iw = (iconImg.width / iconImg.height) * ih;
ctx.drawImage(iconImg, x0 + 8, y0 + (h - ih) / 2, iw, ih);
}
ctx.fillStyle = "#fbf1d9";
ctx.textAlign = "center";
ctx.fillText(label, cx, y0 + 16);
ctx.fillText(label, cx + iconW / 2, y0 + 16);
};
const draw = (now: number) => {
@@ -327,7 +346,7 @@ export default function VillageView({
cat.status.fullness > anim.lastFullness + 15
) {
bubblesRef.current.push({
x: anim.x, y: anim.y - 40, born: now, text: "🍽️",
x: anim.x, y: anim.y - 40, born: now, icon: "plate",
});
}
anim.lastFullness = cat.status.fullness;
@@ -352,7 +371,7 @@ export default function VillageView({
anim.workBuildingId = null;
// 💤 버블
if (Math.random() < dt / 4) {
bubblesRef.current.push({ x: anim.x + 10, y: anim.y - 36, born: now, text: "💤" });
bubblesRef.current.push({ x: anim.x + 10, y: anim.y - 36, born: now, icon: "sleep" });
}
} else if (cat.state === "striking") {
anim.workBuildingId = null;
@@ -363,7 +382,7 @@ export default function VillageView({
anim.ty = t.y;
}
if (Math.random() < dt / 5) {
bubblesRef.current.push({ x: anim.x + 10, y: anim.y - 36, born: now, text: "💢" });
bubblesRef.current.push({ x: anim.x + 10, y: anim.y - 36, born: now, icon: "anger" });
}
} else if (cat.assignedTo) {
const building = state.buildings.find((b) => b.id === cat.assignedTo);
@@ -407,8 +426,7 @@ export default function VillageView({
if (b && catRate(cat, b.aptitude) > 0) {
bubblesRef.current.push({
x: anim.x, y: anim.y - 34, born: now,
text:
b.type === "kitchen" ? "🍳" : RES_EMOJI[b.resource ?? "fish"],
icon: b.type === "kitchen" ? "pan" : RES_ICON[b.resource ?? "fish"],
});
}
}
@@ -569,23 +587,73 @@ export default function VillageView({
for (const t of TREES) {
const x = isoX(t.i, t.j);
const y = isoY(t.i, t.j) + TH / 2;
sprites.push({ y, draw: () => { shadow(x, y + 4, 14); drawEmoji("🌲", x, y + 6, 34); } });
sprites.push({
y,
draw: () => {
const img = getImage(NATURE_IMG.pine);
if (img) {
const h = 52;
const w = (img.width / img.height) * h;
shadow(x, y + 4, 12);
ctx.drawImage(img, x - w / 2, y + 8 - h, w, h);
} else {
shadow(x, y + 4, 14);
drawEmoji("🌲", x, y + 6, 34);
}
},
});
}
for (const d of DECOR) {
const x = isoX(d.i, d.j);
const y = isoY(d.i, d.j) + TH / 2;
sprites.push({ y: y - 6, draw: () => drawEmoji(d.emoji, x, y + 4, 18) });
sprites.push({
y: y - 6,
draw: () => {
const img = getImage(NATURE_IMG[EMOJI_TO_NATURE[d.emoji] ?? ""]);
if (img) {
const h = 22;
const w = (img.width / img.height) * h;
ctx.drawImage(img, x - w / 2, y + 6 - h, w, h);
} else drawEmoji(d.emoji, x, y + 4, 18);
},
});
}
// 보드 밖 배경 장식 (마을을 둘러싼 숲)
for (const t of OUTER_TREES) {
// 보드 밖 배경 장식 (마을을 둘러싼 숲 — 나무 3종 순환)
const OUTER_KINDS = ["pine", "leafy", "pine_small"];
OUTER_TREES.forEach((t, idx) => {
const x = isoX(t.i, t.j);
const y = isoY(t.i, t.j) + TH / 2;
sprites.push({ y, draw: () => { shadow(x, y + 4, 15); drawEmoji("🌲", x, y + 6, 38); } });
}
const kind = OUTER_KINDS[idx % 3];
sprites.push({
y,
draw: () => {
const img = getImage(NATURE_IMG[kind]);
if (img) {
const h = kind === "pine_small" ? 42 : 58;
const w = (img.width / img.height) * h;
shadow(x, y + 4, 13);
ctx.drawImage(img, x - w / 2, y + 8 - h, w, h);
} else {
shadow(x, y + 4, 15);
drawEmoji("🌲", x, y + 6, 38);
}
},
});
});
for (const d of OUTER_DECOR) {
const x = isoX(d.i, d.j);
const y = isoY(d.i, d.j) + TH / 2;
sprites.push({ y: y - 6, draw: () => drawEmoji(d.emoji, x, y + 4, 18) });
sprites.push({
y: y - 6,
draw: () => {
const img = getImage(NATURE_IMG[EMOJI_TO_NATURE[d.emoji] ?? "bush"]);
if (img) {
const h = 24;
const w = (img.width / img.height) * h;
ctx.drawImage(img, x - w / 2, y + 6 - h, w, h);
} else drawEmoji(d.emoji, x, y + 4, 18);
},
});
}
hitRectsRef.current = [];
@@ -658,7 +726,7 @@ export default function VillageView({
const remain = Math.max(0, Math.ceil(((b.constructionUntil ?? 0) - Date.now()) / 1000));
const mm = Math.floor(remain / 60);
const ss = remain % 60;
drawPlankBadge(x, y - imgW * 0.5, `🔨 ${mm}:${String(ss).padStart(2, "0")}`);
drawPlankBadge(x, y - imgW * 0.5, `${mm}:${String(ss).padStart(2, "0")}`, "hammer");
ctx.font = `13px ${juaFamily}`;
ctx.textAlign = "center";
ctx.strokeStyle = "rgba(255,252,238,0.75)";
@@ -689,8 +757,10 @@ export default function VillageView({
ctx.fillStyle = "#4a3418";
ctx.fillText(b.name, x, baseY + 17);
if (stored > 0) {
const label = `${isKitchen ? "🍖" : RES_EMOJI[b.resource ?? "fish"]} ${stored}`;
drawPlankBadge(x, y - imgW * 0.55, label);
drawPlankBadge(
x, y - imgW * 0.55, `${stored}`,
isKitchen ? "meal" : RES_ICON[b.resource ?? "fish"],
);
}
},
});
@@ -750,7 +820,13 @@ 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 && !cat.state) drawEmoji("💦", anim.x + 14, anim.y - 30 + bob, 11);
if (working && !cat.state) {
const sw = getImage(ICON_IMG.sweat);
if (sw) {
const ih = 13;
ctx.drawImage(sw, anim.x + 10, anim.y - 40 + bob, (sw.width / sw.height) * ih, ih);
} else drawEmoji("💦", anim.x + 14, anim.y - 30 + bob, 11);
}
},
});
}
@@ -863,7 +939,29 @@ export default function VillageView({
continue;
}
ctx.globalAlpha = 1 - age;
drawEmoji(bub.text, bub.x, bub.y - age * 22, 14);
const by = bub.y - age * 22;
const iconImg = bub.icon ? getImage(ICON_IMG[bub.icon]) : undefined;
if (iconImg) {
const ih = 16;
const iw = (iconImg.width / iconImg.height) * ih;
const textW = bub.text
? (ctx.font = `bold 12px ${juaFamily}`, ctx.measureText(bub.text).width)
: 0;
const total = iw + (textW ? textW + 4 : 0);
ctx.drawImage(iconImg, bub.x - total / 2, by - ih + 4, iw, ih);
if (bub.text) {
ctx.fillStyle = "#4a3418";
ctx.strokeStyle = "rgba(255,252,238,0.8)";
ctx.lineWidth = 3;
ctx.textAlign = "left";
ctx.strokeText(bub.text, bub.x - total / 2 + iw + 4, by);
ctx.lineWidth = 1;
ctx.fillText(bub.text, bub.x - total / 2 + iw + 4, by);
ctx.textAlign = "center";
}
} else if (bub.text) {
drawEmoji(bub.text, bub.x, by, 14);
}
ctx.globalAlpha = 1;
}
@@ -915,9 +1013,9 @@ export default function VillageView({
const gained = Math.floor(s.resources[b.resource]) - before;
bubblesRef.current.push({
x: r.x + r.w / 2, y: r.y, born: performance.now(),
text: err
? "📦 창고 가득!"
: `+${gained} ${RES_EMOJI[b.resource]}`,
...(err
? { text: "📦 창고 가득!" }
: { icon: RES_ICON[b.resource], text: `+${gained}` }),
});
}
return;
+33
파일 보기
@@ -31,6 +31,39 @@ export const BUILDING_IMG: Record<string, string> = {
lumberyard: "/assets/buildings/lumberyard.png",
quarry: "/assets/buildings/quarry.png",
kitchen: "/assets/buildings/kitchen.png",
cat_house: "/assets/buildings/cat_house.png",
sun_deck: "/assets/buildings/sun_deck.png",
market: "/assets/buildings/market.png",
warehouse: "/assets/buildings/warehouse.png",
};
// 자연 소품 일러스트 (public/assets/nature/)
export const NATURE_IMG: Record<string, string> = {
pine: "/assets/nature/pine.png",
leafy: "/assets/nature/leafy.png",
pine_small: "/assets/nature/pine_small.png",
daisy: "/assets/nature/daisy.png",
tulip: "/assets/nature/tulip.png",
wheat: "/assets/nature/wheat.png",
mushroom: "/assets/nature/mushroom.png",
rock: "/assets/nature/rock.png",
bush: "/assets/nature/bush.png",
};
// 아이콘 일러스트 — 배지·버블·HUD (public/assets/icons/)
export const ICON_IMG: Record<string, string> = {
fish: "/assets/icons/fish.png",
wood: "/assets/icons/wood.png",
stone: "/assets/icons/stone.png",
meal: "/assets/icons/meal.png",
coin: "/assets/icons/coin.png",
hammer: "/assets/icons/hammer.png",
sweat: "/assets/icons/sweat.png",
plate: "/assets/icons/plate.png",
sleep: "/assets/icons/sleep.png",
anger: "/assets/icons/anger.png",
pan: "/assets/icons/pan.png",
heart: "/assets/icons/heart.png",
};
// 적성별 도구 일러스트 (public/assets/tools/)