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.