Fix Y-based depth sorting for world objects #32

Merged
tekki merged 5 commits from feature/depth-sorting into master 2026-03-23 20:07:28 +00:00
2 changed files with 42 additions and 7 deletions
Showing only changes of commit 5f646d54ca - Show all commits

View File

@@ -125,8 +125,11 @@ export class VillagerSystem {
const worldDepth = Math.floor(v.y / TILE_SIZE) + 5 const worldDepth = Math.floor(v.y / TILE_SIZE) + 5
rt.sprite.setPosition(v.x, v.y).setDepth(worldDepth) rt.sprite.setPosition(v.x, v.y).setDepth(worldDepth)
// Outline sprite mirrors position, flip, and angle so the silhouette matches exactly // Show outline only when a world object below the Nisse would occlude them
rt.outlineSprite.setPosition(v.x, v.y).setFlipX(rt.sprite.flipX).setAngle(rt.sprite.angle) const tileX = Math.floor(v.x / TILE_SIZE)
const tileY = Math.floor(v.y / TILE_SIZE)
const occluded = this.isOccluded(tileX, tileY)
rt.outlineSprite.setPosition(v.x, v.y).setFlipX(rt.sprite.flipX).setAngle(rt.sprite.angle).setVisible(occluded)
rt.nameLabel.setPosition(v.x, v.y - 22) rt.nameLabel.setPosition(v.x, v.y - 22)
rt.energyBar.setPosition(0, 0) rt.energyBar.setPosition(0, 0)
@@ -583,13 +586,14 @@ export class VillagerSystem {
* @param v - Villager state to create sprites for * @param v - Villager state to create sprites for
*/ */
private spawnSprite(v: VillagerState): void { private spawnSprite(v: VillagerState): void {
// Silhouette rendered above all world objects so the Nisse is visible even // Silhouette: same texture, white fill, fixed high depth so it shows through
// when occluded by a tree or building. // trees and buildings. Visibility is toggled per frame by isOccluded().
const outlineSprite = this.scene.add.image(v.x, v.y, 'villager') const outlineSprite = this.scene.add.image(v.x, v.y, 'villager')
.setScale(1.3) .setScale(1.1)
.setTintFill(0xffffff) .setTintFill(0xaaddff)
.setAlpha(0.7) .setAlpha(0.85)
.setDepth(900) .setDepth(900)
.setVisible(false)
// Main sprite depth is updated every frame based on Y position. // Main sprite depth is updated every frame based on Y position.
const sprite = this.scene.add.image(v.x, v.y, 'villager') const sprite = this.scene.add.image(v.x, v.y, 'villager')
@@ -640,6 +644,27 @@ export class VillagerSystem {
if (rt.workLog.length > WORK_LOG_MAX) rt.workLog.length = WORK_LOG_MAX if (rt.workLog.length > WORK_LOG_MAX) rt.workLog.length = WORK_LOG_MAX
} }
// ─── Occlusion check ──────────────────────────────────────────────────────
/**
* Returns true if a world object (tree, rock, or building) with a higher tileY
* than the Nisse exists on the same column, meaning the Nisse is visually
* behind that object. Checks 13 tiles below to account for tall tree canopies.
* @param tileX - Nisse's current tile column
* @param tileY - Nisse's current tile row
*/
private isOccluded(tileX: number, tileY: number): boolean {
const state = stateManager.getState()
for (let dy = 1; dy <= 3; dy++) {
const checkY = tileY + dy
if (this.worldSystem.hasResourceAt(tileX, checkY)) return true
if (Object.values(state.world.buildings).some(
b => b.tileX === tileX && b.tileY === checkY && b.kind !== 'stockpile_zone'
)) return true
}
return false
}
// ─── Public API ─────────────────────────────────────────────────────────── // ─── Public API ───────────────────────────────────────────────────────────
/** /**

View File

@@ -172,6 +172,16 @@ export class WorldSystem {
this.resourceTiles.delete(tileY * WORLD_TILES + tileX) this.resourceTiles.delete(tileY * WORLD_TILES + tileX)
} }
/**
* Returns true if a resource (tree or rock) occupies the given tile.
* Uses the O(1) resourceTiles index.
* @param tileX - Tile column
* @param tileY - Tile row
*/
hasResourceAt(tileX: number, tileY: number): boolean {
return this.resourceTiles.has(tileY * WORLD_TILES + tileX)
}
/** /**
* Converts world pixel coordinates to tile coordinates. * Converts world pixel coordinates to tile coordinates.
* @param worldX - World X in pixels * @param worldX - World X in pixels