create-vimp-game 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/bin/create-vimp-game.js +15 -0
  2. package/package.json +40 -0
  3. package/src/cli.js +227 -0
  4. package/src/generator.js +196 -0
  5. package/src/preflight.js +48 -0
  6. package/src/prompts.js +79 -0
  7. package/src/tokens.js +99 -0
  8. package/src/ui.js +66 -0
  9. package/src/versions.generated.json +4 -0
  10. package/src/versions.js +87 -0
  11. package/templates/default/CLAUDE.md.tpl +51 -0
  12. package/templates/default/Cargo.toml.tpl +27 -0
  13. package/templates/default/LICENSE.tpl +21 -0
  14. package/templates/default/README.md.tpl +61 -0
  15. package/templates/default/_gitignore +8 -0
  16. package/templates/default/assets/audio-raw/death.wav +0 -0
  17. package/templates/default/assets/audio-raw/shot.wav +0 -0
  18. package/templates/default/assets/sounds/death.mp3 +0 -0
  19. package/templates/default/assets/sounds/death.webm +0 -0
  20. package/templates/default/assets/sounds/shot.mp3 +0 -0
  21. package/templates/default/assets/sounds/shot.webm +0 -0
  22. package/templates/default/core/Cargo.toml.tpl +19 -0
  23. package/templates/default/core/src/actor.rs +407 -0
  24. package/templates/default/core/src/body_tag.rs +56 -0
  25. package/templates/default/core/src/client/mod.rs +385 -0
  26. package/templates/default/core/src/client/predictor.rs +552 -0
  27. package/templates/default/core/src/config.rs +236 -0
  28. package/templates/default/core/src/game.rs +692 -0
  29. package/templates/default/core/src/lib.rs +118 -0
  30. package/templates/default/core/src/motion.rs +126 -0
  31. package/templates/default/core/tests/sim.rs +351 -0
  32. package/templates/default/dev/main.js +28 -0
  33. package/templates/default/eslint.config.js +84 -0
  34. package/templates/default/index.html.tpl +35 -0
  35. package/templates/default/package.json.tpl +48 -0
  36. package/templates/default/scripts/build-game-manifest.js +188 -0
  37. package/templates/default/scripts/copy-game-images.js +35 -0
  38. package/templates/default/scripts/copy-game-sounds.js +55 -0
  39. package/templates/default/scripts/export-maps.js +19 -0
  40. package/templates/default/scripts/lib/rangeToPattern.js +74 -0
  41. package/templates/default/scripts/process-audio.js +115 -0
  42. package/templates/default/src/client/bakers/actorTexture.js +40 -0
  43. package/templates/default/src/client/bakers/index.js +8 -0
  44. package/templates/default/src/client/index.js +67 -0
  45. package/templates/default/src/client/parts/Actor.js +69 -0
  46. package/templates/default/src/client/parts/Map.js +74 -0
  47. package/templates/default/src/client/parts/ShotEffect.js +107 -0
  48. package/templates/default/src/client/parts/index.js +13 -0
  49. package/templates/default/src/client/style.css +26 -0
  50. package/templates/default/src/config/auth.js +61 -0
  51. package/templates/default/src/config/client.js +195 -0
  52. package/templates/default/src/config/game.js +170 -0
  53. package/templates/default/src/config/snapshot.js +70 -0
  54. package/templates/default/src/config/sounds.js +17 -0
  55. package/templates/default/src/data/maps/arena.js +69 -0
  56. package/templates/default/src/data/maps/index.js +7 -0
  57. package/templates/default/src/data/models.js +39 -0
  58. package/templates/default/src/data/weapons.js +37 -0
  59. package/templates/default/src/host/ScriptedManager.js +142 -0
  60. package/templates/default/src/host/createModules.js +12 -0
  61. package/templates/default/src/host/index.js +56 -0
  62. package/templates/default/src/host/nodeCore.js +23 -0
  63. package/templates/default/src/host/spawnCommand.js +32 -0
  64. package/templates/default/src/host/systemMessages.js +10 -0
  65. package/templates/default/tests/client/parts.test.js +128 -0
  66. package/templates/default/tests/config/contract.test.js +132 -0
  67. package/templates/default/tests/config/game.test.js +45 -0
  68. package/templates/default/tests/core/nodeCore.test.js +61 -0
  69. package/templates/default/tests/host/hostPlugin.test.js +198 -0
  70. package/templates/default/tests/stubs/wasmCore.js +19 -0
  71. package/templates/default/vite.config.js +74 -0
  72. package/templates/default/vitest.config.js +55 -0
