품종별 스프라이트 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>
이 커밋은 다음에 포함됨:
kjs
2026-08-13 13:58:31 +09:00
co-authored by Claude Fable 5
부모 01cca9232c
커밋 e8c062d14f
50개의 변경된 파일과 42개의 추가작업 그리고 12개의 파일을 삭제
바이너리 파일은 표시되지 않음.

이후

너비:  |  높이:  |  크기: 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

+33 -10
파일 보기
@@ -2,7 +2,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { engine, catRate } from "@/lib/game/engine"; 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"; import type { AptitudeKey } from "@/lib/game/types";
// ---------- 아이소메트릭 그리드 ---------- // ---------- 아이소메트릭 그리드 ----------
@@ -93,7 +93,8 @@ export default function VillageView() {
const toolImagesRef = useRef<Map<AptitudeKey, HTMLImageElement>>(new Map()); const toolImagesRef = useRef<Map<AptitudeKey, HTMLImageElement>>(new Map());
const waterImgRef = useRef<HTMLImageElement | null>(null); const waterImgRef = useRef<HTMLImageElement | null>(null);
const grassImgRef = 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< const hitRectsRef = useRef<
Array<{ id: string; x: number; y: number; w: number; h: number }> Array<{ id: string; x: number; y: number; w: number; h: number }>
>([]); >([]);
@@ -123,14 +124,20 @@ export default function VillageView() {
const grass = new Image(); const grass = new Image();
grass.src = "/assets/grass.jpg"; grass.src = "/assets/grass.jpg";
grass.onload = () => (grassImgRef.current = grass); 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 raf = 0;
let last = performance.now(); let last = performance.now();
@@ -439,8 +446,24 @@ export default function VillageView() {
shadow(anim.x, anim.y + 2, 11); shadow(anim.x, anim.y + 2, 11);
// 걷기는 프레임 교대 대신 한 프레임 + 총총 바운스 + 미세 기울임 // 걷기는 프레임 교대 대신 한 프레임 + 총총 바운스 + 미세 기울임
// (두 걷기 프레임의 몸통 각도가 달라 교대 시 도리도리처럼 보이는 문제 회피) // (두 걷기 프레임의 몸통 각도가 달라 교대 시 도리도리처럼 보이는 문제 회피)
const frameName = working ? "work" : moving ? "walk1" : "idle"; const atKitchen =
const sprite = catFramesRef.current.get(frameName); 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) { if (sprite) {
const sh = 42; const sh = 42;
const sw = (sprite.width / sprite.height) * sh; const sw = (sprite.width / sprite.height) * sh;
+9 -2
파일 보기
@@ -6,10 +6,17 @@ export const BASE_RATE = 0.2;
// 품종별 스프라이트 폴더 (public/assets/cats/{폴더}/). 미등록 품종은 기본(오렌지 태비) 사용 // 품종별 스프라이트 폴더 (public/assets/cats/{폴더}/). 미등록 품종은 기본(오렌지 태비) 사용
export const CAT_SPRITE_DIR: Record<string, string> = { 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 { export function catSprite(breed: string, frame: CatFrame): string {
const dir = CAT_SPRITE_DIR[breed]; const dir = CAT_SPRITE_DIR[breed];