|
|
|
@ -42,8 +42,18 @@ type SpritePoolEntry = {
|
|
|
|
textureKey: string;
|
|
|
|
textureKey: string;
|
|
|
|
worldX?: number;
|
|
|
|
worldX?: number;
|
|
|
|
worldY?: number;
|
|
|
|
worldY?: number;
|
|
|
|
|
|
|
|
/** Set when sprite is hidden by pooling; cleared when shown again. */
|
|
|
|
|
|
|
|
hiddenSinceMs?: number;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const SPRITE_HIDDEN_EVICT_MS = 3000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Max width×height (local px) for baking Graphics into a texture via {@link Graphics#cacheAsTexture}.
|
|
|
|
|
|
|
|
* Procedural ground fills the whole visible tile grid — caching would allocate a huge RenderTexture.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
const MAX_STATIC_GRAPHICS_CACHE_AREA = 2_500_000;
|
|
|
|
|
|
|
|
|
|
|
|
const TERRAIN_SEAM_BLEED_SCALE = 1.002;
|
|
|
|
const TERRAIN_SEAM_BLEED_SCALE = 1.002;
|
|
|
|
|
|
|
|
|
|
|
|
/** Convert world (tile) coordinates to screen (pixel) coordinates */
|
|
|
|
/** Convert world (tile) coordinates to screen (pixel) coordinates */
|
|
|
|
@ -129,7 +139,7 @@ export class GameRenderer {
|
|
|
|
| null = null;
|
|
|
|
| null = null;
|
|
|
|
private _lastSpritesReady = false;
|
|
|
|
private _lastSpritesReady = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Reusable Graphics objects (avoid GC in hot path)
|
|
|
|
// Reusable Graphics (avoid GC). Animated / full-screen paths are not texture-cached — see drawTowns / drawGround.
|
|
|
|
private _groundGfx: Graphics | null = null;
|
|
|
|
private _groundGfx: Graphics | null = null;
|
|
|
|
private _heroGfx: Graphics | null = null;
|
|
|
|
private _heroGfx: Graphics | null = null;
|
|
|
|
/** Second adventurer silhouette during hero_meet (opponent at server stand position). */
|
|
|
|
/** Second adventurer silhouette during hero_meet (opponent at server stand position). */
|
|
|
|
@ -372,12 +382,71 @@ export class GameRenderer {
|
|
|
|
return entry;
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private _hideUnusedSprites(pool: Map<string, SpritePoolEntry>, used: Set<string>): void {
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Hide pooled sprites not in `used`. After {@link SPRITE_HIDDEN_EVICT_MS} of being hidden,
|
|
|
|
|
|
|
|
* remove from the pool and recycle or destroy the sprite.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private _hideUnusedSprites(
|
|
|
|
|
|
|
|
pool: Map<string, SpritePoolEntry>,
|
|
|
|
|
|
|
|
used: Set<string>,
|
|
|
|
|
|
|
|
nowMs: number,
|
|
|
|
|
|
|
|
freeList?: Sprite[],
|
|
|
|
|
|
|
|
maxFreeListSize?: number,
|
|
|
|
|
|
|
|
): void {
|
|
|
|
|
|
|
|
const toRemove: string[] = [];
|
|
|
|
for (const [key, entry] of pool) {
|
|
|
|
for (const [key, entry] of pool) {
|
|
|
|
entry.sprite.visible = used.has(key);
|
|
|
|
if (used.has(key)) {
|
|
|
|
|
|
|
|
entry.sprite.visible = true;
|
|
|
|
|
|
|
|
entry.hiddenSinceMs = undefined;
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
entry.sprite.visible = false;
|
|
|
|
|
|
|
|
if (entry.hiddenSinceMs === undefined) {
|
|
|
|
|
|
|
|
entry.hiddenSinceMs = nowMs;
|
|
|
|
|
|
|
|
} else if (nowMs - entry.hiddenSinceMs >= SPRITE_HIDDEN_EVICT_MS) {
|
|
|
|
|
|
|
|
toRemove.push(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key of toRemove) {
|
|
|
|
|
|
|
|
const entry = pool.get(key);
|
|
|
|
|
|
|
|
if (!entry) continue;
|
|
|
|
|
|
|
|
pool.delete(key);
|
|
|
|
|
|
|
|
const sprite = entry.sprite;
|
|
|
|
|
|
|
|
sprite.visible = false;
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
|
|
freeList !== undefined &&
|
|
|
|
|
|
|
|
maxFreeListSize !== undefined &&
|
|
|
|
|
|
|
|
freeList.length < maxFreeListSize
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
freeList.push(sprite);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
sprite.destroy();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Drop GPU cache before mutating vector paths (Pixi 8: prefer cacheAsTexture over legacy cacheAsBitmap). */
|
|
|
|
|
|
|
|
private _releaseGraphicsTextureCache(gfx: Graphics | null): void {
|
|
|
|
|
|
|
|
if (!gfx?.isCachedAsTexture) return;
|
|
|
|
|
|
|
|
gfx.cacheAsTexture(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Bake static vector layers to a single quad. Skips huge bounds (see {@link MAX_STATIC_GRAPHICS_CACHE_AREA}).
|
|
|
|
|
|
|
|
* Do not use for per-frame animated Graphics (hero, enemy, NPC, thought bubbles).
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private _cacheStaticGraphicsIfBounded(gfx: Graphics | null): void {
|
|
|
|
|
|
|
|
if (!gfx || gfx.isCachedAsTexture) return;
|
|
|
|
|
|
|
|
const b = gfx.getLocalBounds();
|
|
|
|
|
|
|
|
const area = Math.max(0, b.width) * Math.max(0, b.height);
|
|
|
|
|
|
|
|
if (area < 16 || area > MAX_STATIC_GRAPHICS_CACHE_AREA) return;
|
|
|
|
|
|
|
|
gfx.cacheAsTexture({
|
|
|
|
|
|
|
|
antialias: false,
|
|
|
|
|
|
|
|
resolution: 1,
|
|
|
|
|
|
|
|
scaleMode: 'linear',
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private _evictSpritesOutsideTileBounds(
|
|
|
|
private _evictSpritesOutsideTileBounds(
|
|
|
|
pool: Map<string, SpritePoolEntry>,
|
|
|
|
pool: Map<string, SpritePoolEntry>,
|
|
|
|
minX: number,
|
|
|
|
minX: number,
|
|
|
|
@ -601,6 +670,9 @@ export class GameRenderer {
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Draw visible ground tiles and decorative objects.
|
|
|
|
* Draw visible ground tiles and decorative objects.
|
|
|
|
* Terrain is procedurally generated — the world is endless.
|
|
|
|
* Terrain is procedurally generated — the world is endless.
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
* Vector fallback spans the full visible tile grid; we do not `cacheAsTexture` on `_groundGfx`
|
|
|
|
|
|
|
|
* (bounds would create a very large RenderTexture). Prefer terrain/object sprites when loaded.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
drawGround(camera: Camera, screenWidth: number, screenHeight: number): void {
|
|
|
|
drawGround(camera: Camera, screenWidth: number, screenHeight: number): void {
|
|
|
|
const gfx = this._groundGfx;
|
|
|
|
const gfx = this._groundGfx;
|
|
|
|
@ -817,9 +889,22 @@ export class GameRenderer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const hideSpritesNow = performance.now();
|
|
|
|
if (spritesReady) {
|
|
|
|
if (spritesReady) {
|
|
|
|
this._hideUnusedSprites(this._tileSpritePool, usedTileSprites);
|
|
|
|
this._hideUnusedSprites(
|
|
|
|
this._hideUnusedSprites(this._objectSpritePool, usedObjectSprites);
|
|
|
|
this._tileSpritePool,
|
|
|
|
|
|
|
|
usedTileSprites,
|
|
|
|
|
|
|
|
hideSpritesNow,
|
|
|
|
|
|
|
|
this._tileSpriteFreeList,
|
|
|
|
|
|
|
|
2800,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
this._hideUnusedSprites(
|
|
|
|
|
|
|
|
this._objectSpritePool,
|
|
|
|
|
|
|
|
usedObjectSprites,
|
|
|
|
|
|
|
|
hideSpritesNow,
|
|
|
|
|
|
|
|
this._objectSpriteFreeList,
|
|
|
|
|
|
|
|
1800,
|
|
|
|
|
|
|
|
);
|
|
|
|
const evictPaddingTiles = 8;
|
|
|
|
const evictPaddingTiles = 8;
|
|
|
|
this._evictSpritesOutsideTileBounds(
|
|
|
|
this._evictSpritesOutsideTileBounds(
|
|
|
|
this._tileSpritePool,
|
|
|
|
this._tileSpritePool,
|
|
|
|
@ -840,8 +925,20 @@ export class GameRenderer {
|
|
|
|
1800,
|
|
|
|
1800,
|
|
|
|
);
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
this._hideUnusedSprites(this._tileSpritePool, this._emptySpriteSet);
|
|
|
|
this._hideUnusedSprites(
|
|
|
|
this._hideUnusedSprites(this._objectSpritePool, this._emptySpriteSet);
|
|
|
|
this._tileSpritePool,
|
|
|
|
|
|
|
|
this._emptySpriteSet,
|
|
|
|
|
|
|
|
hideSpritesNow,
|
|
|
|
|
|
|
|
this._tileSpriteFreeList,
|
|
|
|
|
|
|
|
2800,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
this._hideUnusedSprites(
|
|
|
|
|
|
|
|
this._objectSpritePool,
|
|
|
|
|
|
|
|
this._emptySpriteSet,
|
|
|
|
|
|
|
|
hideSpritesNow,
|
|
|
|
|
|
|
|
this._objectSpriteFreeList,
|
|
|
|
|
|
|
|
1800,
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1151,9 +1248,9 @@ export class GameRenderer {
|
|
|
|
entry.sprite.y = cy;
|
|
|
|
entry.sprite.y = cy;
|
|
|
|
entry.sprite.roundPixels = true;
|
|
|
|
entry.sprite.roundPixels = true;
|
|
|
|
const th = Math.max(1, tex.height || tex.width || 48);
|
|
|
|
const th = Math.max(1, tex.height || tex.width || 48);
|
|
|
|
const targetH = 52;
|
|
|
|
const targetH = 100;
|
|
|
|
entry.sprite.scale.set(targetH / th);
|
|
|
|
entry.sprite.scale.set(targetH / th);
|
|
|
|
entry.sprite.zIndex = cy + 100;
|
|
|
|
entry.sprite.zIndex = cy + 90;
|
|
|
|
entry.sprite.visible = true;
|
|
|
|
entry.sprite.visible = true;
|
|
|
|
drawEnemyHpBarOnly(gfx, enemySlug, enemyArchetype, cx, cy, hp, maxHp);
|
|
|
|
drawEnemyHpBarOnly(gfx, enemySlug, enemyArchetype, cx, cy, hp, maxHp);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
@ -1727,6 +1824,8 @@ export class GameRenderer {
|
|
|
|
this._lastTownDrawMs = now;
|
|
|
|
this._lastTownDrawMs = now;
|
|
|
|
this._lastTownCameraX = cx;
|
|
|
|
this._lastTownCameraX = cx;
|
|
|
|
this._lastTownCameraY = cy;
|
|
|
|
this._lastTownCameraY = cy;
|
|
|
|
|
|
|
|
this._releaseGraphicsTextureCache(gfx);
|
|
|
|
|
|
|
|
this._releaseGraphicsTextureCache(iconGfx);
|
|
|
|
gfx.clear();
|
|
|
|
gfx.clear();
|
|
|
|
iconGfx.clear();
|
|
|
|
iconGfx.clear();
|
|
|
|
|
|
|
|
|
|
|
|
@ -1859,7 +1958,9 @@ export class GameRenderer {
|
|
|
|
labelIdx++;
|
|
|
|
labelIdx++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this._hideUnusedSprites(this._buildingSpritePool, usedBuildingSprites);
|
|
|
|
this._hideUnusedSprites(this._buildingSpritePool, usedBuildingSprites, now);
|
|
|
|
|
|
|
|
this._cacheStaticGraphicsIfBounded(gfx);
|
|
|
|
|
|
|
|
this._cacheStaticGraphicsIfBounded(iconGfx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -1875,6 +1976,7 @@ export class GameRenderer {
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
const gfx = this._npcGfx;
|
|
|
|
const gfx = this._npcGfx;
|
|
|
|
if (!gfx) return;
|
|
|
|
if (!gfx) return;
|
|
|
|
|
|
|
|
// Per-frame idle sway + culling: keep vector path, no cacheAsTexture (would stale or rebuild every frame).
|
|
|
|
gfx.clear();
|
|
|
|
gfx.clear();
|
|
|
|
|
|
|
|
|
|
|
|
for (const lbl of this._npcLabels) {
|
|
|
|
for (const lbl of this._npcLabels) {
|
|
|
|
@ -2109,7 +2211,7 @@ export class GameRenderer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this._hideUnusedSprites(this._npcSpritePool, usedNpcSprites);
|
|
|
|
this._hideUnusedSprites(this._npcSpritePool, usedNpcSprites, now);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -2158,7 +2260,13 @@ export class GameRenderer {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this._hideUnusedSprites(this._townObjectSpritePool, usedTownObjects);
|
|
|
|
this._hideUnusedSprites(
|
|
|
|
|
|
|
|
this._townObjectSpritePool,
|
|
|
|
|
|
|
|
usedTownObjects,
|
|
|
|
|
|
|
|
performance.now(),
|
|
|
|
|
|
|
|
this._townObjectSpriteFreeList,
|
|
|
|
|
|
|
|
1200,
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Clear NPC visuals when there are none to render */
|
|
|
|
/** Clear NPC visuals when there are none to render */
|
|
|
|
@ -2167,7 +2275,7 @@ export class GameRenderer {
|
|
|
|
for (const lbl of this._npcLabels) {
|
|
|
|
for (const lbl of this._npcLabels) {
|
|
|
|
lbl.visible = false;
|
|
|
|
lbl.visible = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._hideUnusedSprites(this._npcSpritePool, this._emptySpriteSet);
|
|
|
|
this._hideUnusedSprites(this._npcSpritePool, this._emptySpriteSet, performance.now());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -2261,7 +2369,7 @@ export class GameRenderer {
|
|
|
|
gfx.zIndex = cy + 100;
|
|
|
|
gfx.zIndex = cy + 100;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this._hideUnusedSprites(this._nearbyHeroSpritePool, usedNearbySprites);
|
|
|
|
this._hideUnusedSprites(this._nearbyHeroSpritePool, usedNearbySprites, performance.now());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
@ -2335,7 +2443,7 @@ export class GameRenderer {
|
|
|
|
for (const lbl of this._nearbyHeroLabels) {
|
|
|
|
for (const lbl of this._nearbyHeroLabels) {
|
|
|
|
lbl.visible = false;
|
|
|
|
lbl.visible = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this._hideUnusedSprites(this._nearbyHeroSpritePool, this._emptySpriteSet);
|
|
|
|
this._hideUnusedSprites(this._nearbyHeroSpritePool, this._emptySpriteSet, performance.now());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Sort entity layer by y-position for correct isometric depth */
|
|
|
|
/** Sort entity layer by y-position for correct isometric depth */
|
|
|
|
|