diff --git a/src/systems/VillagerSystem.ts b/src/systems/VillagerSystem.ts index 609d00d..b008257 100644 --- a/src/systems/VillagerSystem.ts +++ b/src/systems/VillagerSystem.ts @@ -125,8 +125,11 @@ export class VillagerSystem { const worldDepth = Math.floor(v.y / TILE_SIZE) + 5 rt.sprite.setPosition(v.x, v.y).setDepth(worldDepth) - // Outline sprite mirrors position, flip, and angle so the silhouette matches exactly - rt.outlineSprite.setPosition(v.x, v.y).setFlipX(rt.sprite.flipX).setAngle(rt.sprite.angle) + // Show outline only when a world object below the Nisse would occlude them + 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.energyBar.setPosition(0, 0) @@ -583,13 +586,14 @@ export class VillagerSystem { * @param v - Villager state to create sprites for */ private spawnSprite(v: VillagerState): void { - // Silhouette rendered above all world objects so the Nisse is visible even - // when occluded by a tree or building. + // Silhouette: same texture, white fill, fixed high depth so it shows through + // trees and buildings. Visibility is toggled per frame by isOccluded(). const outlineSprite = this.scene.add.image(v.x, v.y, 'villager') - .setScale(1.3) - .setTintFill(0xffffff) - .setAlpha(0.7) + .setScale(1.1) + .setTintFill(0xaaddff) + .setAlpha(0.85) .setDepth(900) + .setVisible(false) // Main sprite depth is updated every frame based on Y position. 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 } + // ─── 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 1–3 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 ─────────────────────────────────────────────────────────── /** diff --git a/src/systems/WorldSystem.ts b/src/systems/WorldSystem.ts index 6c33d1b..5de1ca6 100644 --- a/src/systems/WorldSystem.ts +++ b/src/systems/WorldSystem.ts @@ -172,6 +172,16 @@ export class WorldSystem { 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. * @param worldX - World X in pixels