|
|
|
|
@ -56,6 +56,13 @@ const MAX_STATIC_GRAPHICS_CACHE_AREA = 2_500_000;
|
|
|
|
|
|
|
|
|
|
const TERRAIN_SEAM_BLEED_SCALE = 1.002;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Snap camera center (px, same space as worldToScreen) when computing visible tile indices.
|
|
|
|
|
* Without this, sub-pixel camera follow changes floor(minWX)/ceil(maxWX) often and forces a full
|
|
|
|
|
* tile/object pass + pool scans every frame while walking.
|
|
|
|
|
*/
|
|
|
|
|
const GROUND_CAMERA_BOUNDS_QUANTIZE_PX = TILE_HEIGHT;
|
|
|
|
|
|
|
|
|
|
/** Convert world (tile) coordinates to screen (pixel) coordinates */
|
|
|
|
|
export function worldToScreen(wx: number, wy: number): ScreenPoint {
|
|
|
|
|
return {
|
|
|
|
|
@ -136,6 +143,8 @@ export class GameRenderer {
|
|
|
|
|
endY: number;
|
|
|
|
|
}
|
|
|
|
|
| null = null;
|
|
|
|
|
/** Incremented on each full ground rebuild; used to throttle eviction passes. */
|
|
|
|
|
private _groundRebuildCounter = 0;
|
|
|
|
|
|
|
|
|
|
// Reusable Graphics (avoid GC). UI/combat overlays; world tiles are sprite-only.
|
|
|
|
|
private _heroGfx: Graphics | null = null;
|
|
|
|
|
@ -173,7 +182,8 @@ export class GameRenderer {
|
|
|
|
|
private _usedNearbyHeroSprites = new Set<string>();
|
|
|
|
|
|
|
|
|
|
private _lastEntitySortMs = 0;
|
|
|
|
|
private _entitySortIntervalMs = 120;
|
|
|
|
|
/** Depth sort is O(n log n) on entityLayer; ~8 Hz is enough for walking fights. */
|
|
|
|
|
private _entitySortIntervalMs = 200;
|
|
|
|
|
private _townDrawDirty = true;
|
|
|
|
|
private _lastTownDrawMs = 0;
|
|
|
|
|
private _townDrawIntervalMs = 120;
|
|
|
|
|
@ -502,17 +512,20 @@ export class GameRenderer {
|
|
|
|
|
drawGround(camera: Camera, screenWidth: number, screenHeight: number): void {
|
|
|
|
|
const cx = camera.finalX;
|
|
|
|
|
const cy = camera.finalY;
|
|
|
|
|
const q = GROUND_CAMERA_BOUNDS_QUANTIZE_PX;
|
|
|
|
|
const qcx = Math.round(cx / q) * q;
|
|
|
|
|
const qcy = Math.round(cy / q) * q;
|
|
|
|
|
const halfW = screenWidth / (2 * MAP_ZOOM) + TILE_WIDTH * 2;
|
|
|
|
|
const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 2;
|
|
|
|
|
|
|
|
|
|
const worldCorners = [
|
|
|
|
|
screenToWorld(cx - halfW, cy - halfH),
|
|
|
|
|
screenToWorld(cx + halfW, cy - halfH),
|
|
|
|
|
screenToWorld(cx - halfW, cy + halfH),
|
|
|
|
|
screenToWorld(cx + halfW, cy + halfH),
|
|
|
|
|
screenToWorld(qcx - halfW, qcy - halfH),
|
|
|
|
|
screenToWorld(qcx + halfW, qcy - halfH),
|
|
|
|
|
screenToWorld(qcx - halfW, qcy + halfH),
|
|
|
|
|
screenToWorld(qcx + halfW, qcy + halfH),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const renderPaddingTiles = 4;
|
|
|
|
|
const renderPaddingTiles = 5;
|
|
|
|
|
let minWX = Number.POSITIVE_INFINITY;
|
|
|
|
|
let maxWX = Number.NEGATIVE_INFINITY;
|
|
|
|
|
let minWY = Number.POSITIVE_INFINITY;
|
|
|
|
|
@ -548,6 +561,8 @@ export class GameRenderer {
|
|
|
|
|
|
|
|
|
|
this._groundDirty = false;
|
|
|
|
|
this._lastGroundBounds = { startX, endX, startY, endY };
|
|
|
|
|
this._groundRebuildCounter += 1;
|
|
|
|
|
const runTileEviction = (this._groundRebuildCounter & 1) === 0;
|
|
|
|
|
|
|
|
|
|
const hw = TILE_WIDTH / 2;
|
|
|
|
|
const hh = TILE_HEIGHT / 2;
|
|
|
|
|
@ -556,10 +571,10 @@ export class GameRenderer {
|
|
|
|
|
const usedObjectSprites = this._usedObjectSprites;
|
|
|
|
|
usedTileSprites.clear();
|
|
|
|
|
usedObjectSprites.clear();
|
|
|
|
|
const tileMinX = cx - (halfW + hw);
|
|
|
|
|
const tileMaxX = cx + (halfW + hw);
|
|
|
|
|
const tileMinY = cy - (halfH + hh);
|
|
|
|
|
const tileMaxY = cy + (halfH + hh);
|
|
|
|
|
const tileMinX = qcx - (halfW + hw);
|
|
|
|
|
const tileMaxX = qcx + (halfW + hw);
|
|
|
|
|
const tileMinY = qcy - (halfH + hh);
|
|
|
|
|
const tileMaxY = qcy + (halfH + hh);
|
|
|
|
|
|
|
|
|
|
// Pass 2 needs slightly expanded bounds for oversized props.
|
|
|
|
|
const objectPaddingTiles = 4;
|
|
|
|
|
@ -623,10 +638,10 @@ export class GameRenderer {
|
|
|
|
|
|
|
|
|
|
// Pass 2: objects (drawn after tiles so they layer on top)
|
|
|
|
|
// Slightly expanded object bounds prevent enlarged props from edge clipping.
|
|
|
|
|
const objectMinX = cx - (halfW + TILE_WIDTH * 1.5);
|
|
|
|
|
const objectMaxX = cx + (halfW + TILE_WIDTH * 1.5);
|
|
|
|
|
const objectMinY = cy - (halfH + TILE_HEIGHT * 2);
|
|
|
|
|
const objectMaxY = cy + (halfH + TILE_HEIGHT * 2);
|
|
|
|
|
const objectMinX = qcx - (halfW + TILE_WIDTH * 1.5);
|
|
|
|
|
const objectMaxX = qcx + (halfW + TILE_WIDTH * 1.5);
|
|
|
|
|
const objectMinY = qcy - (halfH + TILE_HEIGHT * 2);
|
|
|
|
|
const objectMaxY = qcy + (halfH + TILE_HEIGHT * 2);
|
|
|
|
|
for (let wx = objectStartX; wx <= objectEndX; wx++) {
|
|
|
|
|
let isoX = (wx - objectStartY) * hw;
|
|
|
|
|
let isoY = (wx + objectStartY) * hh;
|
|
|
|
|
@ -684,6 +699,7 @@ export class GameRenderer {
|
|
|
|
|
this._objectSpriteFreeList,
|
|
|
|
|
1800,
|
|
|
|
|
);
|
|
|
|
|
if (runTileEviction) {
|
|
|
|
|
const evictPaddingTiles = 8;
|
|
|
|
|
this._evictSpritesOutsideTileBounds(
|
|
|
|
|
this._tileSpritePool,
|
|
|
|
|
@ -704,6 +720,7 @@ export class GameRenderer {
|
|
|
|
|
1800,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Draw the hero (sprite only).
|
|
|
|
|
@ -717,7 +734,6 @@ export class GameRenderer {
|
|
|
|
|
let cy = iso.y;
|
|
|
|
|
|
|
|
|
|
if (texture) {
|
|
|
|
|
gfx.clear();
|
|
|
|
|
const entry = this._ensureSprite(
|
|
|
|
|
this._characterSpritePool,
|
|
|
|
|
'hero',
|
|
|
|
|
@ -766,7 +782,6 @@ export class GameRenderer {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gfx.clear();
|
|
|
|
|
const entry = this._ensureSprite(
|
|
|
|
|
this._characterSpritePool,
|
|
|
|
|
'meet_partner',
|
|
|
|
|
@ -824,7 +839,6 @@ export class GameRenderer {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gfx.clear();
|
|
|
|
|
const place = (
|
|
|
|
|
poolKey: string,
|
|
|
|
|
textureKey: string,
|
|
|
|
|
|