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,236 @@
|
|
|
1
|
+
//! The game half of the init JSON (the engine half is
|
|
2
|
+
//! `vimp_engine_core::config`): actors, weapons, keys, panel. Both cores get
|
|
3
|
+
//! one string of the shape `{engine: {...}, game: {...}}` — `engine` is
|
|
4
|
+
//! parsed by the engine crate, `game` by the structs below.
|
|
5
|
+
//!
|
|
6
|
+
//! The unit of the fixed step differs between the halves **on purpose**, and
|
|
7
|
+
//! the field names say so: the host gets `timeStep` in SECONDS
|
|
8
|
+
//! (`EngineConfig`), the client gets `timeStepMs` in MILLISECONDS
|
|
9
|
+
//! (`EngineClientConfig`). Do not "fix" one of them.
|
|
10
|
+
|
|
11
|
+
use indexmap::IndexMap;
|
|
12
|
+
use serde::Deserialize;
|
|
13
|
+
|
|
14
|
+
/// One entry of `gameConfig.playerKeys`: the bit of the key and its kind —
|
|
15
|
+
/// `0` held, `1` one-shot (consumed by exactly one fixed step).
|
|
16
|
+
#[derive(Clone, Deserialize)]
|
|
17
|
+
#[serde(rename_all = "camelCase")]
|
|
18
|
+
pub struct KeyConfig {
|
|
19
|
+
pub key: u32,
|
|
20
|
+
#[serde(default, rename = "type")]
|
|
21
|
+
pub kind: u8,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Start value of a panel field (`gameConfig.panel.fields`).
|
|
25
|
+
#[derive(Clone, Deserialize)]
|
|
26
|
+
#[serde(rename_all = "camelCase")]
|
|
27
|
+
pub struct PanelValue {
|
|
28
|
+
pub value: f64,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Rapier collider parameters of the actor body.
|
|
32
|
+
#[derive(Clone, Deserialize)]
|
|
33
|
+
#[serde(rename_all = "camelCase")]
|
|
34
|
+
pub struct Fixture {
|
|
35
|
+
pub density: f32,
|
|
36
|
+
pub friction: f32,
|
|
37
|
+
pub restitution: f32,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// One actor class (`src/data/models.js`). The template ships a single class;
|
|
41
|
+
/// adding a second one means adding a key there — nothing in the core is
|
|
42
|
+
/// hard-coded to a name.
|
|
43
|
+
#[derive(Clone, Deserialize)]
|
|
44
|
+
#[serde(rename_all = "camelCase")]
|
|
45
|
+
pub struct ActorConfig {
|
|
46
|
+
/// Weapon the actor spawns with (key of `weapons`).
|
|
47
|
+
pub current_weapon: String,
|
|
48
|
+
/// Diameter of the body in world units.
|
|
49
|
+
pub size: f32,
|
|
50
|
+
/// Forward / reverse speed caps (units per second).
|
|
51
|
+
pub max_speed: f32,
|
|
52
|
+
pub max_reverse_speed: f32,
|
|
53
|
+
/// Speed gained per second while a drive key is held.
|
|
54
|
+
pub acceleration: f32,
|
|
55
|
+
/// Speed lost per second with no drive key held.
|
|
56
|
+
pub braking: f32,
|
|
57
|
+
/// Turn rate (radians per second).
|
|
58
|
+
pub turn_speed: f32,
|
|
59
|
+
pub fixture: Fixture,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Camera shake of the victim on a hit (`CoreEvent::Shake`).
|
|
63
|
+
#[derive(Clone, Deserialize)]
|
|
64
|
+
#[serde(rename_all = "camelCase")]
|
|
65
|
+
pub struct CameraShake {
|
|
66
|
+
pub intensity: f64,
|
|
67
|
+
pub duration: f64,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// One weapon (`src/data/weapons.js`). The template ships a single hitscan
|
|
71
|
+
/// weapon; a projectile weapon would add its own fields and a body kind in
|
|
72
|
+
/// `body_tag.rs`.
|
|
73
|
+
#[derive(Clone, Deserialize)]
|
|
74
|
+
#[serde(rename_all = "camelCase")]
|
|
75
|
+
pub struct WeaponConfig {
|
|
76
|
+
pub damage: f64,
|
|
77
|
+
/// Ray length in world units.
|
|
78
|
+
pub range: f32,
|
|
79
|
+
/// Cooldown between shots (SECONDS — the client predictor multiplies by
|
|
80
|
+
/// 1000 itself).
|
|
81
|
+
pub fire_rate: f32,
|
|
82
|
+
/// Random spread of the ray (radians, engine `Rng` only).
|
|
83
|
+
#[serde(default)]
|
|
84
|
+
pub spread: f32,
|
|
85
|
+
/// Ammo spent per shot (default 1).
|
|
86
|
+
#[serde(default)]
|
|
87
|
+
pub consumption: Option<f64>,
|
|
88
|
+
#[serde(default)]
|
|
89
|
+
pub camera_shake: Option<CameraShake>,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// The `game` half of the host init JSON (`GameCore::new`) — assembled by the
|
|
93
|
+
/// engine in `buildCoreConfig` from `HostPlugin.gameConfig`, so the field set
|
|
94
|
+
/// below is fixed by the engine, not by the game.
|
|
95
|
+
#[derive(Clone, Deserialize)]
|
|
96
|
+
#[serde(rename_all = "camelCase")]
|
|
97
|
+
pub struct ArenaConfig {
|
|
98
|
+
#[serde(default)]
|
|
99
|
+
pub friendly_fire: bool,
|
|
100
|
+
pub models: IndexMap<String, ActorConfig>,
|
|
101
|
+
pub weapons: IndexMap<String, WeaponConfig>,
|
|
102
|
+
pub player_keys: IndexMap<String, KeyConfig>,
|
|
103
|
+
/// Start values of the panel: `health` plus one field per weapon (ammo).
|
|
104
|
+
pub panel: IndexMap<String, PanelValue>,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl ArenaConfig {
|
|
108
|
+
/// Every panel key except `health` must name an existing weapon —
|
|
109
|
+
/// otherwise the panel and the ammo bookkeeping drift apart silently.
|
|
110
|
+
pub fn validate(&self) -> Result<(), String> {
|
|
111
|
+
for key in self.panel.keys() {
|
|
112
|
+
if key != "health" && !self.weapons.contains_key(key) {
|
|
113
|
+
return Err(format!(
|
|
114
|
+
"panel key '{key}' has no matching entry in weapons"
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (name, model) in &self.models {
|
|
120
|
+
if !self.weapons.contains_key(&model.current_weapon) {
|
|
121
|
+
return Err(format!(
|
|
122
|
+
"model '{name}': currentWeapon '{}' is not in weapons",
|
|
123
|
+
model.current_weapon
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Ok(())
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The `game` half of the client init JSON (`ClientCore::new`) — assembled by
|
|
133
|
+
/// the engine in `buildClientCoreConfig` from `CONFIG_DATA.prediction`.
|
|
134
|
+
#[derive(Clone, Deserialize)]
|
|
135
|
+
#[serde(rename_all = "camelCase")]
|
|
136
|
+
pub struct ArenaClientConfig {
|
|
137
|
+
pub models: IndexMap<String, ActorConfig>,
|
|
138
|
+
pub weapons: IndexMap<String, WeaponConfig>,
|
|
139
|
+
pub player_keys: IndexMap<String, KeyConfig>,
|
|
140
|
+
/// Seed of the local spread PRNG. It is deliberately NOT synchronised
|
|
141
|
+
/// with the host: the authoritative tracer arrives with the next frame
|
|
142
|
+
/// and replaces the local guess.
|
|
143
|
+
#[serde(default = "default_seed")]
|
|
144
|
+
pub seed: u64,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn default_seed() -> u64 {
|
|
148
|
+
0x5644_4d49_5056_494d
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Root init JSON of `GameCore::new`.
|
|
152
|
+
#[derive(Clone, Deserialize)]
|
|
153
|
+
pub struct RootConfig {
|
|
154
|
+
pub engine: vimp_engine_core::config::EngineConfig,
|
|
155
|
+
pub game: ArenaConfig,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Root init JSON of `ClientCore::new`.
|
|
159
|
+
#[derive(Clone, Deserialize)]
|
|
160
|
+
pub struct RootClientConfig {
|
|
161
|
+
pub engine: vimp_engine_core::config::EngineClientConfig,
|
|
162
|
+
pub game: ArenaClientConfig,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#[cfg(test)]
|
|
166
|
+
mod tests {
|
|
167
|
+
use super::*;
|
|
168
|
+
|
|
169
|
+
fn weapon() -> WeaponConfig {
|
|
170
|
+
WeaponConfig {
|
|
171
|
+
damage: 10.0,
|
|
172
|
+
range: 500.0,
|
|
173
|
+
fire_rate: 0.3,
|
|
174
|
+
spread: 0.0,
|
|
175
|
+
consumption: None,
|
|
176
|
+
camera_shake: None,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn model(current_weapon: &str) -> ActorConfig {
|
|
181
|
+
ActorConfig {
|
|
182
|
+
current_weapon: current_weapon.to_string(),
|
|
183
|
+
size: 32.0,
|
|
184
|
+
max_speed: 200.0,
|
|
185
|
+
max_reverse_speed: 100.0,
|
|
186
|
+
acceleration: 400.0,
|
|
187
|
+
braking: 600.0,
|
|
188
|
+
turn_speed: 3.0,
|
|
189
|
+
fixture: Fixture {
|
|
190
|
+
density: 1.0,
|
|
191
|
+
friction: 0.2,
|
|
192
|
+
restitution: 0.0,
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn config(panel_keys: &[&str], current_weapon: &str) -> ArenaConfig {
|
|
198
|
+
let mut weapons = IndexMap::new();
|
|
199
|
+
let mut models = IndexMap::new();
|
|
200
|
+
let mut panel = IndexMap::new();
|
|
201
|
+
|
|
202
|
+
weapons.insert("w1".to_string(), weapon());
|
|
203
|
+
models.insert("a1".to_string(), model(current_weapon));
|
|
204
|
+
|
|
205
|
+
for key in panel_keys {
|
|
206
|
+
panel.insert((*key).to_string(), PanelValue { value: 0.0 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
ArenaConfig {
|
|
210
|
+
friendly_fire: false,
|
|
211
|
+
models,
|
|
212
|
+
weapons,
|
|
213
|
+
player_keys: IndexMap::new(),
|
|
214
|
+
panel,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn panel_of_health_and_weapons_passes() {
|
|
220
|
+
assert!(config(&["health", "w1"], "w1").validate().is_ok());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn panel_key_without_weapon_fails() {
|
|
225
|
+
let err = config(&["health", "w9"], "w1").validate().unwrap_err();
|
|
226
|
+
|
|
227
|
+
assert!(err.contains("w9"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn unknown_current_weapon_fails() {
|
|
232
|
+
let err = config(&["health", "w1"], "w9").validate().unwrap_err();
|
|
233
|
+
|
|
234
|
+
assert!(err.contains("w9"));
|
|
235
|
+
}
|
|
236
|
+
}
|