|
|
|
|
@ -106,10 +106,11 @@ export class GameRenderer {
|
|
|
|
|
private _usedTileSprites = new Set<string>();
|
|
|
|
|
private _usedObjectSprites = new Set<string>();
|
|
|
|
|
private _emptySpriteSet = new Set<string>();
|
|
|
|
|
private _groundTerrainCache = new Map<string, string>();
|
|
|
|
|
private _buildingSpritePool = new Map<string, SpritePoolEntry>();
|
|
|
|
|
private _characterSpritePool = new Map<string, SpritePoolEntry>();
|
|
|
|
|
private _npcSpritePool = new Map<string, SpritePoolEntry>();
|
|
|
|
|
private _usedBuildingSprites = new Set<string>();
|
|
|
|
|
private _usedNpcSprites = new Set<string>();
|
|
|
|
|
private _groundDirty = true;
|
|
|
|
|
private _lastGroundBounds:
|
|
|
|
|
| {
|
|
|
|
|
@ -157,6 +158,13 @@ export class GameRenderer {
|
|
|
|
|
|
|
|
|
|
private _lastEntitySortMs = 0;
|
|
|
|
|
private _entitySortIntervalMs = 120;
|
|
|
|
|
private _townDrawDirty = true;
|
|
|
|
|
private _lastTownDrawMs = 0;
|
|
|
|
|
private _townDrawIntervalMs = 120;
|
|
|
|
|
private _lastTownCameraX = Number.NaN;
|
|
|
|
|
private _lastTownCameraY = Number.NaN;
|
|
|
|
|
private _lastTownsRef: TownData[] | null = null;
|
|
|
|
|
private _lastTownCount = -1;
|
|
|
|
|
|
|
|
|
|
private _drawBush(gfx: Graphics, x: number, y: number, variant: number): void {
|
|
|
|
|
const s = (0.9 + variant * 0.25) * 3.5;
|
|
|
|
|
@ -571,6 +579,7 @@ export class GameRenderer {
|
|
|
|
|
this.worldContainer.x = viewport.width / 2;
|
|
|
|
|
this.worldContainer.y = viewport.height / 2;
|
|
|
|
|
this.worldContainer.scale.set(MAP_ZOOM);
|
|
|
|
|
this._townDrawDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -641,25 +650,40 @@ export class GameRenderer {
|
|
|
|
|
const usedObjectSprites = this._usedObjectSprites;
|
|
|
|
|
usedTileSprites.clear();
|
|
|
|
|
usedObjectSprites.clear();
|
|
|
|
|
const terrainCache = this._groundTerrainCache;
|
|
|
|
|
terrainCache.clear();
|
|
|
|
|
const tileMinX = cx - (halfW + hw);
|
|
|
|
|
const tileMaxX = cx + (halfW + hw);
|
|
|
|
|
const tileMinY = cy - (halfH + hh);
|
|
|
|
|
const tileMaxY = cy + (halfH + hh);
|
|
|
|
|
|
|
|
|
|
// Pass 2 needs slightly expanded bounds for oversized props.
|
|
|
|
|
const objectPaddingTiles = 4;
|
|
|
|
|
const objectStartX = startX - objectPaddingTiles;
|
|
|
|
|
const objectEndX = endX + objectPaddingTiles;
|
|
|
|
|
const objectStartY = startY - objectPaddingTiles;
|
|
|
|
|
const objectEndY = endY + objectPaddingTiles;
|
|
|
|
|
const objectGridH = objectEndY - objectStartY + 1;
|
|
|
|
|
const terrainGrid = new Array<string | undefined>(
|
|
|
|
|
(objectEndX - objectStartX + 1) * objectGridH,
|
|
|
|
|
);
|
|
|
|
|
const terrainIndex = (wx: number, wy: number): number =>
|
|
|
|
|
(wx - objectStartX) * objectGridH + (wy - objectStartY);
|
|
|
|
|
const terrainAt = (wx: number, wy: number): string => {
|
|
|
|
|
const key = `${wx},${wy}`;
|
|
|
|
|
const cached = terrainCache.get(key);
|
|
|
|
|
if (cached) return cached;
|
|
|
|
|
const idx = terrainIndex(wx, wy);
|
|
|
|
|
const cached = terrainGrid[idx];
|
|
|
|
|
if (cached !== undefined) return cached;
|
|
|
|
|
const terrain = proceduralTerrain(wx, wy, terrainCtx);
|
|
|
|
|
terrainCache.set(key, terrain);
|
|
|
|
|
terrainGrid[idx] = terrain;
|
|
|
|
|
return terrain;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Pass 1: tiles
|
|
|
|
|
for (let wx = startX; wx <= endX; wx++) {
|
|
|
|
|
let isoX = (wx - startY) * hw;
|
|
|
|
|
let isoY = (wx + startY) * hh;
|
|
|
|
|
for (let wy = startY; wy <= endY; wy++) {
|
|
|
|
|
const iso = worldToScreen(wx, wy);
|
|
|
|
|
if (
|
|
|
|
|
Math.abs(iso.x - cx) > halfW + hw ||
|
|
|
|
|
Math.abs(iso.y - cy) > halfH + hh
|
|
|
|
|
) {
|
|
|
|
|
if (isoX < tileMinX || isoX > tileMaxX || isoY < tileMinY || isoY > tileMaxY) {
|
|
|
|
|
isoX -= hw;
|
|
|
|
|
isoY += hh;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const terrain = terrainAt(wx, wy);
|
|
|
|
|
@ -680,9 +704,9 @@ export class GameRenderer {
|
|
|
|
|
wy,
|
|
|
|
|
this._tileSpriteFreeList,
|
|
|
|
|
);
|
|
|
|
|
entry.sprite.x = iso.x;
|
|
|
|
|
entry.sprite.y = iso.y + hh;
|
|
|
|
|
entry.sprite.zIndex = iso.y + hh;
|
|
|
|
|
entry.sprite.x = isoX;
|
|
|
|
|
entry.sprite.y = isoY + hh;
|
|
|
|
|
entry.sprite.zIndex = isoY + hh;
|
|
|
|
|
const texW = entry.sprite.texture.orig.width || entry.sprite.texture.width || TILE_WIDTH;
|
|
|
|
|
const scale = (TILE_WIDTH / texW) * TERRAIN_SEAM_BLEED_SCALE;
|
|
|
|
|
entry.sprite.scale.set(scale);
|
|
|
|
|
@ -691,18 +715,18 @@ export class GameRenderer {
|
|
|
|
|
const color = this._terrainColors(terrain, dark);
|
|
|
|
|
|
|
|
|
|
gfx.poly([
|
|
|
|
|
iso.x, iso.y - hh,
|
|
|
|
|
iso.x + hw, iso.y,
|
|
|
|
|
iso.x, iso.y + hh,
|
|
|
|
|
iso.x - hw, iso.y,
|
|
|
|
|
isoX, isoY - hh,
|
|
|
|
|
isoX + hw, isoY,
|
|
|
|
|
isoX, isoY + hh,
|
|
|
|
|
isoX - hw, isoY,
|
|
|
|
|
]);
|
|
|
|
|
gfx.fill({ color, alpha: 1 });
|
|
|
|
|
|
|
|
|
|
gfx.poly([
|
|
|
|
|
iso.x, iso.y - hh,
|
|
|
|
|
iso.x + hw, iso.y,
|
|
|
|
|
iso.x, iso.y + hh,
|
|
|
|
|
iso.x - hw, iso.y,
|
|
|
|
|
isoX, isoY - hh,
|
|
|
|
|
isoX + hw, isoY,
|
|
|
|
|
isoX, isoY + hh,
|
|
|
|
|
isoX - hw, isoY,
|
|
|
|
|
]);
|
|
|
|
|
gfx.stroke({
|
|
|
|
|
color: this._terrainStrokeColor(terrain),
|
|
|
|
|
@ -710,28 +734,33 @@ export class GameRenderer {
|
|
|
|
|
alpha: 0.25,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
isoX -= hw;
|
|
|
|
|
isoY += hh;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pass 2: objects (drawn after tiles so they layer on top)
|
|
|
|
|
// Slightly expanded object bounds prevent enlarged props from edge clipping.
|
|
|
|
|
const objectPaddingTiles = 4;
|
|
|
|
|
const objectStartX = startX - objectPaddingTiles;
|
|
|
|
|
const objectEndX = endX + objectPaddingTiles;
|
|
|
|
|
const objectStartY = startY - objectPaddingTiles;
|
|
|
|
|
const objectEndY = endY + objectPaddingTiles;
|
|
|
|
|
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);
|
|
|
|
|
for (let wx = objectStartX; wx <= objectEndX; wx++) {
|
|
|
|
|
let isoX = (wx - objectStartY) * hw;
|
|
|
|
|
let isoY = (wx + objectStartY) * hh;
|
|
|
|
|
for (let wy = objectStartY; wy <= objectEndY; wy++) {
|
|
|
|
|
const iso = worldToScreen(wx, wy);
|
|
|
|
|
if (
|
|
|
|
|
Math.abs(iso.x - cx) > halfW + TILE_WIDTH * 1.5 ||
|
|
|
|
|
Math.abs(iso.y - cy) > halfH + TILE_HEIGHT * 2
|
|
|
|
|
) {
|
|
|
|
|
if (isoX < objectMinX || isoX > objectMaxX || isoY < objectMinY || isoY > objectMaxY) {
|
|
|
|
|
isoX -= hw;
|
|
|
|
|
isoY += hh;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const terrainHere = terrainAt(wx, wy);
|
|
|
|
|
const obj = proceduralObject(wx, wy, terrainHere, terrainCtx);
|
|
|
|
|
if (!obj) continue;
|
|
|
|
|
if (!obj) {
|
|
|
|
|
isoX -= hw;
|
|
|
|
|
isoY += hh;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const variant = tileHash(wx, wy, 999);
|
|
|
|
|
const objTextureKey = spritesReady ? objectToTextureKey(obj, variant) : null;
|
|
|
|
|
const objTexture = objTextureKey ? this._spriteRegistry.getTexture(objTextureKey) : null;
|
|
|
|
|
@ -748,25 +777,27 @@ export class GameRenderer {
|
|
|
|
|
wy,
|
|
|
|
|
this._objectSpriteFreeList,
|
|
|
|
|
);
|
|
|
|
|
entry.sprite.x = iso.x;
|
|
|
|
|
entry.sprite.y = iso.y;
|
|
|
|
|
entry.sprite.zIndex = iso.y;
|
|
|
|
|
entry.sprite.x = isoX;
|
|
|
|
|
entry.sprite.y = isoY;
|
|
|
|
|
entry.sprite.zIndex = isoY;
|
|
|
|
|
entry.sprite.visible = true;
|
|
|
|
|
} else {
|
|
|
|
|
if (obj === 'tree') this._drawTree(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'bush') this._drawBush(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'rock') this._drawRock(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'stump') this._drawStump(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'cart') this._drawBrokenCart(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'bones') this._drawBones(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'mushroom') this._drawMushroom(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'ruin') this._drawRuin(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'stall') this._drawMarketStall(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'well') this._drawWell(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'banner') this._drawBanner(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'barrel') this._drawBarrel(gfx, iso.x, iso.y, variant);
|
|
|
|
|
else if (obj === 'leaves') this._drawLeafPile(gfx, iso.x, iso.y, variant);
|
|
|
|
|
if (obj === 'tree') this._drawTree(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'bush') this._drawBush(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'rock') this._drawRock(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'stump') this._drawStump(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'cart') this._drawBrokenCart(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'bones') this._drawBones(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'mushroom') this._drawMushroom(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'ruin') this._drawRuin(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'stall') this._drawMarketStall(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'well') this._drawWell(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'banner') this._drawBanner(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'barrel') this._drawBarrel(gfx, isoX, isoY, variant);
|
|
|
|
|
else if (obj === 'leaves') this._drawLeafPile(gfx, isoX, isoY, variant);
|
|
|
|
|
}
|
|
|
|
|
isoX -= hw;
|
|
|
|
|
isoY += hh;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1578,6 +1609,28 @@ export class GameRenderer {
|
|
|
|
|
const gfx = this._townGfx;
|
|
|
|
|
const iconGfx = this._townIconGfx;
|
|
|
|
|
if (!gfx || !iconGfx) return;
|
|
|
|
|
const cx = camera.finalX;
|
|
|
|
|
const cy = camera.finalY;
|
|
|
|
|
if (this._lastTownsRef !== towns || this._lastTownCount !== towns.length) {
|
|
|
|
|
this._townDrawDirty = true;
|
|
|
|
|
this._lastTownsRef = towns;
|
|
|
|
|
this._lastTownCount = towns.length;
|
|
|
|
|
}
|
|
|
|
|
const now = performance.now();
|
|
|
|
|
const dx = Math.abs(cx - this._lastTownCameraX);
|
|
|
|
|
const dy = Math.abs(cy - this._lastTownCameraY);
|
|
|
|
|
if (
|
|
|
|
|
!this._townDrawDirty &&
|
|
|
|
|
now - this._lastTownDrawMs < this._townDrawIntervalMs &&
|
|
|
|
|
dx < TILE_WIDTH &&
|
|
|
|
|
dy < TILE_HEIGHT
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._townDrawDirty = false;
|
|
|
|
|
this._lastTownDrawMs = now;
|
|
|
|
|
this._lastTownCameraX = cx;
|
|
|
|
|
this._lastTownCameraY = cy;
|
|
|
|
|
gfx.clear();
|
|
|
|
|
iconGfx.clear();
|
|
|
|
|
|
|
|
|
|
@ -1586,14 +1639,13 @@ export class GameRenderer {
|
|
|
|
|
lbl.visible = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cx = camera.finalX;
|
|
|
|
|
const cy = camera.finalY;
|
|
|
|
|
const halfW = screenWidth / (2 * MAP_ZOOM) + TILE_WIDTH * 10;
|
|
|
|
|
const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 10;
|
|
|
|
|
|
|
|
|
|
let labelIdx = 0;
|
|
|
|
|
|
|
|
|
|
const usedBuildingSprites = new Set<string>();
|
|
|
|
|
const usedBuildingSprites = this._usedBuildingSprites;
|
|
|
|
|
usedBuildingSprites.clear();
|
|
|
|
|
for (const town of towns) {
|
|
|
|
|
// Convert town world position to screen space
|
|
|
|
|
const townScreen = worldToScreen(town.centerX, town.centerY);
|
|
|
|
|
@ -1720,7 +1772,8 @@ export class GameRenderer {
|
|
|
|
|
const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 3;
|
|
|
|
|
|
|
|
|
|
let labelIdx = 0;
|
|
|
|
|
const usedNpcSprites = new Set<string>();
|
|
|
|
|
const usedNpcSprites = this._usedNpcSprites;
|
|
|
|
|
usedNpcSprites.clear();
|
|
|
|
|
|
|
|
|
|
for (const npc of npcs) {
|
|
|
|
|
const iso = worldToScreen(npc.worldX, npc.worldY);
|
|
|
|
|
|