✨ add Nisse silhouette outline for occlusion visibility
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 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@ const WORK_LOG_MAX = 20
|
|||||||
|
|
||||||
interface VillagerRuntime {
|
interface VillagerRuntime {
|
||||||
sprite: Phaser.GameObjects.Image
|
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
|
nameLabel: Phaser.GameObjects.Text
|
||||||
energyBar: Phaser.GameObjects.Graphics
|
energyBar: Phaser.GameObjects.Graphics
|
||||||
jobIcon: Phaser.GameObjects.Text
|
jobIcon: Phaser.GameObjects.Text
|
||||||
@@ -118,8 +121,13 @@ export class VillagerSystem {
|
|||||||
case 'sleeping':this.tickSleeping(v, rt, delta); break
|
case 'sleeping':this.tickSleeping(v, rt, delta); break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync sprite to state position
|
// Sync sprite to state position; depth is Y-based so Nisse sort correctly with world objects
|
||||||
rt.sprite.setPosition(v.x, v.y)
|
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.nameLabel.setPosition(v.x, v.y - 22)
|
||||||
rt.energyBar.setPosition(0, 0)
|
rt.energyBar.setPosition(0, 0)
|
||||||
this.drawEnergyBar(rt.energyBar, v.x, v.y, v.energy)
|
this.drawEnergyBar(rt.energyBar, v.x, v.y, v.energy)
|
||||||
@@ -569,21 +577,36 @@ export class VillagerSystem {
|
|||||||
* for a newly added Nisse.
|
* for a newly added Nisse.
|
||||||
* @param v - Villager state to create sprites for
|
* @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 {
|
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, {
|
const nameLabel = this.scene.add.text(v.x, v.y - 22, v.name, {
|
||||||
fontSize: '8px', color: '#ffffff', fontFamily: 'monospace',
|
fontSize: '8px', color: '#ffffff', fontFamily: 'monospace',
|
||||||
backgroundColor: '#00000088', padding: { x: 2, y: 1 },
|
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 energyBar = this.scene.add.graphics().setDepth(901)
|
||||||
const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(13)
|
const jobIcon = this.scene.add.text(v.x, v.y - 18, '', { fontSize: '10px' }).setDepth(902)
|
||||||
|
|
||||||
sprite.setInteractive()
|
sprite.setInteractive()
|
||||||
sprite.on('pointerdown', () => this.onNisseClick?.(v.id))
|
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
|
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.
|
* Destroys all Nisse sprites and clears the runtime map.
|
||||||
* Should be called when the scene shuts down.
|
* Should be called when the scene shuts down.
|
||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
for (const rt of this.runtime.values()) {
|
for (const rt of this.runtime.values()) {
|
||||||
rt.sprite.destroy(); rt.nameLabel.destroy()
|
rt.sprite.destroy(); rt.outlineSprite.destroy()
|
||||||
rt.energyBar.destroy(); rt.jobIcon.destroy()
|
rt.nameLabel.destroy(); rt.energyBar.destroy(); rt.jobIcon.destroy()
|
||||||
}
|
}
|
||||||
this.runtime.clear()
|
this.runtime.clear()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user