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.
- package/bin/create-vimp-game.js +15 -0
- package/package.json +40 -0
- package/src/cli.js +227 -0
- package/src/generator.js +196 -0
- package/src/preflight.js +48 -0
- package/src/prompts.js +79 -0
- package/src/tokens.js +99 -0
- package/src/ui.js +66 -0
- package/src/versions.generated.json +4 -0
- package/src/versions.js +87 -0
- package/templates/default/CLAUDE.md.tpl +51 -0
- package/templates/default/Cargo.toml.tpl +27 -0
- package/templates/default/LICENSE.tpl +21 -0
- package/templates/default/README.md.tpl +61 -0
- package/templates/default/_gitignore +8 -0
- package/templates/default/assets/audio-raw/death.wav +0 -0
- package/templates/default/assets/audio-raw/shot.wav +0 -0
- package/templates/default/assets/sounds/death.mp3 +0 -0
- package/templates/default/assets/sounds/death.webm +0 -0
- package/templates/default/assets/sounds/shot.mp3 +0 -0
- package/templates/default/assets/sounds/shot.webm +0 -0
- package/templates/default/core/Cargo.toml.tpl +19 -0
- package/templates/default/core/src/actor.rs +407 -0
- package/templates/default/core/src/body_tag.rs +56 -0
- package/templates/default/core/src/client/mod.rs +385 -0
- package/templates/default/core/src/client/predictor.rs +552 -0
- package/templates/default/core/src/config.rs +236 -0
- package/templates/default/core/src/game.rs +692 -0
- package/templates/default/core/src/lib.rs +118 -0
- package/templates/default/core/src/motion.rs +126 -0
- package/templates/default/core/tests/sim.rs +351 -0
- package/templates/default/dev/main.js +28 -0
- package/templates/default/eslint.config.js +84 -0
- package/templates/default/index.html.tpl +35 -0
- package/templates/default/package.json.tpl +48 -0
- package/templates/default/scripts/build-game-manifest.js +188 -0
- package/templates/default/scripts/copy-game-images.js +35 -0
- package/templates/default/scripts/copy-game-sounds.js +55 -0
- package/templates/default/scripts/export-maps.js +19 -0
- package/templates/default/scripts/lib/rangeToPattern.js +74 -0
- package/templates/default/scripts/process-audio.js +115 -0
- package/templates/default/src/client/bakers/actorTexture.js +40 -0
- package/templates/default/src/client/bakers/index.js +8 -0
- package/templates/default/src/client/index.js +67 -0
- package/templates/default/src/client/parts/Actor.js +69 -0
- package/templates/default/src/client/parts/Map.js +74 -0
- package/templates/default/src/client/parts/ShotEffect.js +107 -0
- package/templates/default/src/client/parts/index.js +13 -0
- package/templates/default/src/client/style.css +26 -0
- package/templates/default/src/config/auth.js +61 -0
- package/templates/default/src/config/client.js +195 -0
- package/templates/default/src/config/game.js +170 -0
- package/templates/default/src/config/snapshot.js +70 -0
- package/templates/default/src/config/sounds.js +17 -0
- package/templates/default/src/data/maps/arena.js +69 -0
- package/templates/default/src/data/maps/index.js +7 -0
- package/templates/default/src/data/models.js +39 -0
- package/templates/default/src/data/weapons.js +37 -0
- package/templates/default/src/host/ScriptedManager.js +142 -0
- package/templates/default/src/host/createModules.js +12 -0
- package/templates/default/src/host/index.js +56 -0
- package/templates/default/src/host/nodeCore.js +23 -0
- package/templates/default/src/host/spawnCommand.js +32 -0
- package/templates/default/src/host/systemMessages.js +10 -0
- package/templates/default/tests/client/parts.test.js +128 -0
- package/templates/default/tests/config/contract.test.js +132 -0
- package/templates/default/tests/config/game.test.js +45 -0
- package/templates/default/tests/core/nodeCore.test.js +61 -0
- package/templates/default/tests/host/hostPlugin.test.js +198 -0
- package/templates/default/tests/stubs/wasmCore.js +19 -0
- package/templates/default/vite.config.js +74 -0
- 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
|
+
};
|