품종별 스프라이트 8종 적용 (6포즈: 대기·걷기2·일·요리·잠)
- 시트 8장을 연결 성분 분석으로 슬라이스 (이미지에 새겨진 파일명 텍스트는 면적 필터로 배제), 배경 플러드필 투명화, 256px 축소 - CAT_SPRITE_DIR 8품종 등록, catSprite 폴백 체인 (품종→work→기본) - VillageView: src 키 지연 로드 캐시, 부엌 작업 시 cook(냄비 젓기) 포즈 - sleep 포즈는 낮/밤 시스템(3단계)용으로 준비됨 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
이후 너비: | 높이: | 크기: 82 KiB |
|
이후 너비: | 높이: | 크기: 79 KiB |
|
이후 너비: | 높이: | 크기: 75 KiB |
|
이후 너비: | 높이: | 크기: 80 KiB |
|
이후 너비: | 높이: | 크기: 80 KiB |
|
이후 너비: | 높이: | 크기: 86 KiB |
|
이후 너비: | 높이: | 크기: 77 KiB |
|
이후 너비: | 높이: | 크기: 77 KiB |
|
이후 너비: | 높이: | 크기: 69 KiB |
|
이후 너비: | 높이: | 크기: 72 KiB |
|
이후 너비: | 높이: | 크기: 74 KiB |
|
이후 너비: | 높이: | 크기: 81 KiB |
|
이후 너비: | 높이: | 크기: 83 KiB |
|
이후 너비: | 높이: | 크기: 78 KiB |
|
이후 너비: | 높이: | 크기: 66 KiB |
|
이후 너비: | 높이: | 크기: 78 KiB |
|
이후 너비: | 높이: | 크기: 80 KiB |
|
이후 너비: | 높이: | 크기: 88 KiB |
|
이후 너비: | 높이: | 크기: 65 KiB |
|
이후 너비: | 높이: | 크기: 64 KiB |
|
이후 너비: | 높이: | 크기: 52 KiB |
|
이후 너비: | 높이: | 크기: 62 KiB |
|
이후 너비: | 높이: | 크기: 58 KiB |
|
이후 너비: | 높이: | 크기: 66 KiB |
|
이후 너비: | 높이: | 크기: 80 KiB |
|
이후 너비: | 높이: | 크기: 74 KiB |
|
이후 너비: | 높이: | 크기: 69 KiB |
|
이후 너비: | 높이: | 크기: 73 KiB |
|
이후 너비: | 높이: | 크기: 79 KiB |
|
이후 너비: | 높이: | 크기: 75 KiB |
|
이후 너비: | 높이: | 크기: 68 KiB |
|
이후 너비: | 높이: | 크기: 68 KiB |
|
이후 너비: | 높이: | 크기: 49 KiB |
|
이후 너비: | 높이: | 크기: 66 KiB |
|
이후 너비: | 높이: | 크기: 66 KiB |
|
이후 너비: | 높이: | 크기: 69 KiB |
|
이후 너비: | 높이: | 크기: 66 KiB |
|
이후 너비: | 높이: | 크기: 67 KiB |
|
이후 너비: | 높이: | 크기: 56 KiB |
|
이후 너비: | 높이: | 크기: 63 KiB |
|
이후 너비: | 높이: | 크기: 61 KiB |
|
이후 너비: | 높이: | 크기: 69 KiB |
|
이후 너비: | 높이: | 크기: 65 KiB |
|
이후 너비: | 높이: | 크기: 61 KiB |
|
이후 너비: | 높이: | 크기: 55 KiB |
|
이후 너비: | 높이: | 크기: 60 KiB |
|
이후 너비: | 높이: | 크기: 60 KiB |
|
이후 너비: | 높이: | 크기: 67 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { engine, catRate } from "@/lib/game/engine";
|
||||
import { TOOL_IMG } from "@/lib/game/data";
|
||||
import { catSprite, TOOL_IMG, type CatFrame } from "@/lib/game/data";
|
||||
import type { AptitudeKey } from "@/lib/game/types";
|
||||
|
||||
// ---------- 아이소메트릭 그리드 ----------
|
||||
@@ -93,7 +93,8 @@ export default function VillageView() {
|
||||
const toolImagesRef = useRef<Map<AptitudeKey, HTMLImageElement>>(new Map());
|
||||
const waterImgRef = useRef<HTMLImageElement | null>(null);
|
||||
const grassImgRef = useRef<HTMLImageElement | null>(null);
|
||||
const catFramesRef = useRef<Map<string, HTMLImageElement>>(new Map());
|
||||
// src 키 기반 이미지 캐시 (품종별 스프라이트 지연 로드)
|
||||
const imageCacheRef = useRef<Map<string, HTMLImageElement | null>>(new Map());
|
||||
const hitRectsRef = useRef<
|
||||
Array<{ id: string; x: number; y: number; w: number; h: number }>
|
||||
>([]);
|
||||
@@ -123,14 +124,20 @@ export default function VillageView() {
|
||||
const grass = new Image();
|
||||
grass.src = "/assets/grass.jpg";
|
||||
grass.onload = () => (grassImgRef.current = grass);
|
||||
// 고양이 스프라이트 4프레임
|
||||
for (const frame of ["idle", "walk1", "walk2", "work"]) {
|
||||
const catImg = new Image();
|
||||
catImg.src = `/assets/cats/${frame}.png`;
|
||||
catImg.onload = () => catFramesRef.current.set(frame, catImg);
|
||||
}
|
||||
}
|
||||
|
||||
// src로 이미지를 지연 로드 (로드 완료 전/실패 시 undefined 취급)
|
||||
const getImage = (src: string): HTMLImageElement | undefined => {
|
||||
const cache = imageCacheRef.current;
|
||||
if (cache.has(src)) return cache.get(src) ?? undefined;
|
||||
cache.set(src, null); // 로딩 중 표시
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => cache.set(src, img);
|
||||
img.onerror = () => cache.set(src, null);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let raf = 0;
|
||||
let last = performance.now();
|
||||
|
||||
@@ -439,8 +446,24 @@ export default function VillageView() {
|
||||
shadow(anim.x, anim.y + 2, 11);
|
||||
// 걷기는 프레임 교대 대신 한 프레임 + 총총 바운스 + 미세 기울임
|
||||
// (두 걷기 프레임의 몸통 각도가 달라 교대 시 도리도리처럼 보이는 문제 회피)
|
||||
const frameName = working ? "work" : moving ? "walk1" : "idle";
|
||||
const sprite = catFramesRef.current.get(frameName);
|
||||
const atKitchen =
|
||||
working &&
|
||||
state.buildings.find((b) => b.id === cat.assignedTo)?.type === "kitchen";
|
||||
const frameName: CatFrame = working
|
||||
? atKitchen
|
||||
? "cook"
|
||||
: "work"
|
||||
: moving
|
||||
? "walk1"
|
||||
: "idle";
|
||||
// 품종 스프라이트 → (cook 없으면 work) → 기본 스프라이트 순 폴백
|
||||
let sprite = getImage(catSprite(cat.breed, frameName));
|
||||
if (!sprite && frameName === "cook")
|
||||
sprite = getImage(catSprite(cat.breed, "work"));
|
||||
if (!sprite)
|
||||
sprite = getImage(
|
||||
catSprite("", frameName === "cook" ? "work" : frameName),
|
||||
);
|
||||
if (sprite) {
|
||||
const sh = 42;
|
||||
const sw = (sprite.width / sprite.height) * sh;
|
||||
|
||||
@@ -6,10 +6,17 @@ export const BASE_RATE = 0.2;
|
||||
|
||||
// 품종별 스프라이트 폴더 (public/assets/cats/{폴더}/). 미등록 품종은 기본(오렌지 태비) 사용
|
||||
export const CAT_SPRITE_DIR: Record<string, string> = {
|
||||
// 예: korean_shorthair: "korean_shorthair", ← 품종 시트가 준비되면 등록
|
||||
korean_shorthair: "korean_shorthair",
|
||||
munchkin: "munchkin",
|
||||
norwegian_forest: "norwegian_forest",
|
||||
bengal: "bengal",
|
||||
russian_blue: "russian_blue",
|
||||
persian: "persian",
|
||||
sphynx: "sphynx",
|
||||
maine_coon: "maine_coon",
|
||||
};
|
||||
|
||||
export type CatFrame = "idle" | "walk1" | "walk2" | "work";
|
||||
export type CatFrame = "idle" | "walk1" | "walk2" | "work" | "cook" | "sleep";
|
||||
|
||||
export function catSprite(breed: string, frame: CatFrame): string {
|
||||
const dir = CAT_SPRITE_DIR[breed];
|
||||
|
||||