Mirage Source 6
Extending / Client UI & Effects

Adding a client UI panel or visual effect

The client side is Phaser 3 (game rendering) plus plain DOM/CSS overlays (HUD, chat, inventory, dialogs). Two extension points matter most: how a new packet type reaches your code, and how a new spell visual gets drawn.

Reacting to a new server packet

NetworkManager (packages/client/src/network/NetworkManager.ts) is the client's packet registry — it wraps the Colyseus room connection and fans every incoming message out by type:

import { NetworkManager } from '../network/NetworkManager.js';

NetworkManager.on('myCustomPacket', (payload) => {
  // handle it — update local state, emit a Phaser scene event, etc.
});

GameScene (packages/client/src/scenes/GameScene.ts) registers handlers for every built-in ServerPacket this way inside registerPacketHandlers() — most of them parse the payload and either update this.myData/this.mapData directly, or forward to the HUD scene via this.scene.get('HUD')?.events.emit(eventName, payload) for a DOM panel to pick up. Follow that same pattern for a new packet: add a ClientPacket/ServerPacket entry in packages/shared/src/packets/, send it from the server, and register a handler for it here.

Adding a DOM UI panel

Existing panels under packages/client/src/ui/ (ChatPanel.ts, InventoryPanel.ts, ShopDialog.ts, TradeDialog.ts, ...) are small classes that build their own DOM subtree, expose open()/toggle()/destroy(), and use the shared tokens in packages/client/src/ui/theme.ts for a consistent look. HUDScene (packages/client/src/scenes/HUDScene.ts) is what instantiates them and wires their open/close triggers to HUD events — add your panel the same way: instantiate it in HUDScene.create(), destroy it in HUDScene.shutdown(), and listen for whatever event should open it.

Adding a new spell visual effect

Spell visuals are driven by packages/shared/src/types/effects.ts's SPELL_EFFECTS table (an id → { color, style } lookup that a SpellRec.effect string key points into) and rendered by packages/client/src/rendering/SpellEffects.ts's playSpellEffect(), which switches on style (currently projectile/lightning/burst/ nova/ward). A brand-new visual style (not just a new color) means adding a case to that switch and a matching fxYourStyle(scene, ...) drawing function alongside the existing fxProjectile/fxLightning/etc. — this one file is not yet converted to a registry the way the server-side behaviors are, so it's the one place in the client that's still a switch you edit directly rather than a registration call.

// packages/client/src/rendering/SpellEffects.ts
function fxYourStyle(scene: Phaser.Scene, tx: number, ty: number, color: number) {
  // draw with scene.add.* / scene.tweens.add(...), same as the existing fx* functions
}

// in playSpellEffect()'s switch:
case 'yourStyle':
  fxYourStyle(scene, tx, ty, color);
  break;

Local movement & prediction

Movement prediction/reconciliation (the sliding-tile local movement, wall collision prediction, map-edge warp handling) lives inline in GameScene rather than a separate class. This is deliberate: it's genuinely fragile code (small timing changes here have visibly broken movement feel before), so it's kept in one file where the full picture — input handling, prediction, and the server-correction packet handlers that also touch the same state — is visible together rather than split across files in a way that could let them drift out of sync silently. If you extend it, test by actually walking around (map transitions, walking into an NPC, changing direction mid-step) — the server-side unit/smoke tests don't cover client-side movement feel at all.