fix manifest

master
Denis Ranneft 1 month ago
parent 9a801d9557
commit d015f68d14

@ -20,7 +20,8 @@ RUN rm /etc/nginx/conf.d/default.conf
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder
# Vite dist: index.html, hashed chunks, public files (e.g. assets/game/manifest.json),
# and PNGs under assets/tiles|enemies|prop|building|characters|obj (see vite.config assetFileNames).
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

@ -11,6 +11,12 @@ server {
root /usr/share/nginx/html;
index index.html;
location = /assets/game/manifest.json {
default_type application/json;
add_header Cache-Control "public, max-age=300";
try_files $uri =404;
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
@ -45,7 +51,7 @@ server {
proxy_read_timeout 86400;
}
# Cache static assets aggressively
# Hashed build assets (Vite); safe to cache long-term. Excludes manifest (exact location above).
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
@ -73,6 +79,12 @@ server {
root /usr/share/nginx/html;
index index.html;
location = /assets/game/manifest.json {
default_type application/json;
add_header Cache-Control "public, max-age=300";
try_files $uri =404;
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
@ -107,7 +119,6 @@ server {
proxy_read_timeout 86400;
}
# Cache static assets aggressively
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";

File diff suppressed because it is too large Load Diff

@ -11,25 +11,6 @@ export class GameSpriteRegistry {
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;
}
@ -40,12 +21,7 @@ export class GameSpriteRegistry {
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}`);

@ -3,7 +3,11 @@
* Keep in sync with [public/assets/game/manifest.json](../../../../public/assets/game/manifest.json).
*/
const raw = import.meta.glob<string>('../../../assets/**/*.png', { eager: true, as: 'url' });
const raw = import.meta.glob<string>('../../../assets/**/*.png', {
eager: true,
query: '?url',
import: 'default',
});
function toManifestRelativePath(globModulePath: string): string {
const n = globModulePath.replace(/\\/g, '/');
@ -56,21 +60,22 @@ async function fetchManifestJson(resolvedUrl: string): Promise<GameTextureManife
const res = await fetch(resolvedUrl);
if (!res.ok) throw new Error(`Game manifest fetch failed: ${res.status}`);
const manifest = (await res.json()) as GameTextureManifest;
if (!manifest || !manifest.textures) {
throw new Error('Game manifest missing textures');
const textures = manifest?.textures;
if (
!manifest ||
textures === null ||
typeof textures !== 'object' ||
Array.isArray(textures) ||
Object.keys(textures).length === 0
) {
throw new Error('Game manifest missing or empty textures');
}
return manifest;
}
export async function fetchGameTextureManifest(url?: string): Promise<GameTextureManifest> {
const baseUrl = import.meta.env.BASE_URL ?? '/';
const resolvedUrl = url ?? `${baseUrl}assets/game/manifest.json`;
try {
return await fetchManifestJson(resolvedUrl);
} catch (error) {
if (resolvedUrl !== '/assets/game/manifest.json') {
return fetchManifestJson('/assets/game/manifest.json');
}
throw error;
}
const base = import.meta.env.BASE_URL ?? '/';
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
const resolvedUrl = url ?? `${normalizedBase}assets/game/manifest.json?version=82`;
return fetchManifestJson(resolvedUrl);
}

@ -139,8 +139,10 @@ export function resolveEnemySouthTextureKey(
): string | null {
const norm = normalizeEnemyTemplateSlug(slug);
const trySlug = (templateSlug: string): string | null => {
const k = `enemy.${templateSlug}.south`;
return getTexture(k) != null ? k : null;
const withSouth = `enemy.${templateSlug}.south`;
if (getTexture(withSouth) != null) return withSouth;
const bare = `enemy.${templateSlug}`;
return getTexture(bare) != null ? bare : null;
};
const primary = trySlug(norm);

@ -411,6 +411,8 @@ export class GameRenderer {
constructor() {
this.app = new Application();
// @ts-ignore
globalThis.__PIXI_APP__ = this.app;
this.worldContainer = new Container();
this.groundLayer = new Container();
this.entityLayer = new Container();

File diff suppressed because one or more lines are too long

@ -1,5 +1,35 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
/**
* Keep emitted PNG paths aligned with manifest `file` (relative to repo `frontend/assets/`).
* Default Vite/Rollup naming flattens everything into `dist/assets/*.png`, which breaks
* inspection of the image and confuses deployment checks.
*/
function manifestRelativeAssetStem(assetInfo) {
const candidates = [
...(assetInfo.originalFileNames ?? []),
assetInfo.originalFileName,
...(assetInfo.names ?? []),
].filter((s) => typeof s === 'string' && s.length > 0);
for (const ofn of candidates) {
const n = ofn.replace(/\\/g, '/');
const marker = '/assets/';
const i = n.indexOf(marker);
if (i !== -1) {
const rel = n.slice(i + marker.length).replace(/\.png$/i, '');
if (rel)
return rel;
}
const parts = n.split('/');
const ai = parts.lastIndexOf('assets');
if (ai !== -1 && parts[ai + 1]) {
const rel = parts.slice(ai + 1).join('/').replace(/\.png$/i, '');
if (rel)
return rel;
}
}
return undefined;
}
export default defineConfig({
plugins: [react()],
resolve: {
@ -31,6 +61,15 @@ export default defineConfig({
pixi: ['pixi.js'],
react: ['react', 'react-dom'],
},
assetFileNames(assetInfo) {
if (assetInfo.names?.some((n) => n.endsWith('.png'))) {
const stem = manifestRelativeAssetStem(assetInfo);
if (stem) {
return `assets/${stem}-[hash][extname]`;
}
}
return 'assets/[name]-[hash][extname]';
},
},
},
},

@ -1,5 +1,36 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import type { PreRenderedAsset } from 'rollup';
/**
* Keep emitted PNG paths aligned with manifest `file` (relative to repo `frontend/assets/`).
* Default Vite/Rollup naming flattens everything into `dist/assets/*.png`, which breaks
* inspection of the image and confuses deployment checks.
*/
function manifestRelativeAssetStem(assetInfo: PreRenderedAsset): string | undefined {
const candidates = [
...(assetInfo.originalFileNames ?? []),
assetInfo.originalFileName,
...(assetInfo.names ?? []),
].filter((s): s is string => typeof s === 'string' && s.length > 0);
for (const ofn of candidates) {
const n = ofn.replace(/\\/g, '/');
const marker = '/assets/';
const i = n.indexOf(marker);
if (i !== -1) {
const rel = n.slice(i + marker.length).replace(/\.png$/i, '');
if (rel) return rel;
}
const parts = n.split('/');
const ai = parts.lastIndexOf('assets');
if (ai !== -1 && parts[ai + 1]) {
const rel = parts.slice(ai + 1).join('/').replace(/\.png$/i, '');
if (rel) return rel;
}
}
return undefined;
}
export default defineConfig({
plugins: [react()],
@ -32,6 +63,15 @@ export default defineConfig({
pixi: ['pixi.js'],
react: ['react', 'react-dom'],
},
assetFileNames(assetInfo) {
if (assetInfo.names?.some((n) => n.endsWith('.png'))) {
const stem = manifestRelativeAssetStem(assetInfo);
if (stem) {
return `assets/${stem}-[hash][extname]`;
}
}
return 'assets/[name]-[hash][extname]';
},
},
},
},

@ -1,5 +1,6 @@
/**
* One-shot: fill every manifest enemy.<slug>.south missing pixellabObjectId (PixelLab API v2).
* One-shot: fill every south-facing manifest enemy entry missing pixellabObjectId (PixelLab API v2).
* Keys: legacy `enemy.<slug>.south` or `enemy.<slug>` with rotation/file `*.south.png`.
* Requires PIXELLAB_API_TOKEN.
*
* node scripts/pixellab-fill-missing-south-one-shot.mjs
@ -158,9 +159,16 @@ async function runPool(concurrency, items, fn) {
await Promise.all(workers);
}
/** `enemy.<type_slug>` or legacy `enemy.<type_slug>.south` → DB `enemies.type` slug */
function textureKeyToEnemyTypeSlug(texKey) {
let rest = texKey.startsWith('enemy.') ? texKey.slice('enemy.'.length) : texKey;
if (rest.endsWith('.south')) rest = rest.slice(0, -'.south'.length);
return rest;
}
/** @returns {{ slug: string, body: object } | { error: string }} */
function prepareCreateBody(texKey, textures, byType) {
const slug = texKey.slice('enemy.'.length, -'.south'.length);
const slug = textureKeyToEnemyTypeSlug(texKey);
const row = byType.get(slug);
if (!row) return { error: `No SQL row ${slug}` };
const refKey = ARCHETYPE_REF[row.archetype];
@ -243,7 +251,14 @@ async function main() {
const { textures } = manifest;
const missing = Object.keys(textures)
.filter((k) => k.startsWith('enemy.') && k.endsWith('.south') && !textures[k].pixellabObjectId)
.filter((k) => {
if (!k.startsWith('enemy.') || textures[k].pixellabObjectId) return false;
if (k.endsWith('.south')) return true;
const e = textures[k];
if (e.rotation === 'south') return true;
const f = e.file || '';
return f.endsWith('.south.png');
})
.sort();
if (missing.length === 0) {

Loading…
Cancel
Save