rusty_sword_arena/game.rs
1use crate::{
2 gfx::{ButtonState, ButtonValue, Color, Vec2},
3 timer::Timer,
4 VERSION,
5};
6
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::collections::{hash_map::DefaultHasher, HashMap};
10use std::fmt;
11use std::hash::{Hash, Hasher};
12use std::time::Duration;
13
14/// Stateful, stack-based button processor. You can use this to process button state/values and
15/// update a `PlayerInput` that you can send to the server. Also handles the attack button.
16#[derive(Default)]
17pub struct ButtonProcessor {
18 horizontal: Vec<ButtonValue>,
19 vertical: Vec<ButtonValue>,
20}
21
22impl ButtonProcessor {
23 /// Create a new `ButtonProcessor`
24 pub fn new() -> Self {
25 Self {
26 horizontal: Vec::new(),
27 vertical: Vec::new(),
28 }
29 }
30 /// Process one button, and update the PlayerInput accordingly. Handles movement & attack.
31 pub fn process(
32 &mut self,
33 button_state: ButtonState,
34 button_value: ButtonValue,
35 player_input: &mut PlayerInput,
36 ) {
37 match button_state {
38 ButtonState::Pressed => match button_value {
39 ButtonValue::Up | ButtonValue::Down => self.vertical.push(button_value),
40 ButtonValue::Left | ButtonValue::Right => self.horizontal.push(button_value),
41 ButtonValue::Action1 => player_input.attack = true,
42 _ => (),
43 },
44 ButtonState::Released => match button_value {
45 ButtonValue::Up | ButtonValue::Down => self.vertical.retain(|&x| x != button_value),
46 ButtonValue::Left | ButtonValue::Right => {
47 self.horizontal.retain(|&x| x != button_value)
48 }
49 ButtonValue::Action1 => player_input.attack = false,
50 _ => (),
51 },
52 }
53 // Set horizontal movement based on the stack
54 if let Some(last_horiz) = self.horizontal.last() {
55 match last_horiz {
56 ButtonValue::Left => player_input.move_amount.x = -1.0,
57 ButtonValue::Right => player_input.move_amount.x = 1.0,
58 _ => {}
59 }
60 } else {
61 player_input.move_amount.x = 0.0;
62 }
63 // Set vertical movement based on the stack
64 if let Some(last_vert) = self.vertical.last() {
65 match last_vert {
66 ButtonValue::Up => player_input.move_amount.y = 1.0,
67 ButtonValue::Down => player_input.move_amount.y = -1.0,
68 _ => {}
69 }
70 } else {
71 player_input.move_amount.y = 0.0;
72 }
73 }
74}
75
76/// Convenience trait that adds an `.f32()` method that returns a 32-bit float representation of
77/// something. Implemented for `std::time::Duration` and `rusty_sword_arena::timer::Timer`.
78pub trait Floatable {
79 fn f32(&self) -> f32;
80}
81
82impl Floatable for Duration {
83 fn f32(&self) -> f32 {
84 self.as_secs() as f32 + self.subsec_nanos() as f32 * 1e-9
85 }
86}
87
88/// Various game control actions. Used by the networking module and the server.
89#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
90pub enum GameControlMsg {
91 Join { name: String },
92 Leave { id: u8 },
93 Fetch,
94}
95
96/// The game settings. Mostly useful if you want to try to write client-side animations that match
97/// server simulation, movement prediction, AI, etc.
98#[derive(Clone, Debug, Serialize, Deserialize)]
99pub struct GameSettings {
100 /// Version number of the server you are connecting to. Compare to `rusty_sword_arena::version`
101 pub version: String,
102 /// The maximum amount of players this server will allow
103 pub max_players: u8,
104 /// How quickly a player can get moving (magnitude in OpenGL units per second^2)
105 pub acceleration: f32,
106 /// Maximum velocity of a player (magnitude in OpenGL units per second)
107 pub max_velocity: f32,
108 /// How much drag there is on movement when the player is _not_ trying to move (how quick you
109 /// stop)
110 pub drag: f32,
111 /// Move threshold. Magnitude of Vec2 below which a move_speed will be considered 0.
112 pub move_threshold: f32,
113 /// Milliseconds. How long the server will wait to respawn a player who dies.
114 pub respawn_delay: u64,
115 /// Milliseconds. How long the server will allow not receiving input before dropping a player.
116 pub drop_delay: u64,
117}
118
119impl GameSettings {
120 /// Create the default game settings
121 pub fn new() -> Self {
122 GameSettings {
123 version: VERSION.to_string(),
124 max_players: 64,
125 acceleration: 1.5,
126 max_velocity: 0.25,
127 drag: 5.0,
128 move_threshold: 0.05,
129 respawn_delay: 5000,
130 drop_delay: 4000,
131 }
132 }
133 /// Looking forward to the day when game settings can be changed mid-game.
134 pub fn get_hash(&self) -> u64 {
135 let mut hasher = DefaultHasher::new();
136 self.hash(&mut hasher);
137 hasher.finish()
138 }
139}
140
141impl Hash for GameSettings {
142 fn hash<H: Hasher>(&self, state: &mut H) {
143 self.version.hash(state);
144 self.max_players.hash(state);
145 (self.acceleration as u32).hash(state);
146 (self.drag as u32).hash(state);
147 (self.move_threshold as u32).hash(state);
148 self.respawn_delay.hash(state);
149 self.drop_delay.hash(state);
150 }
151}
152
153impl Default for GameSettings {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159/// A single player's score
160#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
161pub struct Score {
162 name: String,
163 points: i32,
164}
165
166impl Score {
167 /// Create a new score
168 fn new(name: &str, points: i32) -> Self {
169 Self {
170 name: name.to_string(),
171 points,
172 }
173 }
174}
175
176impl fmt::Display for Score {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 write!(f, "{:<4.0} {}", self.points, self.name)
179 }
180}
181
182impl Eq for Score {}
183
184impl Ord for Score {
185 fn cmp(&self, other: &Score) -> Ordering {
186 if self.points < other.points {
187 Ordering::Less
188 } else if self.points == other.points {
189 if self.name < other.name {
190 Ordering::Greater
191 } else if self.name == other.name {
192 Ordering::Equal
193 } else {
194 Ordering::Less
195 }
196 } else {
197 Ordering::Greater
198 }
199 }
200}
201
202impl PartialOrd for Score {
203 fn partial_cmp(&self, other: &Score) -> Option<Ordering> {
204 Some(self.cmp(other))
205 }
206}
207
208/// High Scores! High scores are reset every time the server restarts, but other than that they
209/// are persistent across joins/leaves.
210#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
211pub struct HighScores {
212 pub scores: Vec<Score>,
213}
214
215impl HighScores {
216 /// Create a new, blank set of high scores
217 pub fn new() -> Self {
218 Self {
219 scores: Vec::<Score>::new(),
220 }
221 }
222 /// Bump a player's score up by one. If the player is not present, he will be added.
223 pub fn score(&mut self, name: &str) {
224 self.add_player(name);
225 if let Some(score) = self.scores.iter_mut().find(|x| x.name == name) {
226 score.points += 1;
227 }
228 self.sort();
229 }
230 /// Decrement a player's score by one. If the player is not present, he will be added.
231 pub fn penalize(&mut self, name: &str) {
232 self.add_player(name);
233 if let Some(score) = self.scores.iter_mut().find(|x| x.name == name) {
234 score.points -= 1;
235 }
236 self.sort();
237 }
238 /// Return a clone with only the top-10 scoring players. This is what the server sends the
239 /// client.
240 pub fn top10(&self) -> Self {
241 let mut top10 = self.clone();
242 while top10.scores.len() > 10 {
243 top10.scores.pop();
244 }
245 top10
246 }
247 /// Add a new player with a zero score. Used internally by both `score()` and `penalize()`
248 pub fn add_player(&mut self, name: &str) {
249 // Abort if we've already seen this player.
250 if self.scores.iter().any(|x| x.name == name) {
251 return;
252 }
253 self.scores.push(Score::new(name, 0));
254 self.sort();
255 }
256 // Sort the internal score vector in the direction we want.
257 fn sort(&mut self) {
258 self.scores.sort_by(|a, b| b.cmp(a));
259 }
260}
261
262impl fmt::Display for HighScores {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 let _ = write!(f, "-----------\nHigh Scores\n-----------");
265 for score in self.scores.iter() {
266 let _ = write!(f, "\n{}", score);
267 }
268 Ok(())
269 }
270}
271
272/// A player event that has happened to your player this frame! Note that it's possible to receive
273/// a whole bunch of events in the same frame.
274#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
275pub enum PlayerEvent {
276 /// Player has attacked and hit player id.
277 AttackHit { id: u8 },
278 /// Player has attacked, but not hit anyone.
279 AttackMiss,
280 /// Player has died
281 Die,
282 /// Player has spawned
283 Spawn,
284 /// Player has received damage
285 TookDamage,
286 /// Player has joined the game
287 Join,
288}
289
290/// A weapon a player may hold.
291#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
292pub struct Weapon {
293 /// Something like "Rusty Sword", "Shiny Sword", "Rusty Spear", etc.
294 pub description: String,
295 /// How much damage the weapon can cause
296 pub damage: f32,
297 /// How long until the player can attack again
298 pub attack_timer: Timer,
299 /// How far attacks reach from your player, in OpenGL units.
300 pub radius: f32,
301}
302
303impl Weapon {
304 pub fn new() -> Self {
305 Self {
306 description: "Rusty Sword".to_string(),
307 damage: 26.0,
308 radius: 0.1,
309 attack_timer: Timer::from_millis(500),
310 }
311 }
312}
313
314impl Default for Weapon {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320/// Represents the state of the player on the server for the current `GameState`. The server always
321/// creates `PlayeState`s, updates them, and sends them to the client each frame inside a
322/// `GameState`. The client is free to modify its local copy (for example, to remove `PlayerEvent`s
323/// it has processed) -- a new set of `PlayerState`s will be delivered the next frame. Clients
324/// typically look at the fields, drain and process the player events, but don't call any of the
325/// methods (since the methods are for server-side updates). You _could_ potentially do your own
326/// updates just for prediction or AI purposes, though.
327#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
328pub struct PlayerState {
329 /// The ID of the player
330 pub id: u8,
331 /// The name of the player
332 pub name: String,
333 /// The color of the player
334 pub color: Color,
335 /// The position of the player in OpenGL units.
336 pub pos: Vec2,
337 /// The direction the player is facing, in radians
338 pub direction: f32,
339 /// Your player occupies a circle of this radius, in OpenGL units.
340 pub radius: f32,
341 /// Current velocity of the player
342 pub velocity: Vec2,
343 /// Current health of the player [0.0, 100.0]
344 pub health: f32,
345 // Private! No docs for you. We use this when we respawn.
346 starting_health: f32,
347 /// Current weapon of the player
348 pub weapon: Weapon,
349 /// Any player events that have occurred to the player this frame
350 pub player_events: Vec<PlayerEvent>,
351 /// How long the server will wait to get input from you before disconnecting you
352 pub drop_timer: Timer,
353 /// How long until the player respawns. If respawn_timer.ready == false, then the player is
354 /// dead and you should seriously consider indicating that visually somehow, even if only by not
355 /// displaying the player.
356 pub respawn_timer: Timer,
357 /// Are you dead? Untangling health/respawn_timer dynamics is a pain, so we'll use this much
358 /// more convenient boolean.
359 pub dead: bool,
360 /// You start dead when you join...but maybe you want to treat the joining dead differently than
361 /// the properly-slain dead.
362 pub joining: bool,
363}
364
365impl PlayerState {
366 /// The client should never create a `PlayerState` -- the server will do that.
367 pub fn new(
368 game_settings: &GameSettings,
369 id: u8,
370 name: String,
371 color: Color,
372 pos: Vec2,
373 radius: f32,
374 ) -> Self {
375 let mut respawn_timer = Timer::from_millis(game_settings.respawn_delay);
376 respawn_timer.set_millis_transient(2000); // spawn more quickly on initial connect
377 Self {
378 id,
379 name,
380 color,
381 pos,
382 direction: 0.0,
383 radius,
384 velocity: Vec2::new(0., 0.),
385 health: 100.0,
386 starting_health: 100.0,
387 weapon: Weapon::new(),
388 player_events: vec![PlayerEvent::Join],
389 drop_timer: Timer::from_millis(game_settings.drop_delay),
390 respawn_timer,
391 dead: true,
392 joining: true,
393 }
394 }
395 /// Clients never need to call update. The server calls it each time it processes some amount of
396 /// time.
397 pub fn update(&mut self, delta: Duration) {
398 self.weapon.attack_timer.update(delta);
399 self.drop_timer.update(delta);
400 self.respawn_timer.update(delta);
401 }
402 /// Used by the server to reset things that have been taken care of last frame.
403 pub fn new_frame(&mut self) {
404 self.player_events.clear();
405 }
406 /// Used by the server when a player needs to die
407 pub fn die(&mut self, msg: &str) {
408 println!("{}", msg);
409 self.health = -1.0;
410 self.respawn_timer.reset();
411 self.player_events.push(PlayerEvent::Die);
412 self.dead = true;
413 }
414 /// Used by the server when a player needs to spawn
415 pub fn respawn(&mut self, pos: Vec2, msg: &str) {
416 println!("{}", msg);
417 self.pos = pos;
418 self.health = self.starting_health;
419 self.player_events.push(PlayerEvent::Spawn);
420 self.dead = false;
421 self.joining = false;
422 }
423}
424
425/// Once per frame, the server will broadcast a GameState to all clients. IMPORTANT: If you don't
426/// receive a GameState for 2 full seconds, the client MUST DROP its ServerConnection (or just
427/// exit entirely). The underlying networking that we're currently using hides network disconnects,
428/// which can leave the networking in a funky state if one end drops. So we need to rely on
429/// detecting this heartbeat and shutting down the clients to ensure networking is clean when the
430/// server restarts. (In the future, lets switch to a protocol that can detect disconnects...)
431#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
432pub struct GameState {
433 /// Which frame we're on. Starts at zero and increments by 1 each frame.
434 pub frame_number: u64,
435 /// The actual time the server measured since the previous frame.
436 pub delta: Duration,
437 /// The hash of the current game setting. Your client should store this somewhere. If it changes
438 /// then something has changed (most likely a player has joined or disconnected), so you should
439 /// get the new `GameSettings` from the server and update your client state. (Not actually a
440 /// concern yet since the server hasn't implemented changing the game state).
441 pub game_settings_hash: u64,
442 /// All of the current player's states, including your own! **NOTE:** The only reliable method
443 /// of knowing that a player is present in the game or not is whether or not a state is in
444 /// player_states. If there isn't a state, then the player has left or has been kicked/dropped.
445 /// If there is a state, then the player is in the game (and might have joined since the last
446 /// GameState was sent).
447 pub player_states: HashMap<u8, PlayerState>,
448 /// High scores. The server will only send the top 10.
449 pub high_scores: HighScores,
450}
451/// Clients should send `PlayerInput`s to the server often. The quicker the server gets inputs, the
452/// more accurate the simulation will be. But of course, you also shouldn't overload the server
453/// with too much traffic, because that's bad too. Good rule of thumb: Coalesce 15 milliseconds
454/// worth of input together, and send that. That's just faster than frames are sent by the
455/// server (60fps = 16.7ms). The server should be able to handle ~67 pkts/sec per client. I hope.
456///
457/// `PlayerInput` is used to collect input into and send it to the server. You
458/// can use the [angle_between](game/struct.Vec2.html#method.angle_between) method of a
459/// Vec2 to find the direction for the input based off of the position in your own player's
460/// [PlayerState](game/struct.PlayerState.html) and the current position of the mouse,
461/// which you get from one of the [game events](../gfx/struct.Window.html#method.poll_game_events)
462///
463/// Note that `attack` is an indicator of whether the player is currently (still) attempting to
464/// attack. The server will only attack once every once-in-awhile when the weapon's attack timer
465/// is ready, so you probably want to keep attack on while an attack button is held down, and
466/// then switch attack off when the attack button is released.
467///
468/// It's pretty safe to just overwrite move_amount and direction with the latest input you've got.
469/// Direction handling is instantaneous, and the move_amount is treated like a force (you move with
470/// a bit of inertia).
471#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
472pub struct PlayerInput {
473 /// The ID of your player
474 pub id: u8,
475 /// Whether you are attempting to attack (actual attack will occur if the server-side attack
476 /// timer has reached 0)
477 pub attack: bool,
478 /// How much your player is attempting to move horizontally (x) and vertically (y) [-1.0, 1.0].
479 /// Positive is right and up for x and y, respectively. You can derive movement amounts from
480 /// `Button` variants of the `GameEvent`s you get from the `Window.poll_game_events()`.
481 pub move_amount: Vec2,
482 /// What direction your player is facing. You can turn instantly, you lucky dog.
483 pub direction: f32,
484}
485
486impl Default for PlayerInput {
487 fn default() -> Self {
488 Self {
489 id: 0,
490 attack: false,
491 move_amount: Vec2::zeros(),
492 direction: 0.0,
493 }
494 }
495}
496
497impl PlayerInput {
498 /// Create a new PlayerInput
499 pub fn with_id(id: u8) -> Self {
500 Self {
501 id,
502 attack: false,
503 move_amount: Vec2::zeros(),
504 direction: 0.0,
505 }
506 }
507 /// Used by the server. Unlikely to be used by the client.
508 pub fn coalesce(&mut self, new: PlayerInput) {
509 // Any attack sticks
510 self.attack = self.attack || new.attack;
511 // Anything else the new value wins
512 self.move_amount = new.move_amount;
513 self.direction = new.direction;
514 }
515}