이모지 전면 이미지화: 건물 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>
이 커밋은 다음에 포함됨:
+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;
|
||||
|
||||
@@ -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/)
|
||||
|
||||
새 이슈에서 참조
사용자 차단