Skip to main content

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}