graphics3
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1018 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,74 @@
|
|||||||
|
import { Assets, Texture } from 'pixi.js';
|
||||||
|
import {
|
||||||
|
fetchGameTextureManifest,
|
||||||
|
tryResolveGameAssetFile,
|
||||||
|
type GameTextureManifest,
|
||||||
|
} from './resolveGameAssetUrl';
|
||||||
|
import { getRequiredSpriteKeys } from './spriteMapping';
|
||||||
|
|
||||||
|
export class GameSpriteRegistry {
|
||||||
|
private _manifest: GameTextureManifest | null = null;
|
||||||
|
private _textures = new Map<string, Texture>();
|
||||||
|
private _ready = false;
|
||||||
|
|
||||||
|
private _buildFallbackManifest(keys: string[]): GameTextureManifest {
|
||||||
|
const textures: Record<string, { file: string; kind: string }> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
let file: string | null = null;
|
||||||
|
if (key.startsWith('terrain.')) file = `tiles/${key}.png`;
|
||||||
|
else if (key.startsWith('prop.')) file = `prop/${key}.png`;
|
||||||
|
else if (key.startsWith('building.')) file = `building/${key}.png`;
|
||||||
|
else if (key.startsWith('enemy.')) file = `enemies/${key}.png`;
|
||||||
|
else if (key.startsWith('npc.') || key.startsWith('hero.')) file = `characters/${key}.png`;
|
||||||
|
if (!file) continue;
|
||||||
|
textures[key] = { file, kind: 'fallback' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: 0,
|
||||||
|
note: 'Fallback manifest (generated at runtime).',
|
||||||
|
textures,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get ready(): boolean {
|
||||||
|
return this._ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
get manifest(): GameTextureManifest | null {
|
||||||
|
return this._manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAll(): Promise<void> {
|
||||||
|
const requiredKeys = getRequiredSpriteKeys();
|
||||||
|
try {
|
||||||
|
this._manifest = await fetchGameTextureManifest();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Assets] Manifest load failed, using fallback manifest.', error);
|
||||||
|
this._manifest = this._buildFallbackManifest(requiredKeys);
|
||||||
|
}
|
||||||
|
for (const key of requiredKeys) {
|
||||||
|
if (!this._manifest.textures[key]) {
|
||||||
|
console.warn(`[Assets] Missing manifest entry for sprite key: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const entries = Object.entries(this._manifest.textures);
|
||||||
|
await Promise.all(
|
||||||
|
entries.map(async ([key, entry]) => {
|
||||||
|
const url = tryResolveGameAssetFile(entry.file);
|
||||||
|
if (!url) {
|
||||||
|
console.warn(`[Assets] Missing sprite file in bundle: ${entry.file}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const texture = await Assets.load<Texture>(url);
|
||||||
|
// Pixel-art tiles/props: nearest filtering avoids subpixel seam artifacts on tile borders.
|
||||||
|
texture.source.scaleMode = 'nearest';
|
||||||
|
this._textures.set(key, texture);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this._ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTexture(key: string): Texture | null {
|
||||||
|
return this._textures.get(key) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
export const ROAD_SPRITE_KEY = 'terrain.road.v1';
|
||||||
|
const DEFAULT_TERRAIN_KEY = 'terrain.grass.v1';
|
||||||
|
const HERO_SPRITE_KEY = 'hero.player.v1.south';
|
||||||
|
const MEET_PARTNER_SPRITE_KEY = 'hero.meet_partner';
|
||||||
|
|
||||||
|
/** Wilderness rest: separate props (transparent PNG), tent ~30px art height. */
|
||||||
|
export const CAMP_TENT_TEXTURE_KEY = 'prop.camp_tent.v0';
|
||||||
|
export const CAMP_FIRE_TEXTURE_KEY = 'prop.camp_fire.v0';
|
||||||
|
export const CAMP_BAG_TEXTURE_KEY = 'prop.camp_bag.v0';
|
||||||
|
|
||||||
|
const TERRAIN_TEXTURE_BY_KEY: Record<string, string> = {
|
||||||
|
plaza: 'terrain.plaza.v1',
|
||||||
|
road: ROAD_SPRITE_KEY,
|
||||||
|
dirt: 'terrain.dirt.v1',
|
||||||
|
stone: 'terrain.stone.v1',
|
||||||
|
forest_floor: 'terrain.forest_floor.v1',
|
||||||
|
ruins_floor: 'terrain.ruins_floor.v1',
|
||||||
|
canyon_floor: 'terrain.canyon_floor.v1',
|
||||||
|
swamp_floor: 'terrain.swamp_floor.v1',
|
||||||
|
volcanic_floor: 'terrain.volcanic_floor.v1',
|
||||||
|
astral_floor: 'terrain.astral_floor.v1',
|
||||||
|
grass: DEFAULT_TERRAIN_KEY,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OBJECT_TEXTURE_BY_KEY: Record<string, { v0: string; v1: string }> = {
|
||||||
|
tree: { v0: 'prop.tree.v0', v1: 'prop.tree.v1' },
|
||||||
|
rock: { v0: 'prop.rock.v0', v1: 'prop.rock.v1' },
|
||||||
|
cart: { v0: 'prop.cart.v0', v1: 'prop.cart.v1' },
|
||||||
|
barrel: { v0: 'prop.barrel.v0', v1: 'prop.barrel.v1' },
|
||||||
|
bush: { v0: 'prop.bush.v0', v1: 'prop.bush.v1' },
|
||||||
|
mushroom: { v0: 'prop.mushroom.v0', v1: 'prop.mushroom.v1' },
|
||||||
|
leaves: { v0: 'prop.leaves.v0', v1: 'prop.leaves.v1' },
|
||||||
|
stump: { v0: 'prop.stump.v0', v1: 'prop.stump.v1' },
|
||||||
|
bones: { v0: 'prop.bones.v0', v1: 'prop.bones.v1' },
|
||||||
|
ruin: { v0: 'prop.ruin.v0', v1: 'prop.ruin.v1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const NPC_TEXTURE_BY_TYPE: Record<string, string> = {
|
||||||
|
merchant: 'npc.merchant',
|
||||||
|
armorer: 'npc.armorer',
|
||||||
|
weapon: 'npc.weapon',
|
||||||
|
jeweler: 'npc.jeweler',
|
||||||
|
healer: 'npc.healer',
|
||||||
|
bounty_hunter: 'npc.bounty_hunter',
|
||||||
|
elder: 'npc.elder',
|
||||||
|
quest_giver: 'npc.quest_giver',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BUILDING_TEXTURE_BY_TYPE: Record<string, string> = {
|
||||||
|
'house.quest_giver': 'building.house.v1',
|
||||||
|
'house.merchant': 'building.house.v1',
|
||||||
|
'house.armorer': 'building.house.v1',
|
||||||
|
'house.weapon_smith': 'building.house.v1',
|
||||||
|
'house.jeweler': 'building.house.v1',
|
||||||
|
'house.bounty_hunter': 'building.house.v1',
|
||||||
|
'house.elder': 'building.house.v1',
|
||||||
|
'house.healer': 'building.house.v1',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function terrainToTextureKey(terrain: string): string {
|
||||||
|
return TERRAIN_TEXTURE_BY_KEY[terrain] ?? DEFAULT_TERRAIN_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function objectToTextureKey(obj: string, variant: number): string | null {
|
||||||
|
const entry = OBJECT_TEXTURE_BY_KEY[obj];
|
||||||
|
if (!entry) return null;
|
||||||
|
return variant > 0.5 ? entry.v1 : entry.v0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function npcTypeToTextureKey(npcType: string): string | null {
|
||||||
|
return NPC_TEXTURE_BY_TYPE[npcType] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildingTypeToTextureKey(buildingType: string): string | null {
|
||||||
|
return BUILDING_TEXTURE_BY_TYPE[buildingType] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function heroTextureKey(): string {
|
||||||
|
return HERO_SPRITE_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function meetPartnerTextureKey(): string {
|
||||||
|
return MEET_PARTNER_SPRITE_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restCampTextureKeys(): [string, string, string] {
|
||||||
|
return [CAMP_TENT_TEXTURE_KEY, CAMP_FIRE_TEXTURE_KEY, CAMP_BAG_TEXTURE_KEY];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** South-facing sprite per DB template (`enemies.type`); optional until listed in manifest + assets. */
|
||||||
|
export function enemySouthTextureKey(slug: string): string {
|
||||||
|
return `enemy.${slug}.south`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequiredSpriteKeys(): string[] {
|
||||||
|
const terrainKeys = Object.values(TERRAIN_TEXTURE_BY_KEY);
|
||||||
|
const objectKeys = Object.values(OBJECT_TEXTURE_BY_KEY).flatMap((entry) => [
|
||||||
|
entry.v0,
|
||||||
|
entry.v1,
|
||||||
|
]);
|
||||||
|
const npcKeys = Object.values(NPC_TEXTURE_BY_TYPE);
|
||||||
|
const buildingKeys = Object.values(BUILDING_TEXTURE_BY_TYPE);
|
||||||
|
return [
|
||||||
|
...new Set([
|
||||||
|
...terrainKeys,
|
||||||
|
...objectKeys,
|
||||||
|
...npcKeys,
|
||||||
|
...buildingKeys,
|
||||||
|
HERO_SPRITE_KEY,
|
||||||
|
MEET_PARTNER_SPRITE_KEY,
|
||||||
|
CAMP_TENT_TEXTURE_KEY,
|
||||||
|
CAMP_FIRE_TEXTURE_KEY,
|
||||||
|
CAMP_BAG_TEXTURE_KEY,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||