@@ -0,0 +1,40 @@
1
+ import { Graphics, Rectangle } from 'pixi.js';
2
+
3
+ // Procedural texture of the actor: a disc with a muzzle wedge pointing along
4
+ // the +X axis (angle 0 of the core). Baked ONCE per canvas at startup, before
5
+ // any part exists — this is why the package ships no images.
6
+ //
7
+ // It is drawn WHITE and tinted per team in the part: one baked texture serves
8
+ // every colour variant and keeps the draw calls batched.
9
+ //
10
+ // A baker owns what it returns. The engine re-bakes on a WebGL context
11
+ // restore and destroys the previous result together with its TextureSource, so
12
+ // never return a view onto a shared atlas or a texture someone else holds.
13
+ //
14
+ // `params` comes from parts.bakedAssets ({ size, color }); `renderer` is the
15
+ // Pixi renderer of the canvas being baked.
16
+ export default function actorTexture(params, renderer) {
17
+ const { size, color } = params;
18
+ const radius = size / 2;
19
+ const graphics = new Graphics();
20
+
21
+ graphics.circle(radius, radius, radius);
22
+ graphics.fill(color);
23
+
24
+ // the muzzle: a wedge from the centre to the +X edge, so a standing actor
25
+ // still shows where it is aiming
26
+ graphics.moveTo(radius, radius - radius * 0.35);
27
+ graphics.lineTo(size, radius);
28
+ graphics.lineTo(radius, radius + radius * 0.35);
29
+ graphics.closePath();
30
+ graphics.fill({ color, alpha: 0.55 });
31
+
32
+ const texture = renderer.generateTexture({
33
+ target: graphics,
34
+ frame: new Rectangle(0, 0, size, size),
35
+ });
36
+
37
+ graphics.destroy(true);
38
+
39
+ return texture;
40
+ }
@@ -0,0 +1,8 @@
1
+ import actorTexture from './actorTexture.js';
2
+
3
+ // ClientPlugin.bakers: baker name -> function. The names are the ones
4
+ // parts.bakedAssets refers to (src/config/client.js); an entry naming a baker
5
+ // that is not here is skipped in silence, and the part gets an empty `assets`.
6
+ export default {
7
+ actorTexture,
8
+ };
@@ -0,0 +1,67 @@
1
+ import { ENGINE_API_VERSION } from 'vimp-engine/config/opcodes.js';
2
+ import styles from './style.css?inline';
3
+ import parts from './parts/index.js';
4
+ import bakers from './bakers/index.js';
5
+ import { isNodeCore, loadNodeCore, loadWebCore } from '../host/nodeCore.js';
6
+
7
+ // ClientPlugin — the render half, main thread: PixiJS parts, procedural
8
+ // textures and the three hooks into the client core. Default export of the
9
+ // client entry (vite build --mode client); the engine loads it by
10
+ // GameManifest.entries.client.
11
+ export default {
12
+ id: '{{GAME_ID}}',
13
+ engineApi: ENGINE_API_VERSION,
14
+
15
+ // MUST return { core, memory }: `memory` is the WebAssembly memory the
16
+ // engine reads the hot buffer out of every render tick. Without it the
17
+ // client silently renders nothing but the discrete frames.
18
+ async createClientCore(clientConfigJson, { wasmUrl } = {}) {
19
+ if (isNodeCore(wasmUrl)) {
20
+ const node = await loadNodeCore(wasmUrl);
21
+
22
+ // the Node build exposes no WASM memory: the headless client reads the
23
+ // hot buffer by copy (hot_values()) instead of through a view
24
+ return { core: new node.ClientCore(clientConfigJson), memory: null };
25
+ }
26
+
27
+ const { default: init, ClientCore } = await loadWebCore();
28
+ const wasm = await init({ module_or_path: wasmUrl });
29
+
30
+ return { core: new ClientCore(clientConfigJson), memory: wasm.memory };
31
+ },
32
+
33
+ parts,
34
+ bakers,
35
+
36
+ // CSS as a string — see src/client/style.css
37
+ styles,
38
+
39
+ // all three hooks are called unconditionally: an empty body is fine, a
40
+ // missing hook is a crash
41
+ hooks: {
42
+ // the model is known only after authorization, and the predictor cannot
43
+ // move an actor whose speed and turn rate it does not know
44
+ onAuth(core, authData) {
45
+ core.set_model(authData.model);
46
+ },
47
+
48
+ // the authoritative panel: the local shot is gated by ammo, and this is
49
+ // where the client core learns how much of it is left
50
+ onPanel(core, panelData) {
51
+ core.sync_panel(JSON.stringify(panelData));
52
+ },
53
+
54
+ // Local prediction of the shot: the tracer appears on the press instead
55
+ // of a round trip later, and the core drops the authoritative twin of it
56
+ // when the frame with it arrives. Returning null means "nothing to draw
57
+ // locally" — every gate (alive, cooldown, ammo) lives in the core, which
58
+ // mirrors the host: a guess the host would refuse is worse than no guess.
59
+ onLocalAction(core, action, name, now) {
60
+ if (action === 'down' && name === 'fire') {
61
+ return core.try_fire(now) ?? null;
62
+ }
63
+
64
+ return null;
65
+ },
66
+ },
67
+ };
@@ -0,0 +1,69 @@
1
+ import { Container, Sprite } from 'pixi.js';
2
+
3
+ // One actor on the main canvas. The engine builds it from the `a1` snapshot
4
+ // block (src/config/client.js -> parts.gameSets) and feeds it the field array
5
+ // of that block, in the order of src/config/snapshot.js:
6
+ //
7
+ // [x, y, angle, vx, vy, health, team]
8
+ //
9
+ // The very same layout also arrives from the local prediction (the predicted
10
+ // tail of the hot buffer), so this class never learns whether the row it got
11
+ // is authoritative or a guess — and must not care.
12
+ const FIELD = {
13
+ X: 0,
14
+ Y: 1,
15
+ ANGLE: 2,
16
+ HEALTH: 5,
17
+ TEAM: 6,
18
+ };
19
+
20
+ // white texture + tint: one baked asset serves both teams
21
+ const TEAM_TINT = {
22
+ 1: 0x4fa3ff,
23
+ 2: 0xff7a4f,
24
+ };
25
+
26
+ const NEUTRAL_TINT = 0xb0b0b0;
27
+
28
+ export default class Actor extends Container {
29
+ constructor(data, assets) {
30
+ super();
31
+
32
+ // Paint order is `zIndex` and nothing else: the engine marks the stage
33
+ // sortable and calls stage.sortChildren() on every addChild, and PixiJS v8
34
+ // sorts by zIndex there. A `layer` property alone does nothing at all.
35
+ this.zIndex = 3;
36
+
37
+ this._sprite = new Sprite(assets.actorTexture);
38
+ this._sprite.anchor.set(0.5);
39
+
40
+ this.addChild(this._sprite);
41
+
42
+ this.update(data);
43
+ }
44
+
45
+ update(data) {
46
+ this.x = data[FIELD.X] || 0;
47
+ this.y = data[FIELD.Y] || 0;
48
+ this._sprite.rotation = data[FIELD.ANGLE] || 0;
49
+
50
+ const team = data[FIELD.TEAM];
51
+
52
+ if (team !== this._team) {
53
+ this._team = team;
54
+ this._sprite.tint = TEAM_TINT[team] ?? NEUTRAL_TINT;
55
+ }
56
+
57
+ // A dead actor never arrives here: the core sends a null row for it and
58
+ // the engine destroys the instance. Health is kept anyway — the first
59
+ // thing a game grows is a bar over the head, and this is where it reads
60
+ // its value.
61
+ this._health = data[FIELD.HEALTH];
62
+ }
63
+
64
+ destroy() {
65
+ // `true` destroys the children as well; the baked texture is NOT ours to
66
+ // destroy — the engine re-uses it for every actor on this canvas
67
+ super.destroy({ children: true });
68
+ }
69
+ }
@@ -0,0 +1,74 @@
1
+ import { Container, Graphics } from 'pixi.js';
2
+
3
+ // The map, drawn procedurally. The engine splits MAP_DATA into one instance
4
+ // per render layer of `layers` (src/data/maps/arena.js) and hands each one:
5
+ //
6
+ // { type: 'static', map, step, layer, tiles, physicsStatic, spriteSheet,
7
+ // scale }
8
+ //
9
+ // `tiles` are the tile values THIS layer draws — a map with several layers
10
+ // gets several instances of this class, each drawing its own subset.
11
+ //
12
+ // A map with `physicsDynamic` also gets one instance per movable body, with
13
+ // `type: 'dynamic'`; the template has none, and the branch below says so
14
+ // rather than pretending the case cannot happen.
15
+ //
16
+ // Note what is NOT here: the step arrives ALREADY multiplied by the map
17
+ // scale, so scaling the container again would draw the world twice as large
18
+ // as the physics.
19
+ const WALL_COLOR = 0x33384a;
20
+ const WALL_EDGE = 0x4a5168;
21
+
22
+ export default class Map extends Container {
23
+ constructor(data) {
24
+ super();
25
+
26
+ // an ARRAY is a row of the `c1` snapshot block — a movable body of the
27
+ // map, live position included ([x, y, angle]). The template declares no
28
+ // physicsDynamic, so no such row is ever packed; the branch exists so that
29
+ // adding one is a drawing problem, not a crash inside the render tick.
30
+ if (Array.isArray(data)) {
31
+ this.zIndex = 2;
32
+
33
+ return;
34
+ }
35
+
36
+ // under the actors and the tracers
37
+ this.zIndex = Number(data.layer) || 1;
38
+
39
+ if (data.type === 'dynamic') {
40
+ // the map payload of a movable body (image, size, angle) — same story
41
+ return;
42
+ }
43
+
44
+ this._draw(data);
45
+ }
46
+
47
+ _draw({ map, step, tiles }) {
48
+ const drawn = new Set(tiles ?? []);
49
+ const graphics = new Graphics();
50
+
51
+ for (let row = 0; row < map.length; row += 1) {
52
+ for (let col = 0; col < map[row].length; col += 1) {
53
+ if (!drawn.has(map[row][col])) {
54
+ continue;
55
+ }
56
+
57
+ graphics.rect(col * step, row * step, step, step);
58
+ }
59
+ }
60
+
61
+ graphics.fill(WALL_COLOR);
62
+ graphics.stroke({ color: WALL_EDGE, width: 2, alignment: 0 });
63
+
64
+ this.addChild(graphics);
65
+ }
66
+
67
+ // the static map never changes between two MAP_DATA payloads: a new map
68
+ // arrives as a CLEAR of this setId followed by fresh instances
69
+ update() {}
70
+
71
+ destroy() {
72
+ super.destroy({ children: true });
73
+ }
74
+ }
@@ -0,0 +1,107 @@
1
+ import { Container, Graphics, Ticker } from 'pixi.js';
2
+
3
+ // The tracer of one shot. This part is an EFFECT, not an entity: the `e1`
4
+ // block is a list16, its payload is an ARRAY of rows, and for an array the
5
+ // engine creates one short-lived instance per row instead of an addressable
6
+ // object. An effect class implements `run()` on top of the usual contract —
7
+ // the engine calls it right after adding the instance to the stage — and is
8
+ // expected to destroy ITSELF when the animation ends.
9
+ //
10
+ // Row layout (src/config/snapshot.js -> e1):
11
+ //
12
+ // [startX, startY, endX, endY, wasHit, author]
13
+ //
14
+ // The local prediction draws its own tracer the moment the trigger is pressed
15
+ // and the client core then drops the authoritative twin of it by `author`, so
16
+ // a row for the local player normally reaches this class once — from the
17
+ // prediction, one round trip earlier.
18
+ const LIFETIME = 140;
19
+ const HIT_COLOR = 0xffd166;
20
+ const MISS_COLOR = 0x9fb3c8;
21
+
22
+ export default class ShotEffect extends Container {
23
+ constructor(data, assets, dependencies) {
24
+ super();
25
+
26
+ // above the map, below the actors
27
+ this.zIndex = 2;
28
+
29
+ this._startX = data[0];
30
+ this._startY = data[1];
31
+ this._endX = data[2];
32
+ this._endY = data[3];
33
+ this._wasHit = Boolean(data[4]);
34
+
35
+ this._soundManager = dependencies.soundManager;
36
+ this._soundId = null;
37
+ this._tick = null;
38
+ this._elapsed = 0;
39
+
40
+ this._line = new Graphics();
41
+ this.addChild(this._line);
42
+ }
43
+
44
+ run() {
45
+ this._line
46
+ .moveTo(this._startX, this._startY)
47
+ .lineTo(this._endX, this._endY)
48
+ .stroke({
49
+ color: this._wasHit ? HIT_COLOR : MISS_COLOR,
50
+ width: 2,
51
+ alpha: 1,
52
+ });
53
+
54
+ if (this._wasHit) {
55
+ this._line.circle(this._endX, this._endY, 4).fill(HIT_COLOR);
56
+ }
57
+
58
+ // spatial voice: the engine ranks voices by priority and distance and
59
+ // plays the loudest 30, so a busy match stays audible
60
+ this._soundId =
61
+ this._soundManager?.registerSound('shot', {
62
+ position: { x: this._startX, y: this._startY },
63
+ }) ?? null;
64
+
65
+ this._tick = ticker => this._update(ticker.deltaMS);
66
+ Ticker.shared.add(this._tick);
67
+ }
68
+
69
+ _update(deltaMS) {
70
+ this._elapsed += deltaMS;
71
+
72
+ const left = 1 - this._elapsed / LIFETIME;
73
+
74
+ if (left <= 0) {
75
+ this.destroy();
76
+
77
+ return;
78
+ }
79
+
80
+ this._line.alpha = left;
81
+ }
82
+
83
+ // an effect has no update path: every row is a new instance
84
+ update() {}
85
+
86
+ destroy() {
87
+ // the effect destroys itself from the ticker, so a second call is normal
88
+ // (the scene being torn down right after the flash expired)
89
+ if (this.destroyed) {
90
+ return;
91
+ }
92
+
93
+ if (this._tick) {
94
+ Ticker.shared.remove(this._tick);
95
+ this._tick = null;
96
+ }
97
+
98
+ // the flash is over long before the sample is: release, do not
99
+ // unregister — unregistering cuts the sound off mid-shot
100
+ if (this._soundId) {
101
+ this._soundManager.releaseSound(this._soundId);
102
+ this._soundId = null;
103
+ }
104
+
105
+ super.destroy({ children: true });
106
+ }
107
+ }
@@ -0,0 +1,13 @@
1
+ import Map from './Map.js';
2
+ import Actor from './Actor.js';
3
+ import ShotEffect from './ShotEffect.js';
4
+
5
+ // ClientPlugin.parts: class name -> class. The names are the ones
6
+ // parts.gameSets and parts.entitiesOnCanvas use (src/config/client.js) —
7
+ // all three lists must agree, and entitiesOnCanvas is the one that actually
8
+ // registers a class with the factory.
9
+ export default {
10
+ Map,
11
+ Actor,
12
+ ShotEffect,
13
+ };
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Styles of the game, imported with ?inline and handed to the engine as a
3
+ * string (ClientPlugin.styles) — the engine injects them into the document.
4
+ * The plugin build has no HTML entry, so a plain CSS import would be
5
+ * auto-injected by Vite into nothing.
6
+ *
7
+ * The engine ships the layout of #panel, #stat, #chat and #vote; everything
8
+ * here is only the look of the cells THIS game declares
9
+ * (src/config/client.js -> modules.panel.fields).
10
+ */
11
+
12
+ #panel {
13
+ gap: 16px;
14
+ font: 600 20px/1 system-ui, sans-serif;
15
+ color: #e6ecf5;
16
+ text-shadow: 0 1px 2px rgb(0 0 0 / 60%);
17
+ }
18
+
19
+ #panel-am::before {
20
+ content: '⌁ ';
21
+ opacity: 0.7;
22
+ }
23
+
24
+ #panel-time {
25
+ opacity: 0.8;
26
+ }
@@ -0,0 +1,61 @@
1
+ import models from '../data/models.js';
2
+
3
+ // HostPlugin.authSchema — the entry screen the engine renders before a player
4
+ // joins (docs/ai/04-client-plugin.md § Auth screen).
5
+ //
6
+ // Three traps live in this file, and each one has already cost a debugging
7
+ // session in a real game:
8
+ // * the container id is `fieldsId`, NOT `formId` — the engine resolves the
9
+ // wrong key to null and the screen dies with a TypeError on first render;
10
+ // * there is NO nickname field: identity comes from the lobby JWT;
11
+ // * the model field must be named exactly `model` — the engine reads
12
+ // `params.model` when it creates the participant, and any other name never
13
+ // reaches it.
14
+ export default {
15
+ elems: {
16
+ authId: 'auth',
17
+ fieldsId: 'auth-fields',
18
+ errorId: 'auth-error',
19
+ enterId: 'auth-enter',
20
+ titleId: 'auth-title',
21
+ informsId: 'auth-informs',
22
+ },
23
+
24
+ texts: {
25
+ title: '{{GAME_TITLE}}',
26
+ sections: [
27
+ {
28
+ heading: 'Controls',
29
+ lines: [
30
+ { keys: 'W, S', text: 'drive' },
31
+ { keys: 'A, D', text: 'turn' },
32
+ { keys: 'J', text: 'fire' },
33
+ { separator: true },
34
+ { keys: 'C', text: 'chat' },
35
+ { keys: 'M', text: 'vote' },
36
+ { keys: 'Tab', text: 'stats', last: true },
37
+ ],
38
+ },
39
+ ],
40
+ },
41
+
42
+ params: [
43
+ {
44
+ name: 'model',
45
+ value: 'a1',
46
+ options: {
47
+ control: 'select',
48
+ label: 'Model',
49
+ options: Object.keys(models),
50
+ validator: 'isValidModel',
51
+ storage: 'model',
52
+ },
53
+ },
54
+ ],
55
+
56
+ // validators are functions: they are not serialised to the client, they run
57
+ // on the host when the answer comes back
58
+ validators: {
59
+ isValidModel: model => model in models,
60
+ },
61
+ };
@@ -0,0 +1,195 @@
1
+ import sounds from './sounds.js';
2
+
3
+ // The game half of the client CONFIG_DATA: render entities, canvases, key
4
+ // sets, panel/stat schemas and the chat/vote texts. The engine deep-merges its
5
+ // own defaults under this object and adds `prediction` and `snapshot` itself —
6
+ // the host hands the result over through HostPlugin.buildClientGameConfig(),
7
+ // so the client never loads this file directly.
8
+ export default {
9
+ parts: {
10
+ // snapshot key (or map setId) -> the part classes built for it. A key
11
+ // without an entry is a black canvas: the frame arrives and the client
12
+ // does not know what to draw it with.
13
+ gameSets: {
14
+ a1: ['Actor'],
15
+ e1: ['ShotEffect'],
16
+ c1: ['Map'],
17
+ },
18
+
19
+ // part class -> canvas. This is the ONLY registration: a class listed in
20
+ // ClientPlugin.parts and in gameSets but missing here answers
21
+ // "Constructor for X not found." at the first frame that needs it.
22
+ entitiesOnCanvas: {
23
+ Map: 'vimp',
24
+ Actor: 'vimp',
25
+ ShotEffect: 'vimp',
26
+ },
27
+
28
+ // procedural textures, baked once per canvas at startup — the reason this
29
+ // package ships no images. `name` must exist in ClientPlugin.bakers and
30
+ // `component` names the part class that receives the result in `assets`.
31
+ bakedAssets: {
32
+ vimp: [
33
+ {
34
+ name: 'actorTexture',
35
+ component: 'Actor',
36
+ // white on purpose: one baked texture is tinted per team at runtime
37
+ params: { size: 32, color: 0xffffff },
38
+ },
39
+ ],
40
+ },
41
+
42
+ // the service pool has exactly three entries — renderer, soundManager,
43
+ // assetsBase. An unknown name is not an error: the part just gets
44
+ // undefined and draws nothing.
45
+ componentDependencies: {
46
+ soundManager: ['ShotEffect'],
47
+ },
48
+
49
+ sounds,
50
+ },
51
+
52
+ // DOM ids hidden until authentication completes; 'panel' is revealed as
53
+ // display: flex, everything else as display: block
54
+ initIdList: ['vimp', 'panel', 'chat'],
55
+
56
+ modules: {
57
+ canvasManager: {
58
+ // the engine CREATES these <canvas> elements — they are not in the HTML.
59
+ // baseScale '2:1' means world->screen 2 at the 1920 px design width, so
60
+ // the 768-unit arena is fully visible on a laptop.
61
+ canvases: {
62
+ vimp: {
63
+ width: 960,
64
+ height: 600,
65
+ aspectRatio: '16:10',
66
+ baseScale: '2:1',
67
+ dynamicCamera: true,
68
+ shakeCamera: true,
69
+ },
70
+ },
71
+ },
72
+
73
+ controls: {
74
+ // [0] spectator, [1] player. The engine switches between them by the
75
+ // KEYSET_DATA port; codes 9, 13, 27, 67 and 77 belong to the engine
76
+ // (stat, enter, escape, chat, vote) and never reach the game.
77
+ keySetList: [
78
+ {
79
+ 78: 'nextPlayer', // n
80
+ 80: 'prevPlayer', // p
81
+ },
82
+ // every action here must be a key of gameConfig.playerKeys, and vice
83
+ // versa: a name on one side only is a key that sends nothing
84
+ {
85
+ 87: 'forward', // w
86
+ 83: 'back', // s
87
+ 65: 'left', // a
88
+ 68: 'right', // d
89
+ 74: 'fire', // j
90
+ },
91
+ ],
92
+ },
93
+
94
+ chat: {
95
+ params: {
96
+ // texts of the system message codes: the host sends 'group:index',
97
+ // the text lives here. Groups s/v/m/c/n are the engine's — the game
98
+ // owns 'g' (see src/host/systemMessages.js).
99
+ messages: {
100
+ s: [
101
+ 'Team {0} is full. Your current team: {1}',
102
+ 'Your team: {0}',
103
+ 'Your new team: {0}',
104
+ 'Your new status: spectator',
105
+ '{0} killed {1}',
106
+ '{0} joined the game',
107
+ '{0} left the game',
108
+ ],
109
+ v: [
110
+ 'A vote has been created',
111
+ 'Voting has started',
112
+ 'Your vote has been accepted',
113
+ 'Voting is temporarily unavailable',
114
+ 'Vote passed',
115
+ 'Vote failed',
116
+ ],
117
+ m: ['Current map: {0}', 'Next map: {0}'],
118
+ c: ['Command not found', 'Your rank: {0}'],
119
+ n: ['Invalid name', '{0} changed name to {1}'],
120
+ g: ['{0} bot(s) spawned'],
121
+ },
122
+ },
123
+ },
124
+
125
+ panel: {
126
+ // wire key (gameConfig.panel.fields[*].key) -> field name here.
127
+ // 't' is sent by the engine itself and MUST map to a type: 'time'
128
+ // field, or the round time never appears on the HUD.
129
+ keys: {
130
+ h: 'hp',
131
+ a: 'am',
132
+ // gameConfig.panel.activeKey — the active weapon, written by the
133
+ // engine itself; a cell of type 'weapon' takes the .active class
134
+ wa: 'weapon',
135
+ t: 'time',
136
+ },
137
+ fields: [
138
+ { name: 'hp', elem: 'panel-hp', type: 'bar', max: 100, blocks: 20 },
139
+ { name: 'am', elem: 'panel-am', type: 'value' },
140
+ { name: 'weapon', elem: 'panel-weapon', type: 'weapon' },
141
+ { name: 'time', elem: 'panel-time', type: 'time' },
142
+ ],
143
+ },
144
+
145
+ stat: {
146
+ params: {
147
+ // five columns, positionally matched to the host's `key` indexes —
148
+ // the engine populates exactly these and its CSS is laid out for five
149
+ columns: ['names', 'status', 'score', 'deaths', 'latency'],
150
+ heads: {
151
+ 1: 'team1',
152
+ 2: 'team2',
153
+ },
154
+ bodies: {
155
+ 1: 'team1',
156
+ 2: 'team2',
157
+ 3: 'spectators',
158
+ },
159
+ // [columnIndex, descending]; sorting is numeric, a text column sorts
160
+ // as 0
161
+ sortList: {
162
+ team1: [
163
+ [2, true],
164
+ [3, false],
165
+ ],
166
+ team2: [
167
+ [2, true],
168
+ [3, false],
169
+ ],
170
+ },
171
+ },
172
+ },
173
+
174
+ vote: {
175
+ params: {
176
+ // template = [title, values?, timeOff?]; 'teams' and 'maps' are
177
+ // substituted by the engine with the live lists
178
+ templates: {
179
+ teamChange: ['Choose a team', 'teams', true],
180
+ mapChangeBySystem: ['Choose the next map'],
181
+ mapChangeByUser: ['{0} suggested the map: {1}', ['Yes', 'No']],
182
+ },
183
+ menu: [
184
+ ['teamChange', ['Switch team', 'teams']],
185
+ ['mapChange', ['Suggest map', 'maps']],
186
+ ],
187
+ },
188
+ },
189
+ },
190
+
191
+ // texts of the GAME_INFORM_DATA port, addressed by index
192
+ gameInform: {
193
+ list: ['{0} WINS!', 'ROUND START!', 'GAME OVER!'],
194
+ },
195
+ };