A browser client for the Encounter+ virtual tabletop app. Encounter+ serves this client to players' browsers, TVs and table displays. The client shows the current map, tokens, lighting and fog of war, the initiative order and the chat. Everything updates live over a WebSocket. Depending on how the host has set up the screen, players can also move their tokens, ping the map and send messages.
| Version | |
|---|---|
| Angular (CLI, core, build) | 22.1 |
| TypeScript | 6.0 |
| PixiJS | 8.21 |
| pixi-viewport | 6.0 |
| RxJS | 7.8 |
| Bootstrap / ng-bootstrap | 5.3 / 21 |
| Tests | Karma 6 + Jasmine 5 |
The app is zoneless: it uses provideZonelessChangeDetection(), loads no zone.js, and relies on signals for reactive UI state. It builds with the application builder (esbuild). Components are declared in AppModule (standalone: false).
npm ci
The scripts below call the Angular CLI through npm run, so you don't need a global ng.
npm start
This starts a dev server at http://localhost:4200/. Point it at a running Encounter+ instance with the remoteHost query parameter:
http://localhost:4200/?remoteHost=192.168.1.10:8080
| Parameter | Values | Effect |
|---|---|---|
remoteHost |
host:port |
Encounter+ server to connect to. Falls back to the last host that connected successfully, then to window.location.host. |
viewMode |
player (default), dm, spectator |
Controls which content is visible. |
runMode |
normal, tv |
tv hides the interactive chrome, such as the zoom controls. Defaults to tv when device is set. |
device |
e.g. gameboard |
Marks a dedicated display device. gameboard also applies a 1.5× viewport scale. |
interactions |
all |
Enables every interaction, whatever the screen settings say. |
User preferences (max FPS, video playback, name, selected token, open panel) are kept in localStorage.
src/app
├── app.component.* root component: connection, event dispatch, app-wide signals
├── app.module.ts declarations + zoneless / HttpClient providers
├── core/
│ ├── map/ PixiJS rendering (see below)
│ ├── toolbar/, zoombar/ on-screen controls
│ ├── initiative-list/, combatant/
│ ├── message-list/, message/
│ ├── overlay/, image-handout/, lightbox/, paused/
│ └── *-modal/ settings, about, entity (ng-bootstrap modals)
└── shared/
├── models/ data models mirroring the Encounter+ API (map, token, tile, light, wall, …)
├── services/ DataService (HTTP + WebSocket), ToastService
└── utils.ts
- Bootstrap.
AppComponentreads the query parameters intoAppStateand configuresDataServicewith the remote host. - Initial load.
DataService.getData()fetches the full state (ApiData) fromGET /api. - Live updates.
DataService.connect()opens a WebSocket to/wsthat reconnects automatically. IncomingWSEvents (WSEventNamelists them:mapLoaded,tokenMoved,fogUpdated,gameUpdated,messageCreated, …) go toAppComponent.handleEvent. That method updatesAppStateand the Angular signals (game,messages,screen,paused), then forwards map changes toMapComponent. - Outgoing events. Player actions, such as moving a token, pinging or sending a message, go back to the server as
WSEvents throughDataService.send.
The Angular UI (initiative, chat, toolbar, overlays) renders from signals. The map runs its own PixiJS ticker outside Angular change detection. In development builds, provideCheckNoChangesConfig({ exhaustive: true }) flags any Pixi callback that changes Angular-rendered state without going through a signal.
MapComponent creates the PIXI.Application and a pixi-viewport Viewport for pan and zoom. The viewport holds a MapContainer, which stacks one layer (layers/) for each kind of content, listed here from bottom to top:
mapLayer: background → bottom tiles → grid → middle tiles → top tiles → drawings
lights → paths → auras → area effects → measurements → markers
monsters → vision / fog → effects → players → overlay
- Layers (
layers/*-layer.ts) own a collection of models and manage their views. - Views (
views/*-view.ts) draw a single object: a token, tile, aura, marker, area effect, measurement or pointer.asset-artwork/asset-sprite/split-alpha-videohandle image and video assets. - Models (
models/) cover grid geometry (square-grid,hex-grid) and the shared assetLoader. - Renderers (
renderers/vision-renderer.ts) compute line of sight and fog from walls, lights and token sight. TrackedObjectsContainershows tracked objects (physical tokens and pointers that a touch-enabled table display reports).
npm run build # development build
npm run build:prod # production build → dist/web-client/browser
npm run package # production build + zip → dist/web-client.zip
dist/web-client.zip is the archive Encounter+ imports. node scripts/package.mjs packs an existing production build without rebuilding.
package.json owns the version; src/manifest.json follows it. Bump both and tag in one step, then push:
npm version 0.9.18
git push --follow-tags
The tag triggers the Release workflow (.github/workflows/release.yml). It builds with the production configuration and publishes web-client.zip and manifest.json as a GitHub release titled v0.9.18. A pre-release suffix (npm version 0.9.18-beta) publishes a pre-release; Encounter+ lists it but never offers it as an update. The workflow refuses to release when the tag, package.json and src/manifest.json disagree.
To rebuild an existing release, run the Release workflow manually with its tag. The assets are replaced.
npm test # Karma in watch mode
npm run test:ci # single run
Specs sit next to the code they cover (*.spec.ts). core/map/testing/ and shared/models/testing/ hold stubs and fixtures.