17621fbd59
Replace the 16x16 tileset with LPC Base Assets (32x32 tiles, 1.5x scale). Uses a 4-layer rendering approach: - Layer 0: solid grass base (grass.png) - Layer 1: water transitions (watergrass.png) with LPC autotile edges - Layer 2: dirt transitions (dirt.png) with LPC autotile edges - Layer 3: tree canopy decorations (treetop.png) LPC autotile format: 3x6 grid with inner/outer corners, edges, and fill tiles. Edge tiles are selected based on 8-neighbor terrain analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
565 lines
20 KiB
TypeScript
565 lines
20 KiB
TypeScript
import Phaser from 'phaser';
|
|
import type { SocketClient } from '../network/SocketClient.js';
|
|
import { CharacterCompositor } from '../sprites/CharacterCompositor.js';
|
|
import { PortraitCompositor } from '../sprites/PortraitCompositor.js';
|
|
import { NpcInfoPanel } from '../ui/NpcInfoPanel.js';
|
|
import { CommandPanel } from '../ui/CommandPanel.js';
|
|
import { InteractionEmojiManager } from '../ui/InteractionEmoji.js';
|
|
import type { WorldState, StateUpdate, EntityState, Appearance } from '@dflike/shared';
|
|
import { TILE_SIZE, SPRITE_FRAME_WIDTH, SPRITE_FRAME_HEIGHT, SPRITE_COLS, Direction, Terrain, TILESET_TILE_SIZE, TILESET_SCALE } from '@dflike/shared';
|
|
|
|
interface EntitySprite {
|
|
sprite: Phaser.GameObjects.Sprite;
|
|
lastState: EntityState;
|
|
}
|
|
|
|
export class GameScene extends Phaser.Scene {
|
|
private client!: SocketClient;
|
|
private compositor!: CharacterCompositor;
|
|
private worldState!: WorldState;
|
|
private entitySprites: Map<number, EntitySprite> = new Map();
|
|
private creatingEntities: Set<number> = new Set();
|
|
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
|
|
private wasd!: Record<string, Phaser.Input.Keyboard.Key>;
|
|
private mode: 'avatar' | 'camera' | 'follow' = 'camera';
|
|
private modeText!: Phaser.GameObjects.Text;
|
|
private moveThrottle = 0;
|
|
private followTargetIndex = 0;
|
|
private followThrottle = 0;
|
|
private portraitCompositor!: PortraitCompositor;
|
|
private npcInfoPanel!: NpcInfoPanel;
|
|
private emojiManager!: InteractionEmojiManager;
|
|
private commandPanel!: CommandPanel;
|
|
private followCommandPanel!: CommandPanel;
|
|
private highlightEnabled = false;
|
|
private highlightedSpriteId: number | null = null;
|
|
|
|
constructor() {
|
|
super({ key: 'GameScene' });
|
|
}
|
|
|
|
init(data: { client: SocketClient; worldState: WorldState }): void {
|
|
this.client = data.client;
|
|
this.worldState = data.worldState;
|
|
}
|
|
|
|
create(): void {
|
|
this.compositor = new CharacterCompositor(this);
|
|
this.portraitCompositor = new PortraitCompositor();
|
|
this.npcInfoPanel = new NpcInfoPanel();
|
|
this.emojiManager = new InteractionEmojiManager();
|
|
this.commandPanel = new CommandPanel('command-panel', 'COMMANDS', [{ key: '1', label: 'Spawn NPC' }]);
|
|
this.commandPanel.show(); // Start in camera mode, so show immediately
|
|
this.followCommandPanel = new CommandPanel('follow-command-panel', 'FOLLOW', [
|
|
{ key: '\u2190 \u2192', label: 'Cycle NPC' },
|
|
{ key: '1', label: 'Highlight' },
|
|
{ key: 'TAB', label: 'Camera Mode' },
|
|
]);
|
|
|
|
// Draw tile grid
|
|
this.drawWorld();
|
|
|
|
// Setup camera
|
|
const worldPixelW = this.worldState.worldWidth * TILE_SIZE;
|
|
const worldPixelH = this.worldState.worldHeight * TILE_SIZE;
|
|
this.cameras.main.setBounds(0, 0, worldPixelW, worldPixelH);
|
|
|
|
// Input
|
|
this.cursors = this.input.keyboard!.createCursorKeys();
|
|
this.wasd = {
|
|
W: this.input.keyboard!.addKey('W'),
|
|
A: this.input.keyboard!.addKey('A'),
|
|
S: this.input.keyboard!.addKey('S'),
|
|
D: this.input.keyboard!.addKey('D'),
|
|
};
|
|
|
|
// Tab to toggle mode
|
|
this.input.keyboard!.addKey('TAB').on('down', () => {
|
|
const prevMode = this.mode;
|
|
if (this.mode === 'camera') {
|
|
this.mode = 'follow';
|
|
} else {
|
|
this.mode = 'camera';
|
|
}
|
|
this.updateModeText();
|
|
|
|
// Panel visibility
|
|
if (this.mode === 'follow') {
|
|
this.showFollowPanel();
|
|
this.commandPanel.hide();
|
|
this.followCommandPanel.show();
|
|
} else if (prevMode === 'follow') {
|
|
this.npcInfoPanel.hide();
|
|
this.followCommandPanel.hide();
|
|
this.clearHighlight();
|
|
this.highlightEnabled = false;
|
|
}
|
|
|
|
// Command panel visibility
|
|
if (this.mode === 'camera') {
|
|
this.commandPanel.show();
|
|
} else {
|
|
this.commandPanel.hide();
|
|
}
|
|
});
|
|
|
|
// Spawn NPC command (camera mode only)
|
|
this.input.keyboard!.addKey('ONE').on('down', () => {
|
|
if (this.mode === 'follow') {
|
|
this.highlightEnabled = !this.highlightEnabled;
|
|
if (this.highlightEnabled) {
|
|
const npcIds = this.getNpcIds();
|
|
const targetId = npcIds[this.followTargetIndex];
|
|
if (targetId != null) this.applyHighlight(targetId);
|
|
} else {
|
|
this.clearHighlight();
|
|
}
|
|
return;
|
|
}
|
|
if (this.mode !== 'camera') return;
|
|
const cam = this.cameras.main;
|
|
const centerTileX = Math.floor((cam.scrollX + cam.width / 2) / TILE_SIZE);
|
|
const centerTileY = Math.floor((cam.scrollY + cam.height / 2) / TILE_SIZE);
|
|
this.client.spawnNpc(centerTileX, centerTileY);
|
|
});
|
|
|
|
// UI text
|
|
this.modeText = this.add.text(10, 10, '', {
|
|
fontSize: '16px', color: '#ffffff', backgroundColor: '#000000aa',
|
|
padding: { x: 8, y: 4 },
|
|
}).setScrollFactor(0).setDepth(1000);
|
|
this.updateModeText();
|
|
|
|
// Spawn initial entities, then listen for state updates
|
|
this.spawnEntities(this.worldState.entities).then(() => {
|
|
this.client.onStateUpdate = (update: StateUpdate) => {
|
|
this.handleStateUpdate(update);
|
|
};
|
|
});
|
|
|
|
this.client.onPlayerLeft = (_data) => {
|
|
// Entity removal will be handled by next state update
|
|
};
|
|
}
|
|
|
|
private drawWorld(): void {
|
|
const { worldWidth, worldHeight, terrain, decorations, pointsOfInterest } = this.worldState;
|
|
|
|
// --- LPC autotile layout (3 cols x 6 rows per tileset, 32x32 tiles) ---
|
|
// Row 0: [inner-SE=0] [inner-SW=1] [??=2]
|
|
// Row 1: [inner-NE=3] [inner-NW=4] [??=5]
|
|
// Row 2: [outer-NW=6] [N-edge=7] [outer-NE=8]
|
|
// Row 3: [W-edge=9] [center=10] [E-edge=11]
|
|
// Row 4: [outer-SW=12][S-edge=13] [outer-SE=14]
|
|
// Row 5: [fill-1=15] [fill-2=16] [fill-3=17]
|
|
//
|
|
// "outer" = convex corner of the terrain island
|
|
// "inner" = concave notch (diagonal-only neighbor is the other terrain)
|
|
|
|
const FILL = 16; // solid fill tile (row 5, col 1)
|
|
|
|
const getTerr = (x: number, y: number): number => {
|
|
if (x < 0 || y < 0 || x >= worldWidth || y >= worldHeight) return Terrain.GRASS;
|
|
return terrain[y * worldWidth + x];
|
|
};
|
|
|
|
// For a GRASS tile, pick the watergrass/dirt transition tile based on
|
|
// which neighbors are the "other" terrain type.
|
|
// The LPC edge tiles are drawn FROM the grass perspective (grass is the terrain,
|
|
// water/dirt is the background bleeding through at edges).
|
|
const pickEdgeTile = (x: number, y: number, otherTerrain: number): number => {
|
|
const isOther = (dx: number, dy: number) => getTerr(x + dx, y + dy) === otherTerrain;
|
|
const n = isOther(0, -1), s = isOther(0, 1), w = isOther(-1, 0), e = isOther(1, 0);
|
|
const nw = isOther(-1, -1), ne = isOther(1, -1), sw = isOther(-1, 1), se = isOther(1, 1);
|
|
|
|
// Outer corners (two cardinal sides are the other terrain)
|
|
if (n && w) return 6; // outer-NW: grass only in SE, other terrain in NW
|
|
if (n && e) return 8; // outer-NE
|
|
if (s && w) return 12; // outer-SW
|
|
if (s && e) return 14; // outer-SE
|
|
// Edges (one cardinal side is the other terrain)
|
|
if (n) return 7; // N-edge: other terrain bleeds from north
|
|
if (s) return 13; // S-edge
|
|
if (w) return 9; // W-edge
|
|
if (e) return 11; // E-edge
|
|
// Inner corners (all cardinal = grass, but a diagonal is the other terrain)
|
|
if (se) return 0; // inner-SE: other terrain peeks in SE corner
|
|
if (sw) return 1; // inner-SW
|
|
if (ne) return 3; // inner-NE
|
|
if (nw) return 4; // inner-NW
|
|
// No adjacent other-terrain at all
|
|
return -1;
|
|
};
|
|
|
|
// Build tile data arrays for each layer
|
|
const grassData: number[][] = []; // base: solid grass everywhere
|
|
const waterData: number[][] = []; // watergrass transitions + solid water
|
|
const dirtData: number[][] = []; // dirt transitions + solid dirt
|
|
const decoData: number[][] = []; // tree decorations
|
|
|
|
for (let y = 0; y < worldHeight; y++) {
|
|
const grassRow: number[] = [];
|
|
const waterRow: number[] = [];
|
|
const dirtRow: number[] = [];
|
|
const decoRow: number[] = [];
|
|
|
|
for (let x = 0; x < worldWidth; x++) {
|
|
const t = terrain[y * worldWidth + x];
|
|
|
|
// Base: always solid grass
|
|
grassRow.push(FILL);
|
|
|
|
// Water layer
|
|
if (t === Terrain.WATER) {
|
|
waterRow.push(FILL); // solid water
|
|
} else if (t === Terrain.GRASS) {
|
|
waterRow.push(pickEdgeTile(x, y, Terrain.WATER));
|
|
} else {
|
|
waterRow.push(-1);
|
|
}
|
|
|
|
// Dirt layer
|
|
if (t === Terrain.DIRT) {
|
|
dirtRow.push(FILL); // solid dirt
|
|
} else if (t === Terrain.GRASS) {
|
|
dirtRow.push(pickEdgeTile(x, y, Terrain.DIRT));
|
|
} else {
|
|
dirtRow.push(-1);
|
|
}
|
|
|
|
// Decorations
|
|
const deco = decorations[y * worldWidth + x];
|
|
decoRow.push(deco >= 0 ? deco : -1);
|
|
}
|
|
|
|
grassData.push(grassRow);
|
|
waterData.push(waterRow);
|
|
dirtData.push(dirtRow);
|
|
decoData.push(decoRow);
|
|
}
|
|
|
|
// Helper to create a scaled tilemap layer
|
|
const createLayer = (
|
|
data: number[][],
|
|
tilesetName: string,
|
|
textureKey: string,
|
|
depth: number,
|
|
) => {
|
|
const tm = this.make.tilemap({
|
|
data,
|
|
tileWidth: TILESET_TILE_SIZE,
|
|
tileHeight: TILESET_TILE_SIZE,
|
|
});
|
|
const ts = tm.addTilesetImage(
|
|
tilesetName, textureKey,
|
|
TILESET_TILE_SIZE, TILESET_TILE_SIZE, 0, 0,
|
|
)!;
|
|
const layer = tm.createLayer(0, ts, 0, 0)!;
|
|
layer.setScale(TILESET_SCALE);
|
|
layer.setDepth(depth);
|
|
return layer;
|
|
};
|
|
|
|
createLayer(grassData, 'grass', 'lpc_grass', -4);
|
|
createLayer(waterData, 'water', 'lpc_watergrass', -3);
|
|
createLayer(dirtData, 'dirt', 'lpc_dirt', -2);
|
|
createLayer(decoData, 'trees', 'lpc_treetop', -1);
|
|
|
|
// Points of interest markers
|
|
const graphics = this.add.graphics();
|
|
graphics.setDepth(0);
|
|
for (const poi of pointsOfInterest) {
|
|
const color = poi.type === 'food' ? 0xcc8833 : 0x3355cc;
|
|
graphics.fillStyle(color, 0.7);
|
|
graphics.fillRect(
|
|
poi.position.x * TILE_SIZE + 8,
|
|
poi.position.y * TILE_SIZE + 8,
|
|
TILE_SIZE - 16,
|
|
TILE_SIZE - 16,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async spawnEntities(entities: EntityState[]): Promise<void> {
|
|
for (const entity of entities) {
|
|
if (!this.entitySprites.has(entity.id) && entity.appearance) {
|
|
await this.createEntitySprite(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async createEntitySprite(entity: EntityState): Promise<void> {
|
|
const textureKey = await this.compositor.preloadAppearance(entity.appearance);
|
|
|
|
// Create animations for this texture
|
|
this.createAnimations(textureKey);
|
|
|
|
const sprite = this.add.sprite(
|
|
entity.position.x * TILE_SIZE + TILE_SIZE / 2,
|
|
entity.position.y * TILE_SIZE + TILE_SIZE / 2,
|
|
textureKey,
|
|
0,
|
|
);
|
|
|
|
this.entitySprites.set(entity.id, { sprite, lastState: entity });
|
|
this.playAnimation(sprite, textureKey, entity.movement.direction, entity.movement.state);
|
|
}
|
|
|
|
private createAnimations(textureKey: string): void {
|
|
const dirNames = ['down', 'left', 'up', 'right'];
|
|
for (let dir = 0; dir < 4; dir++) {
|
|
const walkKey = `${textureKey}_walk_${dirNames[dir]}`;
|
|
const idleKey = `${textureKey}_idle_${dirNames[dir]}`;
|
|
|
|
if (!this.anims.exists(walkKey)) {
|
|
this.anims.create({
|
|
key: walkKey,
|
|
frames: this.anims.generateFrameNumbers(textureKey, {
|
|
start: dir * SPRITE_COLS + 1,
|
|
end: dir * SPRITE_COLS + SPRITE_COLS - 1,
|
|
}),
|
|
frameRate: 8,
|
|
repeat: -1,
|
|
});
|
|
}
|
|
if (!this.anims.exists(idleKey)) {
|
|
this.anims.create({
|
|
key: idleKey,
|
|
frames: [{ key: textureKey, frame: dir * SPRITE_COLS }],
|
|
frameRate: 1,
|
|
repeat: 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private playAnimation(sprite: Phaser.GameObjects.Sprite, textureKey: string, direction: number, state: string): void {
|
|
const dirNames = ['down', 'left', 'up', 'right'];
|
|
const dirName = dirNames[direction] ?? 'down';
|
|
const animKey = state === 'walking'
|
|
? `${textureKey}_walk_${dirName}`
|
|
: `${textureKey}_idle_${dirName}`;
|
|
if (sprite.anims.currentAnim?.key !== animKey) {
|
|
sprite.play(animKey);
|
|
}
|
|
}
|
|
|
|
private handleStateUpdate(update: StateUpdate): void {
|
|
const activeIds = new Set<number>();
|
|
|
|
for (const entity of update.entities) {
|
|
activeIds.add(entity.id);
|
|
const existing = this.entitySprites.get(entity.id);
|
|
|
|
if (existing) {
|
|
// Interpolation target
|
|
const targetX = entity.position.x * TILE_SIZE + TILE_SIZE / 2;
|
|
const targetY = entity.position.y * TILE_SIZE + TILE_SIZE / 2;
|
|
|
|
// Smooth movement via tween
|
|
const dx = Math.abs(existing.sprite.x - targetX);
|
|
const dy = Math.abs(existing.sprite.y - targetY);
|
|
if (dx > 1 || dy > 1) {
|
|
this.tweens.add({
|
|
targets: existing.sprite,
|
|
x: targetX,
|
|
y: targetY,
|
|
duration: 200,
|
|
ease: 'Linear',
|
|
});
|
|
}
|
|
|
|
// Update animation
|
|
const textureKey = this.compositor.getKey(entity.appearance);
|
|
this.playAnimation(existing.sprite, textureKey, entity.movement.direction, entity.movement.state);
|
|
existing.lastState = entity;
|
|
} else if (entity.appearance && !this.creatingEntities.has(entity.id)) {
|
|
this.creatingEntities.add(entity.id);
|
|
this.createEntitySprite(entity).finally(() => {
|
|
this.creatingEntities.delete(entity.id);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Remove entities no longer in update
|
|
for (const [id, es] of this.entitySprites) {
|
|
if (!activeIds.has(id)) {
|
|
es.sprite.destroy();
|
|
if (id === this.highlightedSpriteId) {
|
|
this.highlightedSpriteId = null;
|
|
}
|
|
this.entitySprites.delete(id);
|
|
}
|
|
}
|
|
|
|
// Update interaction emojis
|
|
const entityPositions = new Map<number, { screenX: number; screenY: number }>();
|
|
const cam = this.cameras.main;
|
|
for (const entity of update.entities) {
|
|
const es = this.entitySprites.get(entity.id);
|
|
if (es) {
|
|
const screenX = (es.sprite.x - cam.scrollX) * cam.zoom;
|
|
const screenY = (es.sprite.y - cam.scrollY) * cam.zoom;
|
|
entityPositions.set(entity.id, { screenX, screenY });
|
|
}
|
|
}
|
|
this.emojiManager.update(update.entities, cam, entityPositions);
|
|
|
|
// Live-update the follow panel if visible
|
|
if (this.mode === 'follow' && this.npcInfoPanel.isVisible()) {
|
|
const npcIds = this.getNpcIds();
|
|
const followedId = npcIds[this.followTargetIndex];
|
|
const followed = this.entitySprites.get(followedId);
|
|
if (followed) {
|
|
this.npcInfoPanel.updateInfo(followed.lastState);
|
|
}
|
|
}
|
|
}
|
|
|
|
update(_time: number, delta: number): void {
|
|
this.handleCameraAndMovement(delta);
|
|
}
|
|
|
|
private handleCameraAndMovement(delta: number): void {
|
|
const cam = this.cameras.main;
|
|
const speed = 6.25; // tiles per second for camera pan
|
|
|
|
const left = this.cursors.left?.isDown || this.wasd.A.isDown;
|
|
const right = this.cursors.right?.isDown || this.wasd.D.isDown;
|
|
const up = this.cursors.up?.isDown || this.wasd.W.isDown;
|
|
const down = this.cursors.down?.isDown || this.wasd.S.isDown;
|
|
|
|
if (this.mode === 'camera') {
|
|
// Pan camera freely
|
|
const panSpeed = speed * TILE_SIZE;
|
|
if (left) cam.scrollX -= panSpeed * delta / 1000;
|
|
if (right) cam.scrollX += panSpeed * delta / 1000;
|
|
if (up) cam.scrollY -= panSpeed * delta / 1000;
|
|
if (down) cam.scrollY += panSpeed * delta / 1000;
|
|
} else if (this.mode === 'avatar') {
|
|
// Avatar mode — send movement input (throttled)
|
|
this.moveThrottle -= delta;
|
|
if (this.moveThrottle <= 0) {
|
|
let dx = 0, dy = 0;
|
|
if (left) dx = -1;
|
|
else if (right) dx = 1;
|
|
else if (up) dy = -1;
|
|
else if (down) dy = 1;
|
|
|
|
if (dx !== 0 || dy !== 0) {
|
|
this.client.sendInput({ type: 'move', direction: { dx, dy } });
|
|
this.moveThrottle = 150;
|
|
}
|
|
}
|
|
|
|
// Follow own entity
|
|
const myEntityId = this.client.entityId;
|
|
if (myEntityId != null) {
|
|
const mySprite = this.entitySprites.get(myEntityId);
|
|
if (mySprite) {
|
|
cam.centerOn(mySprite.sprite.x, mySprite.sprite.y);
|
|
}
|
|
}
|
|
} else {
|
|
// Follow mode — track an NPC
|
|
this.followThrottle -= delta;
|
|
const npcIds = this.getNpcIds();
|
|
|
|
if (npcIds.length === 0) return;
|
|
|
|
// Clamp index to valid range
|
|
if (this.followTargetIndex >= npcIds.length) {
|
|
this.followTargetIndex = 0;
|
|
}
|
|
|
|
// Cycle NPCs with left/right (throttled)
|
|
if (this.followThrottle <= 0) {
|
|
if (left) {
|
|
this.followTargetIndex = (this.followTargetIndex - 1 + npcIds.length) % npcIds.length;
|
|
this.followThrottle = 150;
|
|
if (this.highlightEnabled) {
|
|
this.applyHighlight(npcIds[this.followTargetIndex]);
|
|
}
|
|
this.updateModeText();
|
|
this.updateFollowPanel();
|
|
} else if (right) {
|
|
this.followTargetIndex = (this.followTargetIndex + 1) % npcIds.length;
|
|
this.followThrottle = 150;
|
|
if (this.highlightEnabled) {
|
|
this.applyHighlight(npcIds[this.followTargetIndex]);
|
|
}
|
|
this.updateModeText();
|
|
this.updateFollowPanel();
|
|
}
|
|
}
|
|
|
|
// Center camera on followed NPC
|
|
const targetId = npcIds[this.followTargetIndex];
|
|
const targetSprite = this.entitySprites.get(targetId);
|
|
if (targetSprite) {
|
|
cam.centerOn(targetSprite.sprite.x, targetSprite.sprite.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async showFollowPanel(): Promise<void> {
|
|
const npcIds = this.getNpcIds();
|
|
if (npcIds.length === 0) return;
|
|
const targetId = npcIds[this.followTargetIndex];
|
|
const es = this.entitySprites.get(targetId);
|
|
if (!es) return;
|
|
|
|
const portraitUrl = await this.portraitCompositor.compositePortrait(es.lastState.appearance);
|
|
this.npcInfoPanel.show(es.lastState, portraitUrl);
|
|
}
|
|
|
|
private async updateFollowPanel(): Promise<void> {
|
|
const npcIds = this.getNpcIds();
|
|
if (npcIds.length === 0) return;
|
|
const targetId = npcIds[this.followTargetIndex];
|
|
const es = this.entitySprites.get(targetId);
|
|
if (!es) return;
|
|
|
|
const portraitUrl = await this.portraitCompositor.compositePortrait(es.lastState.appearance);
|
|
this.npcInfoPanel.updatePortrait(portraitUrl);
|
|
this.npcInfoPanel.updateInfo(es.lastState);
|
|
}
|
|
|
|
private getNpcIds(): number[] {
|
|
return [...this.entitySprites.entries()]
|
|
.filter(([, es]) => !es.lastState.playerControlled)
|
|
.map(([id]) => id)
|
|
.sort((a, b) => a - b);
|
|
}
|
|
|
|
private updateModeText(): void {
|
|
if (this.mode === 'follow') {
|
|
const npcIds = this.getNpcIds();
|
|
const targetId = npcIds[this.followTargetIndex];
|
|
const es = targetId != null ? this.entitySprites.get(targetId) : undefined;
|
|
const label = es?.lastState.name ?? (targetId != null ? `NPC #${targetId}` : 'none');
|
|
this.modeText.setText(`Mode: FOLLOW: ${label} [TAB to toggle]`);
|
|
} else {
|
|
this.modeText.setText(`Mode: ${this.mode.toUpperCase()} [TAB to toggle]`);
|
|
}
|
|
}
|
|
|
|
private applyHighlight(entityId: number): void {
|
|
const es = this.entitySprites.get(entityId);
|
|
if (!es) return;
|
|
this.clearHighlight();
|
|
es.sprite.postFX.addGlow(0xff0000, 2, 0, false, 0.1, 4);
|
|
this.highlightedSpriteId = entityId;
|
|
}
|
|
|
|
private clearHighlight(): void {
|
|
if (this.highlightedSpriteId != null) {
|
|
const es = this.entitySprites.get(this.highlightedSpriteId);
|
|
if (es) {
|
|
es.sprite.postFX.clear();
|
|
}
|
|
this.highlightedSpriteId = null;
|
|
}
|
|
}
|
|
}
|