diff --git a/server-config/src/config.rs b/server-config/src/config.rs index d6bfb907..267d3cfb 100644 --- a/server-config/src/config.rs +++ b/server-config/src/config.rs @@ -7,8 +7,8 @@ use std::ptr::NonNull; use crate::util::{DropPtr, MaybeDrop}; use crate::{ - EffectPrototype, GameConfigCommon, GamePrototype, MissilePrototype, MobPrototype, PlanePrototype, - PtrRef, SpecialPrototype, StringRef, ValidationError, + GameConfigCommon, GamePrototype, MissilePrototype, MobPrototype, PlanePrototype, + PowerupPrototype, PtrRef, SpecialPrototype, StringRef, ValidationError, }; macro_rules! transform_protos { @@ -40,8 +40,8 @@ pub struct GameConfig { pub planes: HashMap<&'static str, &'static PlanePrototype<'static, PtrRef>>, pub missiles: HashMap<&'static str, &'static MissilePrototype>, pub specials: HashMap<&'static str, &'static SpecialPrototype<'static, PtrRef>>, - pub effects: HashMap<&'static str, &'static EffectPrototype>, - pub mobs: HashMap<&'static str, &'static MobPrototype>, + pub powerups: HashMap<&'static str, &'static PowerupPrototype>, + pub mobs: HashMap<&'static str, &'static MobPrototype<'static, PtrRef>>, pub common: GameConfigCommon<'static, PtrRef>, @@ -53,12 +53,12 @@ impl GameConfig { planes: &[PlanePrototype], missiles: &[MissilePrototype], specials: &[SpecialPrototype], - mobs: &[MobPrototype], - effects: &[EffectPrototype], + mobs: &[MobPrototype], + powerups: &[PowerupPrototype], common: GameConfigCommon, ) -> Result { - let data = unsafe { GameConfigData::new(&planes, &missiles, &specials, &mobs, &effects) }; + let data = unsafe { GameConfigData::new(&planes, &missiles, &specials, &mobs, &powerups) }; let mut missiles = HashMap::new(); let mut planes = HashMap::new(); @@ -66,7 +66,7 @@ impl GameConfig { let mut effects = HashMap::new(); let mut mobs = HashMap::new(); - for effect in data.effects() { + for effect in data.powerups() { if effects.insert(&*effect.name, effect).is_some() { return Err( ValidationError::custom("name", "multiple effect prototypes had the same name") @@ -120,7 +120,7 @@ impl GameConfig { missiles, planes, specials, - effects, + powerups: effects, mobs, common: common.resolve(data.planes())?, @@ -133,9 +133,9 @@ impl GameConfig { // These ones will be automatically dropped if something goes wrong. In order to // prevent UB we just need to call MaybeDrop::cancel_drop if everything works // out at the end. - let mobs = MaybeDrop::from(transform_protos!(proto.mobs => |m| m.resolve())?); let missiles = MaybeDrop::from(transform_protos!(proto.missiles => |m| m.resolve())?); - let effects = MaybeDrop::from(transform_protos!(proto.effects => |m| m.resolve())?); + let effects = MaybeDrop::from(transform_protos!(proto.powerups => |m| m.resolve())?); + let mobs = MaybeDrop::from(transform_protos!(proto.mobs => |m| m.resolve(&effects))?); let mut specials = transform_protos!(proto.specials => |s| s.resolve(&missiles))?; // Due to some lifetime issues it's not actually possible to store specials in @@ -213,8 +213,8 @@ struct GameConfigData { planes: NonNull<[PlanePrototype<'static, PtrRef>]>, missiles: NonNull<[MissilePrototype]>, specials: NonNull<[SpecialPrototype<'static, PtrRef>]>, - effects: NonNull<[EffectPrototype]>, - mobs: NonNull<[MobPrototype]>, + effects: NonNull<[PowerupPrototype]>, + mobs: NonNull<[MobPrototype<'static, PtrRef>]>, } impl GameConfigData { @@ -228,8 +228,8 @@ impl GameConfigData { planes: &[PlanePrototype], missiles: &[MissilePrototype], specials: &[SpecialPrototype], - mobs: &[MobPrototype], - effects: &[EffectPrototype], + mobs: &[MobPrototype], + effects: &[PowerupPrototype], ) -> Self { Self { planes: NonNull::new(planes as *const _ as *mut _).unwrap(), @@ -266,11 +266,11 @@ impl GameConfigData { unsafe { self.specials.as_ref() } } - fn mobs(&self) -> &'static [MobPrototype] { + fn mobs(&self) -> &'static [MobPrototype<'static, PtrRef>] { unsafe { self.mobs.as_ref() } } - fn effects(&self) -> &'static [EffectPrototype] { + fn powerups(&self) -> &'static [PowerupPrototype] { unsafe { self.effects.as_ref() } } } diff --git a/server-config/src/effect.rs b/server-config/src/effect.rs index 3fe55e24..7b437d89 100644 --- a/server-config/src/effect.rs +++ b/server-config/src/effect.rs @@ -1,145 +1,65 @@ -use std::borrow::Cow; -use std::time::Duration; - use serde::{Deserialize, Serialize}; -use crate::util::option_duration; -use crate::ValidationError; - #[derive(Clone, Debug, Serialize, Deserialize)] #[non_exhaustive] -pub struct EffectPrototype { - pub name: Cow<'static, str>, - - #[serde(with = "option_duration")] - pub duration: Option, - - #[serde(flatten)] - pub data: EffectPrototypeData, +#[serde(tag = "type", rename = "kebab-case")] +pub enum EffectPrototype { + Shield { + damage_mult: f32, + }, + Inferno, + FixedSpeed { + speed: f32, + }, + Upgrade, + /// Despawn the mob that just collided. + Despawn, } impl EffectPrototype { pub const fn shield() -> Self { - Self { - name: Cow::Borrowed("shield"), - duration: Some(Duration::from_secs(10)), - data: EffectPrototypeData::Shield(ShieldEffectPrototype { damage_mult: 0.0 }), - } - } - - pub const fn spawn_shield() -> Self { - Self { - name: Cow::Borrowed("spawn-shield"), - duration: Some(Duration::from_secs(2)), - data: EffectPrototypeData::Shield(ShieldEffectPrototype { damage_mult: 0.0 }), - } - } - - pub const fn invulnerable() -> Self { - Self { - name: Cow::Borrowed("invulnerable"), - duration: None, - data: EffectPrototypeData::Shield(ShieldEffectPrototype { damage_mult: 0.0 }), - } + Self::Shield { damage_mult: 0.0 } } pub const fn inferno() -> Self { - Self { - name: Cow::Borrowed("inferno"), - duration: Some(Duration::from_secs(10)), - data: EffectPrototypeData::Inferno, - } + Self::Inferno } pub const fn flag_speed() -> Self { - Self { - name: Cow::Borrowed("flag-speed"), - duration: None, - data: EffectPrototypeData::FixedSpeed(FixedSpeedEffectPrototype { speed: 5.0 }), - } + Self::FixedSpeed { speed: 5.0 } } pub const fn upgrade() -> Self { - Self { - name: Cow::Borrowed("upgrade"), - duration: None, - data: EffectPrototypeData::Upgrade, - } + Self::Upgrade + } + + pub const fn despawn() -> Self { + Self::Despawn } } impl EffectPrototype { pub const fn is_shield(&self) -> bool { - matches!(self.data, EffectPrototypeData::Shield(_)) - } - - pub const fn as_shield(&self) -> Option<&ShieldEffectPrototype> { - match &self.data { - EffectPrototypeData::Shield(shield) => Some(shield), - _ => None, - } + matches!(self, Self::Shield { .. }) } pub const fn is_inferno(&self) -> bool { - matches!(self.data, EffectPrototypeData::Inferno) + matches!(self, Self::Inferno) } pub const fn is_fixed_speed(&self) -> bool { - matches!(self.data, EffectPrototypeData::FixedSpeed(_)) - } - - pub const fn as_fixed_speed(&self) -> Option<&FixedSpeedEffectPrototype> { - match &self.data { - EffectPrototypeData::FixedSpeed(speed) => Some(speed), - _ => None, - } + matches!(self, Self::FixedSpeed { .. }) } pub const fn is_upgrade(&self) -> bool { - matches!(self.data, EffectPrototypeData::Upgrade) + matches!(self, Self::Upgrade) } -} - -impl EffectPrototype { - pub(crate) fn resolve(self) -> Result { - if self.name.is_empty() { - return Err(ValidationError::custom("name", "prototype had empty name")); - } - Ok(self) + pub const fn is_despawn(&self) -> bool { + matches!(self, Self::Despawn) } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[non_exhaustive] -#[serde(tag = "type", rename = "kebab-case")] -pub enum EffectPrototypeData { - Shield(ShieldEffectPrototype), - Inferno, - FixedSpeed(FixedSpeedEffectPrototype), - Upgrade, -} - -/// Effect that modifies the amount of damage done to a plane when it is hit by -/// a missile. -/// -/// This is named after the shield (which sets all damage to 0) but can be used -/// for any effect that needs to modify the damage taken by a plane. If multiple -/// instances of this effect are present the all the damage multipliers will be -/// applied. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[non_exhaustive] -pub struct ShieldEffectPrototype { - pub damage_mult: f32, -} -/// Effect that sets a plane's speed to a fixed value. -/// -/// This is used for the flag speed effect in CTF and will set the corresponding -/// bit in the keystate. Note that setting the speed to anything other than 5 -/// will cause client desyncs unless the client has been modified as well. -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[non_exhaustive] -pub struct FixedSpeedEffectPrototype { - pub speed: f32, + pub const fn is_instant(&self) -> bool { + matches!(self, Self::Upgrade | Self::Despawn) + } } diff --git a/server-config/src/game.rs b/server-config/src/game.rs index f0b56127..46448031 100644 --- a/server-config/src/game.rs +++ b/server-config/src/game.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; use crate::{ - EffectPrototype, GameConfigCommon, MissilePrototype, MobPrototype, PlanePrototype, PrototypeRef, + GameConfigCommon, MissilePrototype, MobPrototype, PlanePrototype, PowerupPrototype, PrototypeRef, SpecialPrototype, StringRef, }; @@ -14,14 +14,14 @@ use crate::{ serialize = " Ref::MissileRef: Serialize, Ref::SpecialRef: Serialize, - Ref::EffectRef: Serialize, + Ref::PowerupRef: Serialize, Ref::PlaneRef: Serialize, Ref::MobRef: Serialize, ", deserialize = " Ref::MissileRef: Deserialize<'de>, Ref::SpecialRef: Deserialize<'de>, - Ref::EffectRef: Deserialize<'de>, + Ref::PowerupRef: Deserialize<'de>, Ref::PlaneRef: Deserialize<'de>, Ref::MobRef: Deserialize<'de>, " @@ -30,8 +30,8 @@ pub struct GamePrototype<'a, Ref: PrototypeRef<'a>> { pub planes: Vec>, pub missiles: Vec, pub specials: Vec>, - pub mobs: Vec, - pub effects: Vec, + pub mobs: Vec>, + pub powerups: Vec, #[serde(flatten)] pub common: GameConfigCommon<'a, Ref>, @@ -67,13 +67,11 @@ impl Default for GamePrototype<'_, StringRef> { MobPrototype::shield(), MobPrototype::upgrade(), ], - effects: vec![ - EffectPrototype::shield(), - EffectPrototype::spawn_shield(), - EffectPrototype::invulnerable(), - EffectPrototype::inferno(), - EffectPrototype::flag_speed(), - EffectPrototype::upgrade(), + powerups: vec![ + PowerupPrototype::shield(), + PowerupPrototype::spawn_shield(), + PowerupPrototype::inferno(), + PowerupPrototype::upgrade(), ], common: GameConfigCommon::default(), } diff --git a/server-config/src/lib.rs b/server-config/src/lib.rs index 2f950e89..964d600c 100644 --- a/server-config/src/lib.rs +++ b/server-config/src/lib.rs @@ -8,6 +8,7 @@ mod game; mod missile; mod mob; mod plane; +mod powerup; mod special; mod util; @@ -19,12 +20,13 @@ use std::fmt::Debug; pub use self::common::GameConfigCommon; pub use self::config::GameConfig; -pub use self::effect::*; +pub use self::effect::EffectPrototype; pub use self::error::{Path, Segment, ValidationError}; pub use self::game::GamePrototype; pub use self::missile::MissilePrototype; pub use self::mob::MobPrototype; pub use self::plane::PlanePrototype; +pub use self::powerup::PowerupPrototype; pub use self::special::*; mod sealed { @@ -40,7 +42,7 @@ pub trait PrototypeRef<'a>: Sealed { type SpecialRef: Clone + Debug + 'a; type PlaneRef: Clone + Debug + 'a; type MobRef: Clone + Debug + 'a; - type EffectRef: Clone + Debug + 'a; + type PowerupRef: Clone + Debug + 'a; } #[derive(Copy, Clone, Debug)] @@ -55,7 +57,7 @@ impl Sealed for PtrRef {} impl<'a> PrototypeRef<'a> for StringRef { type MissileRef = Cow<'a, str>; type SpecialRef = Cow<'a, str>; - type EffectRef = Cow<'a, str>; + type PowerupRef = Cow<'a, str>; type PlaneRef = Cow<'a, str>; type MobRef = Cow<'a, str>; } @@ -63,7 +65,7 @@ impl<'a> PrototypeRef<'a> for StringRef { impl<'a> PrototypeRef<'a> for PtrRef { type MissileRef = &'a MissilePrototype; type SpecialRef = &'a SpecialPrototype<'a, Self>; - type EffectRef = &'a EffectPrototype; + type PowerupRef = &'a PowerupPrototype; type PlaneRef = &'a PlanePrototype<'a, Self>; - type MobRef = &'a MobPrototype; + type MobRef = &'a MobPrototype<'a, Self>; } diff --git a/server-config/src/mob.rs b/server-config/src/mob.rs index 0fd29ae2..118e4b8e 100644 --- a/server-config/src/mob.rs +++ b/server-config/src/mob.rs @@ -4,12 +4,29 @@ use std::time::Duration; use protocol::MobType; use serde::{Deserialize, Serialize}; +use crate::powerup::PowerupPrototype; use crate::util::duration; -use crate::ValidationError; +use crate::{PrototypeRef, PtrRef, StringRef, ValidationError}; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct MobPrototype { +#[serde(bound( + serialize = " + Ref::MissileRef: Serialize, + Ref::SpecialRef: Serialize, + Ref::PowerupRef: Serialize, + Ref::PlaneRef: Serialize, + Ref::MobRef: Serialize, + ", + deserialize = " + Ref::MissileRef: Deserialize<'de>, + Ref::SpecialRef: Deserialize<'de>, + Ref::PowerupRef: Deserialize<'de>, + Ref::PlaneRef: Deserialize<'de>, + Ref::MobRef: Deserialize<'de>, + " +))] +pub struct MobPrototype<'a, Ref: PrototypeRef<'a>> { /// The name that will be used to this mob. pub name: Cow<'static, str>, @@ -22,40 +39,65 @@ pub struct MobPrototype { /// How long this mob will stick around before despawning. #[serde(with = "duration")] pub lifetime: Duration, + + /// The effects of colliding with this mob. + pub powerup: Ref::PowerupRef, } -impl MobPrototype { - pub const fn inferno() -> Self { +impl MobPrototype<'_, StringRef> { + pub fn inferno() -> Self { Self { name: Cow::Borrowed("inferno"), server_type: MobType::Inferno, lifetime: Duration::from_secs(60), + powerup: Cow::Borrowed("inferno"), } } - pub const fn shield() -> Self { + pub fn shield() -> Self { Self { name: Cow::Borrowed("shield"), server_type: MobType::Shield, lifetime: Duration::from_secs(60), + powerup: Cow::Borrowed("shield"), } } - pub const fn upgrade() -> Self { + pub fn upgrade() -> Self { Self { name: Cow::Borrowed("upgrade"), server_type: MobType::Upgrade, lifetime: Duration::from_secs(60), + powerup: Cow::Borrowed("upgrade"), } } } -impl MobPrototype { - pub fn resolve(self) -> Result { +impl MobPrototype<'_, StringRef> { + pub fn resolve( + self, + powerups: &[PowerupPrototype], + ) -> Result, ValidationError> { if self.name.is_empty() { return Err(ValidationError::custom("name", "prototype had empty name")); } - Ok(self) + let powerup = powerups + .iter() + .find(|proto| proto.name == self.powerup) + .ok_or(ValidationError::custom( + "powerup", + format_args!( + "mob prototype refers to nonexistant powerup prototype `{}`", + self.powerup + ), + ))?; + + Ok(MobPrototype { + name: self.name, + server_type: self.server_type, + lifetime: self.lifetime, + powerup, + }) } } diff --git a/server-config/src/plane.rs b/server-config/src/plane.rs index 718f74be..50e3c379 100644 --- a/server-config/src/plane.rs +++ b/server-config/src/plane.rs @@ -13,14 +13,14 @@ use crate::{MissilePrototype, PrototypeRef, PtrRef, SpecialPrototype, StringRef, serialize = " Ref::MissileRef: Serialize, Ref::SpecialRef: Serialize, - Ref::EffectRef: Serialize, + Ref::PowerupRef: Serialize, Ref::PlaneRef: Serialize, Ref::MobRef: Serialize, ", deserialize = " Ref::MissileRef: Deserialize<'de>, Ref::SpecialRef: Deserialize<'de>, - Ref::EffectRef: Deserialize<'de>, + Ref::PowerupRef: Deserialize<'de>, Ref::PlaneRef: Deserialize<'de>, Ref::MobRef: Deserialize<'de>, " diff --git a/server-config/src/powerup.rs b/server-config/src/powerup.rs new file mode 100644 index 00000000..23363e79 --- /dev/null +++ b/server-config/src/powerup.rs @@ -0,0 +1,66 @@ +use std::borrow::Cow; +use std::time::Duration; + +use protocol::PowerupType; +use serde::{Deserialize, Serialize}; + +use crate::{EffectPrototype, ValidationError}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PowerupPrototype { + pub name: Cow<'static, str>, + pub server_type: Option, + #[serde(with = "crate::util::option_duration")] + pub duration: Option, + pub effects: Vec, +} + +impl PowerupPrototype { + pub fn shield() -> Self { + Self { + name: Cow::Borrowed("shield"), + server_type: Some(PowerupType::Shield), + duration: Some(Duration::from_secs(10)), + effects: vec![EffectPrototype::shield(), EffectPrototype::despawn()], + } + } + + pub fn spawn_shield() -> Self { + Self { + name: Cow::Borrowed("spawn-shield"), + duration: Some(Duration::from_secs(2)), + ..Self::shield() + } + } + + pub fn inferno() -> Self { + Self { + name: Cow::Borrowed("inferno"), + server_type: Some(PowerupType::Inferno), + duration: Some(Duration::from_secs(10)), + effects: vec![EffectPrototype::inferno(), EffectPrototype::despawn()], + } + } + + pub fn upgrade() -> Self { + Self { + name: Cow::Borrowed("upgrade"), + server_type: None, + duration: None, + effects: vec![EffectPrototype::upgrade(), EffectPrototype::despawn()], + } + } +} + +impl PowerupPrototype { + pub(crate) fn resolve(self) -> Result { + if self.name.is_empty() { + return Err(ValidationError::custom( + "name", + "powerup protoype had an empty name", + )); + } + + Ok(self) + } +} diff --git a/server-config/src/special.rs b/server-config/src/special.rs index 38e14c1c..279ba10a 100644 --- a/server-config/src/special.rs +++ b/server-config/src/special.rs @@ -96,14 +96,14 @@ pub struct StealthPrototype { serialize = " Ref::MissileRef: Serialize, Ref::SpecialRef: Serialize, - Ref::EffectRef: Serialize, + Ref::PowerupRef: Serialize, Ref::PlaneRef: Serialize, Ref::MobRef: Serialize, ", deserialize = " Ref::MissileRef: Deserialize<'de>, Ref::SpecialRef: Deserialize<'de>, - Ref::EffectRef: Deserialize<'de>, + Ref::PowerupRef: Deserialize<'de>, Ref::PlaneRef: Deserialize<'de>, Ref::MobRef: Deserialize<'de>, " diff --git a/server/src/component/effect.rs b/server/src/component/effect.rs new file mode 100644 index 00000000..bcf07442 --- /dev/null +++ b/server/src/component/effect.rs @@ -0,0 +1,113 @@ +use std::collections::HashMap; +use std::time::Instant; + +use crate::config::EffectPrototype; +use crate::protocol::PowerupType; + +/// Effect manager for a player. +/// +/// This component tracks the set of effects that a player has. It has two types +/// of effects: +/// 1. short-term effects associated with a powerup, and, +/// 2. long-term effects that have their lifetime explicitly managed. +#[derive(Clone, Debug, Default)] +pub struct Effects { + permanent: HashMap<&'static str, EffectPrototype>, + powerup: Option, +} + +#[derive(Clone, Debug)] +struct PowerupEffects { + powerup: PowerupType, + expiry: Instant, + effects: Vec, +} + +impl Effects { + /// Enable a new set of effects associated with the powerup. This will + /// overwrite any effects associated with the previously active powerup. + pub fn set_powerup( + &mut self, + powerup: PowerupType, + expiry: Instant, + effects: &[EffectPrototype], + ) { + self.powerup = Some(PowerupEffects { + powerup, + expiry, + effects: effects + .iter() + .filter(|e| !e.is_instant()) + .cloned() + .collect(), + }); + } + + pub fn clear_powerup(&mut self) { + self.powerup = None; + } + + /// Get the expiry time of the current powerup. + pub fn expiry(&self) -> Option { + self.powerup.as_ref().map(|p| p.expiry) + } + + /// Get the server type of the current powerup. + pub fn powerup(&self) -> Option { + self.powerup.as_ref().map(|p| p.powerup) + } + + /// Add a new long-term effect. Long-term effects are deduplicated by name. + pub fn add_effect(&mut self, name: &'static str, effect: EffectPrototype) { + self.permanent.insert(name, effect); + } + + /// Remove a long-term effect by prototype name. + pub fn erase_effect(&mut self, name: &str) -> bool { + self.permanent.remove(name).is_some() + } + + pub fn effects<'a>(&'a self) -> impl Iterator { + let permanent = self.permanent.iter().map(|x| x.1); + + let temporary = self + .powerup + .as_ref() + .map(|p| p.effects.as_slice()) + .unwrap_or(&[]) + .iter(); + + permanent.chain(temporary) + } +} + +impl Effects { + /// Whether any of the effects within this component are inferno effects. + pub fn has_inferno(&self) -> bool { + self + .effects() + .any(|e| matches!(e, EffectPrototype::Inferno)) + } + + pub fn has_shield(&self) -> bool { + self.damage_mult() == 0.0 + } + + pub fn damage_mult(&self) -> f32 { + self + .effects() + .filter_map(|e| match e { + EffectPrototype::Shield { damage_mult } => Some(*damage_mult), + _ => None, + }) + .reduce(|acc, mult| acc * mult) + .unwrap_or(1.0) + } + + pub fn fixed_speed(&self) -> Option { + self.effects().find_map(|e| match e { + EffectPrototype::FixedSpeed { speed } => Some(*speed), + _ => None, + }) + } +} diff --git a/server/src/component/mod.rs b/server/src/component/mod.rs index 8379fed3..b74893b9 100644 --- a/server/src/component/mod.rs +++ b/server/src/component/mod.rs @@ -7,8 +7,10 @@ use bstr::BString; use hecs::Entity; use uuid::Uuid; +mod effect; mod keystate; +pub use self::effect::Effects; pub use self::keystate::KeyState; pub use crate::protocol::{FlagCode, MobType, PlaneType, PowerupType}; @@ -161,49 +163,6 @@ pub struct PowerupData { pub end_time: Instant, } -/// The current powerup that a player has, or none if the player has no powerup. -/// -/// Utility methods are provided to simplify checking what powerups a player -/// has. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] -pub struct Powerup { - pub data: Option, -} - -impl Powerup { - pub fn none() -> Self { - Self::default() - } - - /// Create a new powerup with the provided type and expiry time. - pub fn new(ty: PowerupType, end_time: Instant) -> Self { - Self { - data: Some(PowerupData { ty, end_time }), - } - } - - /// Whether the current powerup is an inferno. - pub fn inferno(&self) -> bool { - self - .data - .map(|x| x.ty == PowerupType::Inferno) - .unwrap_or(false) - } - - /// Whether the current powerup is a shield. - pub fn shield(&self) -> bool { - self - .data - .map(|x| x.ty == PowerupType::Shield) - .unwrap_or(false) - } - - /// The time at which the current powerup expires, should there be one. - pub fn expires(&self) -> Option { - self.data.map(|x| x.end_time) - } -} - /// The current state of the upgrades that a player has. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Upgrades { diff --git a/server/src/defaults.rs b/server/src/defaults.rs index 67fe3eac..87fabf78 100644 --- a/server/src/defaults.rs +++ b/server/src/defaults.rs @@ -50,13 +50,13 @@ pub(crate) fn build_default_player( .add(LastActionTime(start_time)) .add(SpecialActive(false)) .add(RespawnAllowed(true)) - .add(Powerup::default()) .add(JoinTime(this_frame)) .add(Spectating::default()) .add(PlayerPing(Duration::ZERO)) .add(TotalDamage(0.0)) .add(Captures(0)) - .add(MissileFiringSide::Left); + .add(MissileFiringSide::Left) + .add(Effects::default()); builder } diff --git a/server/src/event/player.rs b/server/src/event/player.rs index e6202b4b..30968fbb 100644 --- a/server/src/event/player.rs +++ b/server/src/event/player.rs @@ -3,7 +3,7 @@ use std::time::Duration; use hecs::Entity; use smallvec::SmallVec; -use crate::config::PlanePrototypeRef; +use crate::config::{PlanePrototypeRef, PowerupPrototypeRef}; use crate::protocol::PowerupType; /// A new player has joined the game. @@ -68,6 +68,7 @@ pub struct PlayerPowerup { pub player: Entity, pub ty: PowerupType, pub duration: Duration, + pub powerup: PowerupPrototypeRef, } /// A goliath has used their special. diff --git a/server/src/lib.rs b/server/src/lib.rs index 448f423b..28ee7482 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -12,7 +12,8 @@ pub mod config { pub type PlanePrototypeRef = &'static PlanePrototype<'static, PtrRef>; pub type MissilePrototypeRef = &'static MissilePrototype; pub type SpecialPrototypeRef = &'static SpecialPrototype<'static, PtrRef>; - pub type MobPrototypeRef = &'static MobPrototype; + pub type PowerupPrototypeRef = &'static PowerupPrototype; + pub type MobPrototypeRef = &'static MobPrototype<'static, PtrRef>; } pub extern crate hecs; diff --git a/server/src/system/handler/on_player_join.rs b/server/src/system/handler/on_player_join.rs index d299e521..f736301f 100644 --- a/server/src/system/handler/on_player_join.rs +++ b/server/src/system/handler/on_player_join.rs @@ -23,13 +23,13 @@ fn send_login_packet(event: &PlayerJoin, game: &mut AirmashGame) { &Rotation, &FlagCode, &Upgrades, - &Powerup, + &Effects, )>() .with::(); let players = query .into_iter() .map( - |(ent, (alive, level, name, plane, team, pos, rot, flag, upgrades, powerup))| LoginPlayer { + |(ent, (alive, level, name, plane, team, pos, rot, flag, upgrades, effects))| LoginPlayer { id: ent.id() as u16, status: PlayerStatus::from(*alive), level: level.0, @@ -39,7 +39,7 @@ fn send_login_packet(event: &PlayerJoin, game: &mut AirmashGame) { pos: pos.0, rot: rot.0, flag: *flag, - upgrades: crate::util::get_server_upgrades(upgrades, powerup), + upgrades: crate::util::get_server_upgrades(upgrades, effects), }, ) .collect::>(); @@ -107,14 +107,14 @@ fn send_player_new(event: &PlayerJoin, game: &mut AirmashGame) { &Rotation, &FlagCode, &Upgrades, - &Powerup, + &Effects, )>(event.player) { Ok(query) => query.with::(), Err(_) => return, }; - if let Some((alive, name, plane, team, pos, rot, flag, upgrades, powerup)) = query.get() { + if let Some((alive, name, plane, team, pos, rot, flag, upgrades, effects)) = query.get() { let packet = PlayerNew { id: event.player.id() as _, status: PlayerStatus::from(*alive), @@ -124,7 +124,7 @@ fn send_player_new(event: &PlayerJoin, game: &mut AirmashGame) { pos: pos.0, rot: rot.0, flag: *flag, - upgrades: crate::util::get_server_upgrades(upgrades, powerup), + upgrades: crate::util::get_server_upgrades(upgrades, effects), }; game.send_to_others(event.player, packet); diff --git a/server/src/system/handler/on_player_missile_collision.rs b/server/src/system/handler/on_player_missile_collision.rs index aa5089a1..80d7e0f2 100644 --- a/server/src/system/handler/on_player_missile_collision.rs +++ b/server/src/system/handler/on_player_missile_collision.rs @@ -29,7 +29,7 @@ fn damage_player(event: &PlayerMissileCollision, game: &mut AirmashGame) { let query = game.world.query_one::<( &mut Health, &PlanePrototypeRef, - &Powerup, + &Effects, &Upgrades, &mut IsAlive, )>(player); @@ -38,28 +38,17 @@ fn damage_player(event: &PlayerMissileCollision, game: &mut AirmashGame) { Err(_) => continue, }; - if let Some((health, &plane, powerup, upgrades, alive)) = query.get() { + if let Some((health, &plane, effects, upgrades, alive)) = query.get() { // No damage can be done if the player is dead if !alive.0 { continue; } - // No damage can be done if the player is shielded - if powerup.shield() { - hits.push(PlayerHit { - player, - missile: event.missile, - damage: 0.0, - attacker, - }); - - continue; - } - let damage = match game_config.allow_damage { true => { mob.damage * plane.damage_factor / config.upgrades.defense.factor[upgrades.defense as usize] + * effects.damage_mult() } false => 0.0, }; diff --git a/server/src/system/handler/on_player_mob_collision.rs b/server/src/system/handler/on_player_mob_collision.rs index 2ebafa04..7c7cb587 100644 --- a/server/src/system/handler/on_player_mob_collision.rs +++ b/server/src/system/handler/on_player_mob_collision.rs @@ -1,9 +1,6 @@ -use std::time::Duration; - use crate::component::*; use crate::config::MobPrototypeRef; use crate::event::{MobDespawn, MobDespawnType, PlayerMobCollision, PlayerPowerup, PowerupExpire}; -use crate::protocol::PowerupType; use crate::AirmashGame; #[handler] @@ -98,15 +95,15 @@ fn update_player_powerup(event: &PlayerMobCollision, game: &mut AirmashGame) { return; } - let (&powerup, _) = match game + let (effects, _) = match game .world - .query_one_mut::<(&Powerup, &IsPlayer)>(event.player) + .query_one_mut::<(&Effects, &IsPlayer)>(event.player) { Ok(query) => query, Err(_) => return, }; - if powerup.data.is_some() { + if effects.expiry().is_some() { game.dispatch(PowerupExpire { player: event.player, }); @@ -114,11 +111,8 @@ fn update_player_powerup(event: &PlayerMobCollision, game: &mut AirmashGame) { game.dispatch(PlayerPowerup { player: event.player, - ty: match mob.server_type { - MobType::Shield => PowerupType::Shield, - MobType::Inferno => PowerupType::Inferno, - _ => unreachable!(), - }, - duration: Duration::from_secs(10), + ty: mob.powerup.server_type.unwrap(), + duration: mob.powerup.duration.unwrap(), + powerup: mob.powerup, }); } diff --git a/server/src/system/handler/on_player_powerup.rs b/server/src/system/handler/on_player_powerup.rs index b3b5063a..e8058d9a 100644 --- a/server/src/system/handler/on_player_powerup.rs +++ b/server/src/system/handler/on_player_powerup.rs @@ -1,6 +1,5 @@ use crate::component::{IsPlayer, *}; use crate::event::PlayerPowerup; -use crate::resource::{StartTime, ThisFrame}; use crate::AirmashGame; #[handler] @@ -11,30 +10,40 @@ fn send_packet(event: &PlayerPowerup, game: &mut AirmashGame) { return; } - let duration = event.duration.as_secs() * 1000 + event.duration.subsec_millis() as u64; + let (duration, ty) = match (event.powerup.duration, event.powerup.server_type) { + (Some(duration), Some(ty)) => (duration, ty), + _ => return, + }; + + let duration = duration.as_secs() * 1000 + duration.subsec_millis() as u64; game.send_to( event.player, s::PlayerPowerup { duration: duration as u32, - ty: event.ty, + ty, }, ); } #[handler] -fn update_fields(event: &PlayerPowerup, game: &mut AirmashGame) { - let start_time = game.resources.read::().0; - let this_frame = game.resources.read::().0; +fn update_effects(event: &PlayerPowerup, game: &mut AirmashGame) { + let start_time = game.start_time(); + let this_frame = game.this_frame(); + + let (duration, ty) = match (event.powerup.duration, event.powerup.server_type) { + (Some(duration), Some(ty)) => (duration, ty), + _ => return, + }; - let (last_update, powerup, _) = match game + let (last_update, effects, _) = match game .world - .query_one_mut::<(&mut LastUpdateTime, &mut Powerup, &IsPlayer)>(event.player) + .query_one_mut::<(&mut LastUpdateTime, &mut Effects, &IsPlayer)>(event.player) { Ok(query) => query, Err(_) => return, }; last_update.0 = start_time; - *powerup = Powerup::new(event.ty, this_frame + event.duration); + effects.set_powerup(ty, this_frame + duration, &event.powerup.effects); } diff --git a/server/src/system/handler/on_player_respawn.rs b/server/src/system/handler/on_player_respawn.rs index 637b569f..16363024 100644 --- a/server/src/system/handler/on_player_respawn.rs +++ b/server/src/system/handler/on_player_respawn.rs @@ -1,18 +1,17 @@ use crate::component::*; use crate::config::PlanePrototypeRef; use crate::event::{PlayerPowerup, PlayerRespawn, PlayerSpawn}; -use crate::protocol::PowerupType; -use crate::resource::Config; +use crate::resource::{Config, GameConfig}; use crate::{AirmashGame, EntitySetBuilder, Vector2}; #[handler] fn send_packet(event: &PlayerRespawn, game: &mut AirmashGame) { use crate::protocol::server::PlayerRespawn; - let (&pos, &rot, upgrades, powerup, _) = + let (&pos, &rot, upgrades, effects, _) = match game .world - .query_one_mut::<(&Position, &Rotation, &Upgrades, &Powerup, &IsAlive)>(event.player) + .query_one_mut::<(&Position, &Rotation, &Upgrades, &Effects, &IsAlive)>(event.player) { Ok(query) => query, Err(_) => return, @@ -22,7 +21,7 @@ fn send_packet(event: &PlayerRespawn, game: &mut AirmashGame) { id: event.player.id() as _, pos: pos.0, rot: rot.0, - upgrades: crate::util::get_server_upgrades(upgrades, powerup), + upgrades: crate::util::get_server_upgrades(upgrades, effects), }; game.send_to_entities( @@ -36,6 +35,7 @@ fn send_packet(event: &PlayerRespawn, game: &mut AirmashGame) { #[handler(priority = crate::priority::PRE_LOGIN)] fn reset_player(event: &PlayerRespawn, game: &mut AirmashGame) { let config = game.resources.read::(); + let gconfig = game.resources.read::(); let mut query = match game.world.query_one::<( &mut Position, @@ -89,16 +89,21 @@ fn reset_player(event: &PlayerRespawn, game: &mut AirmashGame) { active.0 = false; spectgt.0 = None; - let powerup = PlayerPowerup { - player: event.player, - ty: PowerupType::Shield, - duration: config.spawn_shield_duration, - }; + let proto = gconfig.powerups.get("spawn-shield").copied(); + drop(gconfig); drop(config); drop(query); - game.dispatch(powerup); + if let Some(proto) = proto { + #[allow(deprecated)] + game.dispatch(PlayerPowerup { + player: event.player, + ty: proto.server_type.unwrap(), + duration: proto.duration.unwrap(), + powerup: proto, + }); + } } #[handler] diff --git a/server/src/system/keys.rs b/server/src/system/keys.rs index 07ef1fb8..5004ff63 100644 --- a/server/src/system/keys.rs +++ b/server/src/system/keys.rs @@ -20,13 +20,13 @@ fn fire_missiles(game: &mut AirmashGame) { &LastFireTime, &mut Energy, &PlanePrototypeRef, - &Powerup, + &Effects, &IsAlive, )>() .with::(); let mut events = Vec::new(); - for (ent, (keystate, last_fire, energy, plane, powerup, alive)) in query.iter() { + for (ent, (keystate, last_fire, energy, plane, effects, alive)) in query.iter() { if !alive.0 || !keystate.fire || this_frame - last_fire.0 < plane.fire_delay @@ -38,7 +38,7 @@ fn fire_missiles(game: &mut AirmashGame) { energy.0 -= plane.fire_energy; let mut count = 1; - if powerup.inferno() { + if effects.has_inferno() { count = count * 2 + 1; } diff --git a/server/src/system/physics.rs b/server/src/system/physics.rs index 58486d25..4978eb52 100644 --- a/server/src/system/physics.rs +++ b/server/src/system/physics.rs @@ -31,14 +31,14 @@ fn update_player_positions(game: &mut AirmashGame) { &mut Velocity, &KeyState, &Upgrades, - &Powerup, + &Effects, &PlanePrototypeRef, &SpecialActive, &IsAlive, )>() .with::(); - for (_entity, (pos, rot, vel, keystate, upgrades, powerup, plane, active, alive)) in query { + for (_entity, (pos, rot, vel, keystate, upgrades, effects, plane, active, alive)) in query { if !alive.0 { continue; } @@ -104,12 +104,12 @@ fn update_player_positions(game: &mut AirmashGame) { max_speed *= config.upgrades.speed.factor[upgrades.speed as usize]; } - if powerup.inferno() { + if effects.has_inferno() { max_speed *= plane.inferno_factor; } - if keystate.flagspeed { - max_speed = plane.flag_speed; + if let Some(speed) = effects.fixed_speed() { + max_speed = speed; } if speed > max_speed { @@ -202,7 +202,7 @@ fn send_update_packets(game: &mut AirmashGame) { &PlanePrototypeRef, &KeyState, &Upgrades, - &Powerup, + &Effects, &mut LastUpdateTime, &Team, &SpecialActive, @@ -214,7 +214,7 @@ fn send_update_packets(game: &mut AirmashGame) { for ( ent, - (pos, rot, vel, plane, keystate, upgrades, powerup, last_update, team, active, alive), + (pos, rot, vel, plane, keystate, upgrades, effects, last_update, team, active, alive), ) in query.iter() { if !alive.0 { @@ -228,8 +228,8 @@ fn send_update_packets(game: &mut AirmashGame) { let ups = ServerUpgrades { speed: upgrades.speed, - shield: powerup.shield(), - inferno: powerup.inferno(), + shield: effects.has_shield(), + inferno: effects.has_inferno(), }; let packet = PlayerUpdate { diff --git a/server/src/system/powerups.rs b/server/src/system/powerups.rs index c2096e5d..770f1986 100644 --- a/server/src/system/powerups.rs +++ b/server/src/system/powerups.rs @@ -2,33 +2,32 @@ use smallvec::SmallVec; use crate::component::*; use crate::event::PowerupExpire; -use crate::resource::ThisFrame; use crate::AirmashGame; pub fn update(game: &mut AirmashGame) { - expire_powerups(game); + expire_effects(game); } -fn expire_powerups(game: &mut AirmashGame) { - let this_frame = game.resources.read::().0; - let query = game.world.query_mut::<&Powerup>().with::(); +fn expire_effects(game: &mut AirmashGame) { + let this_frame = game.this_frame(); + let query = game.world.query_mut::<&Effects>().with::(); let mut events = SmallVec::<[_; 16]>::new(); - for (ent, powerup) in query { - let powerup = match &powerup.data { - Some(data) => data, - None => continue, + for (ent, effects) in query { + match effects.expiry() { + Some(expiry) if expiry <= this_frame => (), + _ => continue, }; - if powerup.end_time > this_frame { - continue; - } - events.push(PowerupExpire { player: ent }); } for event in events { game.dispatch(event); - game.world.get_mut::(event.player).unwrap().data = None; + game + .world + .get_mut::(event.player) + .unwrap() + .clear_powerup(); } } diff --git a/server/src/system/specials.rs b/server/src/system/specials.rs index 552a8d4f..743fd438 100644 --- a/server/src/system/specials.rs +++ b/server/src/system/specials.rs @@ -60,7 +60,7 @@ fn tornado_special_fire(game: &mut AirmashGame) { &LastFireTime, &mut Energy, &PlanePrototypeRef, - &Powerup, + &Effects, &IsAlive, )>() .with::(); @@ -84,7 +84,7 @@ fn tornado_special_fire(game: &mut AirmashGame) { let mut missiles = SmallVec::<[_; 5]>::new(); // FIXME: This currently ignores the multishot.count property. - if powerup.inferno() { + if powerup.has_inferno() { missiles.extend_from_slice(&tornado_inferno_missile_details(multishot.missile)) } else { missiles.extend_from_slice(&tornado_missile_details(multishot.missile)); diff --git a/server/src/util/mod.rs b/server/src/util/mod.rs index 179dea3d..9bf6b3f6 100644 --- a/server/src/util/mod.rs +++ b/server/src/util/mod.rs @@ -4,7 +4,7 @@ use std::time::{Duration, Instant}; use nalgebra::vector; -use crate::component::{Powerup, Upgrades}; +use crate::component::{Effects, Upgrades}; use crate::protocol::{Time, Vector2}; use crate::resource::*; use crate::AirmashGame; @@ -39,11 +39,11 @@ pub fn get_current_clock(game: &AirmashGame) -> u32 { get_time_clock(game, this_frame.0) } -pub fn get_server_upgrades(upgrades: &Upgrades, powerup: &Powerup) -> crate::protocol::Upgrades { +pub fn get_server_upgrades(upgrades: &Upgrades, effects: &Effects) -> crate::protocol::Upgrades { crate::protocol::Upgrades { speed: upgrades.speed, - shield: powerup.shield(), - inferno: powerup.inferno(), + shield: effects.has_shield(), + inferno: effects.has_inferno(), } } diff --git a/server/tests/behaviour/powerups.rs b/server/tests/behaviour/powerups.rs index 49fd3e3a..1f096982 100644 --- a/server/tests/behaviour/powerups.rs +++ b/server/tests/behaviour/powerups.rs @@ -1,6 +1,7 @@ use std::time::Duration; use server::component::*; +use server::protocol::{server as s, ServerPacket}; use server::test::TestGame; use server::Vector2; @@ -20,13 +21,9 @@ fn player_is_upgraded_on_collision_with_upgrade() { "Powerup was not despawned despite having collided with a player" ); - let powerup = game.world.get::(player).unwrap(); + let effects = game.world.get::(player).unwrap(); - assert!(powerup.data.is_some()); - - if let Some(data) = &powerup.data { - assert_eq!(data.ty, PowerupType::Inferno); - } + assert!(matches!(effects.powerup(), Some(PowerupType::Inferno))); } #[test] @@ -51,8 +48,37 @@ fn dual_powerup_collision() { "Powerup was not despawned despite having collided with a player" ); - let p1pow = game.world.get::(p1).unwrap(); - let p2pow = game.world.get::(p2).unwrap(); + let p1pow = game.world.get::(p1).unwrap(); + let p2pow = game.world.get::(p2).unwrap(); + + assert!(p1pow.powerup().is_some() != p2pow.powerup().is_some()); +} - assert!(p1pow.data.is_some() != p2pow.data.is_some()); +#[test] +fn inferno_slows_down_plane() { + let (mut game, mut mock) = TestGame::new(); + + let mut client = mock.open(); + let entity = client.login("test", &mut game); + + game.world.get_mut::(entity).unwrap().0 = Vector2::zeros(); + game.spawn_mob(MobType::Inferno, Vector2::zeros(), Duration::from_secs(60)); + game.run_for(Duration::from_secs(2)); + + assert!(client.packets().any(|p| matches!( + p, + ServerPacket::PlayerPowerup(s::PlayerPowerup { + ty: PowerupType::Inferno, + .. + }) + ))); + + let has_inferno = client + .packets() + .filter_map(|p| match p { + ServerPacket::PlayerUpdate(p) => Some(p), + _ => None, + }) + .any(|p| p.upgrades.inferno); + assert!(has_inferno); }