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,407 @@
|
|
|
1
|
+
//! The authoritative actor: a Rapier body, health, ammo, cooldowns and the
|
|
2
|
+
//! key bookkeeping. All movement math comes from `crate::motion` — this file
|
|
3
|
+
//! only decides WHEN to call it and writes the result onto the body.
|
|
4
|
+
|
|
5
|
+
use indexmap::IndexMap;
|
|
6
|
+
use rapier2d::prelude::*;
|
|
7
|
+
use serde::{Deserialize, Serialize};
|
|
8
|
+
|
|
9
|
+
use vimp_engine_core::config::PLAYER_STATE_LEN;
|
|
10
|
+
use vimp_engine_core::events::CoreEvent;
|
|
11
|
+
use vimp_engine_core::physics::{deg_to_rad, round2};
|
|
12
|
+
use vimp_engine_core::rng::Rng;
|
|
13
|
+
|
|
14
|
+
use crate::body_tag::BodyTag;
|
|
15
|
+
use crate::config::{ActorConfig, KeyConfig, PanelValue, WeaponConfig};
|
|
16
|
+
use crate::motion::{self, MoveInput};
|
|
17
|
+
|
|
18
|
+
/// Key bits resolved from `playerKeys` once, at construction: the engine
|
|
19
|
+
/// hands over the table but never interprets it — mapping names to bits and
|
|
20
|
+
/// honouring `type: 1` (one-shot) is the core's job.
|
|
21
|
+
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
|
|
22
|
+
pub struct PlayerKeyBits {
|
|
23
|
+
pub forward: u32,
|
|
24
|
+
pub back: u32,
|
|
25
|
+
pub left: u32,
|
|
26
|
+
pub right: u32,
|
|
27
|
+
pub fire: u32,
|
|
28
|
+
/// Bits of every `type: 1` key — consumed by exactly one fixed step.
|
|
29
|
+
pub one_shot_mask: u32,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl PlayerKeyBits {
|
|
33
|
+
pub fn from_config(keys: &IndexMap<String, KeyConfig>) -> Self {
|
|
34
|
+
let bit = |name: &str| keys.get(name).map(|k| k.key).unwrap_or(0);
|
|
35
|
+
let one_shot_mask = keys
|
|
36
|
+
.values()
|
|
37
|
+
.filter(|k| k.kind == 1)
|
|
38
|
+
.fold(0, |mask, k| mask | k.key);
|
|
39
|
+
|
|
40
|
+
Self {
|
|
41
|
+
forward: bit("forward"),
|
|
42
|
+
back: bit("back"),
|
|
43
|
+
left: bit("left"),
|
|
44
|
+
right: bit("right"),
|
|
45
|
+
fire: bit("fire"),
|
|
46
|
+
one_shot_mask,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A shot taken off the actor on this step — the ray the game then casts.
|
|
52
|
+
pub struct ShotCommand {
|
|
53
|
+
pub start: Vector,
|
|
54
|
+
pub direction: Vector,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// One participant of the match: player or bot (the bot only differs by who
|
|
58
|
+
/// presses its keys — see `on_ai_tick` in `game.rs`).
|
|
59
|
+
#[derive(Serialize, Deserialize)]
|
|
60
|
+
pub struct Actor {
|
|
61
|
+
pub game_id: u32,
|
|
62
|
+
pub team_id: u8,
|
|
63
|
+
pub model: String,
|
|
64
|
+
pub body: RigidBodyHandle,
|
|
65
|
+
|
|
66
|
+
// input
|
|
67
|
+
current_keys: u32,
|
|
68
|
+
one_shot_events: u32,
|
|
69
|
+
|
|
70
|
+
// movement state (the body carries position, these carry intent)
|
|
71
|
+
pub angle: f32,
|
|
72
|
+
pub speed: f32,
|
|
73
|
+
|
|
74
|
+
pub health: f64,
|
|
75
|
+
|
|
76
|
+
// per weapon index, in the order of `weapons` in the config
|
|
77
|
+
pub ammo: Vec<f64>,
|
|
78
|
+
cooldowns: Vec<f32>,
|
|
79
|
+
pub current_weapon: usize,
|
|
80
|
+
|
|
81
|
+
pub last_input_seq: u32,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl Actor {
|
|
85
|
+
#[allow(clippy::too_many_arguments)]
|
|
86
|
+
pub fn new(
|
|
87
|
+
world: &mut PhysicsWorld,
|
|
88
|
+
weapons: &IndexMap<String, WeaponConfig>,
|
|
89
|
+
panel: &IndexMap<String, PanelValue>,
|
|
90
|
+
model_name: &str,
|
|
91
|
+
model: &ActorConfig,
|
|
92
|
+
game_id: u32,
|
|
93
|
+
team_id: u8,
|
|
94
|
+
x: f32,
|
|
95
|
+
y: f32,
|
|
96
|
+
angle_deg: f32,
|
|
97
|
+
) -> Self {
|
|
98
|
+
let angle = deg_to_rad(angle_deg);
|
|
99
|
+
let tag = BodyTag::Player { game_id, team_id };
|
|
100
|
+
|
|
101
|
+
// lock_rotations: the facing angle is gameplay state written by
|
|
102
|
+
// `update`, not something the physics solver may spin
|
|
103
|
+
let body_handle = world.insert_body(
|
|
104
|
+
RigidBodyBuilder::dynamic()
|
|
105
|
+
.translation(Vector::new(x, y))
|
|
106
|
+
.rotation(angle)
|
|
107
|
+
.lock_rotations()
|
|
108
|
+
.user_data(tag.encode()),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
world.insert_collider(
|
|
112
|
+
ColliderBuilder::ball(model.size / 2.0)
|
|
113
|
+
.density(model.fixture.density)
|
|
114
|
+
.friction(model.fixture.friction)
|
|
115
|
+
.restitution(model.fixture.restitution),
|
|
116
|
+
Some(body_handle),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let current_weapon = weapons.get_index_of(&model.current_weapon).unwrap_or(0);
|
|
120
|
+
let health = panel.get("health").map(|p| p.value).unwrap_or(100.0);
|
|
121
|
+
let ammo = weapons
|
|
122
|
+
.keys()
|
|
123
|
+
.map(|name| panel.get(name).map(|p| p.value).unwrap_or(0.0))
|
|
124
|
+
.collect();
|
|
125
|
+
|
|
126
|
+
Self {
|
|
127
|
+
game_id,
|
|
128
|
+
team_id,
|
|
129
|
+
model: model_name.to_string(),
|
|
130
|
+
body: body_handle,
|
|
131
|
+
current_keys: 0,
|
|
132
|
+
one_shot_events: 0,
|
|
133
|
+
angle,
|
|
134
|
+
speed: 0.0,
|
|
135
|
+
health,
|
|
136
|
+
ammo,
|
|
137
|
+
cooldowns: vec![0.0; weapons.len()],
|
|
138
|
+
current_weapon,
|
|
139
|
+
last_input_seq: 0,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub fn is_alive(&self) -> bool {
|
|
144
|
+
self.health > 0.0
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Wire input: `down` sets the bit (one-shot keys go to their own mask),
|
|
148
|
+
/// `up` clears it. The client predictor repeats this rule verbatim.
|
|
149
|
+
pub fn update_keys(&mut self, action: &str, key_bit: u32, bits: &PlayerKeyBits) {
|
|
150
|
+
if key_bit == 0 {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if action == "down" {
|
|
155
|
+
if bits.one_shot_mask & key_bit != 0 {
|
|
156
|
+
self.one_shot_events |= key_bit;
|
|
157
|
+
} else {
|
|
158
|
+
self.current_keys |= key_bit;
|
|
159
|
+
}
|
|
160
|
+
} else if action == "up" {
|
|
161
|
+
self.current_keys &= !key_bit;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Held keys of a bot, replacing the whole mask at once.
|
|
166
|
+
pub fn set_held_keys(&mut self, keys: u32) {
|
|
167
|
+
self.current_keys = keys;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// One-shot press of a bot (fire).
|
|
171
|
+
pub fn press_once(&mut self, key_bit: u32) {
|
|
172
|
+
self.one_shot_events |= key_bit;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn reset_keys(&mut self) {
|
|
176
|
+
self.current_keys = 0;
|
|
177
|
+
self.one_shot_events = 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn keys_for_processing(&mut self) -> u32 {
|
|
181
|
+
let keys = self.current_keys | self.one_shot_events;
|
|
182
|
+
|
|
183
|
+
self.one_shot_events = 0;
|
|
184
|
+
keys
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Applies damage; `true` means this hit killed the actor.
|
|
188
|
+
pub fn take_damage(
|
|
189
|
+
&mut self,
|
|
190
|
+
amount: f64,
|
|
191
|
+
body: &mut RigidBody,
|
|
192
|
+
events: &mut Vec<CoreEvent>,
|
|
193
|
+
) -> bool {
|
|
194
|
+
if !self.is_alive() {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
self.health = (self.health - amount).max(0.0);
|
|
199
|
+
|
|
200
|
+
events.push(CoreEvent::PanelSet {
|
|
201
|
+
id: self.game_id,
|
|
202
|
+
field: "health".to_string(),
|
|
203
|
+
value: self.health,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if self.health <= 0.0 {
|
|
207
|
+
self.speed = 0.0;
|
|
208
|
+
body.set_linvel(Vector::ZERO, true);
|
|
209
|
+
// a corpse leaves the world until the respawn: disabling the body
|
|
210
|
+
// takes it out of the broad phase, so it neither blocks a ray nor
|
|
211
|
+
// collides — it only waits for `change_player_data`
|
|
212
|
+
body.set_enabled(false);
|
|
213
|
+
self.reset_keys();
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Cooldown + ammo gate of a shot; spends the ammo on success.
|
|
222
|
+
fn try_consume_ammo(
|
|
223
|
+
&mut self,
|
|
224
|
+
weapons: &IndexMap<String, WeaponConfig>,
|
|
225
|
+
events: &mut Vec<CoreEvent>,
|
|
226
|
+
) -> bool {
|
|
227
|
+
let (name, weapon) = weapons.get_index(self.current_weapon).unwrap();
|
|
228
|
+
let consumption = weapon.consumption.unwrap_or(1.0);
|
|
229
|
+
|
|
230
|
+
if self.cooldowns[self.current_weapon] > 0.0
|
|
231
|
+
|| self.ammo[self.current_weapon] < consumption
|
|
232
|
+
{
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
self.ammo[self.current_weapon] =
|
|
237
|
+
(self.ammo[self.current_weapon] - consumption).max(0.0);
|
|
238
|
+
self.cooldowns[self.current_weapon] = weapon.fire_rate;
|
|
239
|
+
|
|
240
|
+
events.push(CoreEvent::PanelSet {
|
|
241
|
+
id: self.game_id,
|
|
242
|
+
field: name.clone(),
|
|
243
|
+
value: self.ammo[self.current_weapon],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn update_cooldowns(&mut self, dt: f32) {
|
|
250
|
+
for cooldown in &mut self.cooldowns {
|
|
251
|
+
*cooldown = (*cooldown - dt).max(0.0);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// One fixed step of the actor. The order — turn, drive, write the body,
|
|
256
|
+
/// then fire — is what the client predictor replays, so it is a contract
|
|
257
|
+
/// of `mod parity`, not a style choice.
|
|
258
|
+
#[allow(clippy::too_many_arguments)]
|
|
259
|
+
pub fn update(
|
|
260
|
+
&mut self,
|
|
261
|
+
dt: f32,
|
|
262
|
+
body: &mut RigidBody,
|
|
263
|
+
model: &ActorConfig,
|
|
264
|
+
weapons: &IndexMap<String, WeaponConfig>,
|
|
265
|
+
bits: &PlayerKeyBits,
|
|
266
|
+
rng: &mut Rng,
|
|
267
|
+
events: &mut Vec<CoreEvent>,
|
|
268
|
+
) -> Option<ShotCommand> {
|
|
269
|
+
let keys = self.keys_for_processing();
|
|
270
|
+
|
|
271
|
+
let input = MoveInput {
|
|
272
|
+
forward: keys & bits.forward != 0,
|
|
273
|
+
back: keys & bits.back != 0,
|
|
274
|
+
left: keys & bits.left != 0,
|
|
275
|
+
right: keys & bits.right != 0,
|
|
276
|
+
};
|
|
277
|
+
let fire = keys & bits.fire != 0;
|
|
278
|
+
|
|
279
|
+
self.update_cooldowns(dt);
|
|
280
|
+
|
|
281
|
+
self.angle = motion::step_angle(self.angle, input, model, dt);
|
|
282
|
+
self.speed = motion::step_speed(self.speed, input, model, dt);
|
|
283
|
+
|
|
284
|
+
body.set_rotation(Rotation::from_angle(self.angle), true);
|
|
285
|
+
body.set_linvel(motion::velocity(self.angle, self.speed), true);
|
|
286
|
+
|
|
287
|
+
if !fire || !self.try_consume_ammo(weapons, events) {
|
|
288
|
+
return None;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let position = body.translation();
|
|
292
|
+
let weapon = &weapons[self.current_weapon];
|
|
293
|
+
let mut direction = self.angle;
|
|
294
|
+
|
|
295
|
+
// spread goes through the engine Rng only — `rand` or a clock would
|
|
296
|
+
// break the determinism the whole handoff/replay machinery rests on
|
|
297
|
+
if weapon.spread > 0.0 {
|
|
298
|
+
direction += rng.range(-weapon.spread, weapon.spread);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Some(ShotCommand {
|
|
302
|
+
start: motion::muzzle(position.x, position.y, self.angle, model),
|
|
303
|
+
direction: Vector::new(direction.cos(), direction.sin()),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Respawn / team change; health and ammo are reset separately by
|
|
308
|
+
/// `reset_vitals` (the engine drives both).
|
|
309
|
+
pub fn change_player_data(
|
|
310
|
+
&mut self,
|
|
311
|
+
team_id: u8,
|
|
312
|
+
x: f32,
|
|
313
|
+
y: f32,
|
|
314
|
+
angle_deg: f32,
|
|
315
|
+
body: &mut RigidBody,
|
|
316
|
+
) {
|
|
317
|
+
self.team_id = team_id;
|
|
318
|
+
body.user_data = BodyTag::Player {
|
|
319
|
+
game_id: self.game_id,
|
|
320
|
+
team_id,
|
|
321
|
+
}
|
|
322
|
+
.encode();
|
|
323
|
+
|
|
324
|
+
self.angle = deg_to_rad(angle_deg);
|
|
325
|
+
self.speed = 0.0;
|
|
326
|
+
|
|
327
|
+
body.set_enabled(true);
|
|
328
|
+
body.set_linvel(Vector::ZERO, true);
|
|
329
|
+
body.set_translation(Vector::new(x, y), true);
|
|
330
|
+
body.set_rotation(Rotation::from_angle(self.angle), true);
|
|
331
|
+
|
|
332
|
+
self.reset_keys();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Health and ammo back to the panel defaults. The body of a corpse is
|
|
336
|
+
/// woken up by `change_player_data` — the engine always respawns through
|
|
337
|
+
/// it, so this method never needs the world.
|
|
338
|
+
pub fn reset_vitals(
|
|
339
|
+
&mut self,
|
|
340
|
+
panel: &IndexMap<String, PanelValue>,
|
|
341
|
+
weapons: &IndexMap<String, WeaponConfig>,
|
|
342
|
+
events: &mut Vec<CoreEvent>,
|
|
343
|
+
) {
|
|
344
|
+
self.health = panel.get("health").map(|p| p.value).unwrap_or(100.0);
|
|
345
|
+
|
|
346
|
+
events.push(CoreEvent::PanelSet {
|
|
347
|
+
id: self.game_id,
|
|
348
|
+
field: "health".to_string(),
|
|
349
|
+
value: self.health,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
for (index, name) in weapons.keys().enumerate() {
|
|
353
|
+
self.ammo[index] = panel.get(name).map(|p| p.value).unwrap_or(0.0);
|
|
354
|
+
self.cooldowns[index] = 0.0;
|
|
355
|
+
|
|
356
|
+
events.push(CoreEvent::PanelSet {
|
|
357
|
+
id: self.game_id,
|
|
358
|
+
field: name.clone(),
|
|
359
|
+
value: self.ammo[index],
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// Snapshot row of the actor block: 5 floats + health + team. Floats are
|
|
365
|
+
/// rounded to 2 decimals because the client decoder rounds them anyway —
|
|
366
|
+
/// an unrounded value would come back as a different number.
|
|
367
|
+
pub fn snapshot_row(&self, body: &RigidBody) -> ([f32; 5], u8, u8) {
|
|
368
|
+
let pos = body.translation();
|
|
369
|
+
let vel = body.linvel();
|
|
370
|
+
|
|
371
|
+
(
|
|
372
|
+
[
|
|
373
|
+
round2(pos.x),
|
|
374
|
+
round2(pos.y),
|
|
375
|
+
round2(self.angle),
|
|
376
|
+
round2(vel.x),
|
|
377
|
+
round2(vel.y),
|
|
378
|
+
],
|
|
379
|
+
self.health.round() as u8,
|
|
380
|
+
self.team_id,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// The whole reconciliation channel: eight floats, unrounded (prediction
|
|
385
|
+
/// needs the precision). Layout — `[x, y, angle, vx, vy, hp, ammo, 0]`;
|
|
386
|
+
/// the client predictor and the `arena` client part read it by these
|
|
387
|
+
/// indices. The flag is `centering` in the engine's vocabulary: this game
|
|
388
|
+
/// has nothing to centre, so it is always `false`.
|
|
389
|
+
pub fn prediction_state(&self, body: &RigidBody) -> ([f32; PLAYER_STATE_LEN], bool) {
|
|
390
|
+
let pos = body.translation();
|
|
391
|
+
let vel = body.linvel();
|
|
392
|
+
|
|
393
|
+
(
|
|
394
|
+
[
|
|
395
|
+
pos.x,
|
|
396
|
+
pos.y,
|
|
397
|
+
self.angle,
|
|
398
|
+
vel.x,
|
|
399
|
+
vel.y,
|
|
400
|
+
self.health as f32,
|
|
401
|
+
self.ammo[self.current_weapon] as f32,
|
|
402
|
+
0.0,
|
|
403
|
+
],
|
|
404
|
+
false,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//! Tag of a Rapier body in `user_data` (u128): the kind lives in the low
|
|
2
|
+
//! byte, the payload above it. Low byte `1` is reserved by the engine
|
|
3
|
+
//! (`vimp_engine_core::physics::MAP_OBJECT_TAG`) — game kinds start at `2`.
|
|
4
|
+
|
|
5
|
+
/// Kinds of body this game creates. A projectile weapon would add
|
|
6
|
+
/// `Shot { shot_id, owner_id, team_id, weapon }` here with `TAG_SHOT = 3`.
|
|
7
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
8
|
+
pub enum BodyTag {
|
|
9
|
+
Player { game_id: u32, team_id: u8 },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TAG_PLAYER: u128 = 2;
|
|
13
|
+
|
|
14
|
+
impl BodyTag {
|
|
15
|
+
pub fn encode(self) -> u128 {
|
|
16
|
+
match self {
|
|
17
|
+
BodyTag::Player { game_id, team_id } => {
|
|
18
|
+
TAG_PLAYER | ((game_id as u128) << 8) | ((team_id as u128) << 40)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub fn decode(data: u128) -> Option<BodyTag> {
|
|
24
|
+
match data & 0xff {
|
|
25
|
+
TAG_PLAYER => Some(BodyTag::Player {
|
|
26
|
+
game_id: (data >> 8) as u32,
|
|
27
|
+
team_id: (data >> 40) as u8,
|
|
28
|
+
}),
|
|
29
|
+
_ => None,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[cfg(test)]
|
|
35
|
+
mod tests {
|
|
36
|
+
use super::*;
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn player_tag_round_trips() {
|
|
40
|
+
let tag = BodyTag::Player {
|
|
41
|
+
game_id: 250,
|
|
42
|
+
team_id: 2,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
assert_eq!(BodyTag::decode(tag.encode()), Some(tag));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn map_object_is_not_a_game_tag() {
|
|
50
|
+
assert_eq!(
|
|
51
|
+
BodyTag::decode(vimp_engine_core::physics::encode_map_object()),
|
|
52
|
+
None
|
|
53
|
+
);
|
|
54
|
+
assert_eq!(BodyTag::decode(0), None);
|
|
55
|
+
}
|
|
56
|
+
}
|