From 94b2f7f4572862b101e11187bb01e9bacb9de2fb Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Mon, 23 Mar 2026 19:47:51 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20Nisse=20silhouette=20outline?= =?UTF-8?q?=20for=20occlusion=20visibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #33. Each Nisse now has a white filled outline sprite at depth 900 that is always visible above trees and buildings. The main sprite uses Y-based depth (floor(y/TILE_SIZE)+5) so Nisse sort correctly with world objects. Name label, energy bar and job icon moved to depth 901/902 so they remain readable regardless of occlusion. Co-Authored-By: Claude Sonnet 4.6 --- src/systems/VillagerSystem.ts | 45 ++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/systems/VillagerSystem.ts b/src/systems/VillagerSystem.ts index 388331d..609d00d 100644 --- a/src/systems/VillagerSystem.ts +++ b/src/systems/VillagerSystem.ts @@ -15,6 +15,9 @@ const WORK_LOG_MAX = 20 interface VillagerRuntime { sprite: Phaser.GameObjects.Image + /** White silhouette sprite rendered above all world objects so the Nisse is + * always locatable even when occluded by trees or buildings. */ + outlineSprite: Phaser.GameObjects.Image nameLabel: Phaser.GameObjects.Text energyBar: Phaser.GameObjects.Graphics jobIcon: Phaser.GameObjects.Text @@ -118,8 +121,13 @@ export class VillagerSystem { case 'sleeping':this.tickSleeping(v, rt, delta); break } - // Sync sprite to state position - rt.sprite.setPosition(v.x, v.y) + // Sync sprite to state position; depth is Y-based so Nisse sort correctly with world objects + 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) + rt.nameLabel.setPosition(v.x, v.y - 22) rt.energyBar.setPosition(0, 0) this.drawEnergyBar(rt.energyBar, v.x, v.y, v.energy) @@ -569,21 +577,36 @@ export class VillagerSystem { * for a newly added Nisse. * @param v - Villager state to create sprites for */ + /** + * Creates and registers all runtime objects (sprite, outline, label, energy bar, icon) + * for a newly added Nisse. + * @param v - Villager state to create sprites for + */ private spawnSprite(v: VillagerState): void { - const sprite = this.scene.add.image(v.x, v.y, 'villager').setDepth(11) + // Silhouette rendered above all world objects so the Nisse is visible even + // when occluded by a tree or building. + const outlineSprite = this.scene.add.image(v.x, v.y, 'villager') + .setScale(1.3) + .setTintFill(0xffffff) + .setAlpha(0.7) + .setDepth(900) + + // Main sprite depth is updated every frame based on Y position. + const sprite = this.scene.add.image(v.x, v.y, 'villager') + .setDepth(Math.floor(v.y / TILE_SIZE) + 5) const nameLabel = this.scene.add.text(v.x, v.y - 22, v.name, { fontSize: '8px', color: '#ffffff', fontFamily: 'monospace', backgroundColor: '#00000088', padding: { x: 2, y: 1 }, - }).setOrigin(0.5, 1).setDepth(12) + }).setOrigin(0.5, 1).setDepth(901) - const energyBar = this.scene.add.graphics().setDepth(12) - const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(13) + const energyBar = this.scene.add.graphics().setDepth(901) + const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(902) sprite.setInteractive() sprite.on('pointerdown', () => this.onNisseClick?.(v.id)) - this.runtime.set(v.id, { sprite, nameLabel, energyBar, jobIcon, path: [], destination: null, workTimer: 0, idleScanTimer: 0, workLog: [] }) + this.runtime.set(v.id, { sprite, outlineSprite, nameLabel, energyBar, jobIcon, path: [], destination: null, workTimer: 0, idleScanTimer: 0, workLog: [] }) } /** @@ -667,14 +690,18 @@ export class VillagerSystem { return result } + /** + * Destroys all Nisse sprites and clears the runtime map. + * Should be called when the scene shuts down. + */ /** * Destroys all Nisse sprites and clears the runtime map. * Should be called when the scene shuts down. */ destroy(): void { for (const rt of this.runtime.values()) { - rt.sprite.destroy(); rt.nameLabel.destroy() - rt.energyBar.destroy(); rt.jobIcon.destroy() + rt.sprite.destroy(); rt.outlineSprite.destroy() + rt.nameLabel.destroy(); rt.energyBar.destroy(); rt.jobIcon.destroy() } this.runtime.clear() }