diff --git a/CHANGELOG.md b/CHANGELOG.md index d21aeda126..d890aa03d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fireflies - Fullscreen modes now show two options (exclusive and borderless) - Added banlist and `/ban`, `/unban`, and `/kick` commands for admins +- A new dungeon boss (venture there and discover it yourself) ### Changed diff --git a/assets/common/items/npc_weapons/npcweapon/stone_golems_fist.ron b/assets/common/items/npc_weapons/npcweapon/stone_golems_fist.ron new file mode 100644 index 0000000000..ad00cb189c --- /dev/null +++ b/assets/common/items/npc_weapons/npcweapon/stone_golems_fist.ron @@ -0,0 +1,13 @@ +ItemDef( + name: "Stone Golem's Fist", + description: "Was attached to a mighty stone golem.", + kind: Tool( + ( + kind: NpcWeapon("StoneGolemsFist"), + stats: ( + equip_time_millis: 500, + power: 1.00, + ), + ) + ), +) diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 32de189e4c..c22cf9a264 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -47,13 +47,14 @@ const int FIREWORK_YELLOW = 8; const int LEAF = 9; const int FIREFLY = 10; const int BEE = 11; +const int GROUND_SHOCKWAVE = 12; // meters per second squared (acceleration) const float earth_gravity = 9.807; struct Attr { vec3 offs; - float scale; + vec3 scale; vec4 col; mat4 rot; }; @@ -122,7 +123,7 @@ void main() { vec3(0), vec3(rand2 * 0.02, rand3 * 0.02, 1.0 + rand4 * 0.1) ), - linear_scale(0.5), + vec3(linear_scale(0.5)), vec4(1, 1, 1, start_end(1.0, 0.0)), spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 0.5) ); @@ -132,7 +133,7 @@ void main() { vec3(rand0 * 0.25, rand1 * 0.25, 0.3), vec3(rand2 * 0.1, rand3 * 0.1, 2.0 + rand4 * 1.0) ), - 1.0, + vec3(1.0), vec4(2, 0.8 + rand5 * 0.3, 0, 1), spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3) ); @@ -142,7 +143,7 @@ void main() { vec3(rand0, rand1, rand3) * 0.3, vec3(rand4, rand5, rand6) * 2.0 + grav_vel(earth_gravity) ), - 1.0, + vec3(1.0), vec4(3.5, 3 + rand7, 0, 1), spin_in_axis(vec3(1,0,0),0) ); @@ -152,7 +153,7 @@ void main() { vec3(0), vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) ), - 3.0 + rand0, + vec3(3.0 + rand0), vec4(vec3(0.6 + rand7 * 0.4), 1), spin_in_axis(vec3(1,0,0),0) ); @@ -162,7 +163,7 @@ void main() { vec3(0), vec3(rand1, rand2, rand3) * 40.0 + grav_vel(earth_gravity) ), - 3.0 + rand0, + vec3(3.0 + rand0), vec4(0.15, 0.4, 1, 1), identity() ); @@ -172,7 +173,7 @@ void main() { vec3(0), vec3(rand1, rand2, rand3) * 40.0 + grav_vel(earth_gravity) ), - 3.0 + rand0, + vec3(3.0 + rand0), vec4(0, 1, 0, 1), identity() ); @@ -182,7 +183,7 @@ void main() { vec3(0), vec3(rand1, rand2, rand3) * 40.0 + grav_vel(earth_gravity) ), - 3.0 + rand0, + vec3(3.0 + rand0), vec4(0.7, 0.0, 1.0, 1.0), identity() ); @@ -192,7 +193,7 @@ void main() { vec3(0), vec3(rand1, rand2, rand3) * 40.0 + grav_vel(earth_gravity) ), - 3.0 + rand0, + vec3(3.0 + rand0), vec4(1, 0, 0, 1), identity() ); @@ -202,7 +203,7 @@ void main() { vec3(0), vec3(rand1, rand2, rand3) * 40.0 + grav_vel(earth_gravity) ), - 3.0 + rand0, + vec3(3.0 + rand0), vec4(1, 1, 0, 1), identity() ); @@ -212,7 +213,7 @@ void main() { vec3(0), vec3(0, 0, -2) ) + vec3(sin(lifetime), sin(lifetime + 0.7), sin(lifetime * 0.5)) * 2.0, - 4, + vec3(4), vec4(vec3(0.2 + rand7 * 0.2, 0.2 + (0.5 + rand6 * 0.5) * 0.6, 0), 1), spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 5) ); @@ -224,7 +225,7 @@ void main() { sin(lifetime * 3.0 + rand1) + sin(lifetime * 8.0 + rand4) * 0.3, sin(lifetime * 2.0 + rand2) + sin(lifetime * 9.0 + rand5) * 0.3 ), - raise, + vec3(raise), vec4(vec3(5, 5, 1.1), 1), spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 5) ); @@ -236,17 +237,24 @@ void main() { sin(lifetime * 3.0 + rand1) + sin(lifetime * 10.0 + rand4) * 0.3, sin(lifetime * 4.0 + rand2) + sin(lifetime * 11.0 + rand5) * 0.3 ) * 0.5, - lower, + vec3(lower), vec4(vec3(1, 0.7, 0), 1), spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 5) ); + } else if (inst_mode == GROUND_SHOCKWAVE) { + attr = Attr( + vec3(0.0), + vec3(11.0, 11.0, (33.0 * rand0 * sin(2.0 * lifetime * 3.14 * 2.0))) / 3, + vec4(vec3(0.32 + (rand0 * 0.04), 0.22 + (rand1 * 0.03), 0.05 + (rand2 * 0.01)), 1), + spin_in_axis(vec3(1,0,0),0) + ); } else { attr = Attr( linear_motion( vec3(rand0 * 0.25, rand1 * 0.25, 1.7 + rand5), vec3(rand2 * 0.1, rand3 * 0.1, 1.0 + rand4 * 0.5) ), - exp_scale(-0.2), + vec3(exp_scale(-0.2)), vec4(1), spin_in_axis(vec3(1,0,0),0) ); diff --git a/assets/voxygen/voxel/weapon/npcweapon/cyclops_hammer.vox b/assets/voxygen/voxel/weapon/npcweapon/cyclops_hammer.vox new file mode 100644 index 0000000000..93bf3d1752 --- /dev/null +++ b/assets/voxygen/voxel/weapon/npcweapon/cyclops_hammer.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef11b80feaf3144e07c4779baa8eaf248e4311e3c927e050193f254ff7136111 +size 29267 diff --git a/client/src/lib.rs b/client/src/lib.rs index 5750dd5f0b..e74ed9bd9d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -22,6 +22,7 @@ use common::{ group, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, InventoryManip, InventoryUpdateEvent, }, + event::{EventBus, LocalEvent}, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, DisconnectReason, InviteAnswer, Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, @@ -1426,6 +1427,15 @@ impl Client { ServerMsg::Outcomes(outcomes) => { frontend_events.extend(outcomes.into_iter().map(Event::Outcome)) }, + ServerMsg::Knockback(impulse) => { + self.state + .ecs() + .read_resource::>() + .emit_now(LocalEvent::ApplyImpulse { + entity: self.entity, + impulse, + }); + }, } } } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 3f4931de89..4c328be14d 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -24,6 +24,7 @@ pub enum CharacterAbilityType { TripleStrike(Stage), LeapMelee, SpinMelee, + GroundShockwave, } impl From<&CharacterState> for CharacterAbilityType { @@ -38,6 +39,7 @@ impl From<&CharacterState> for CharacterAbilityType { CharacterState::TripleStrike(data) => Self::TripleStrike(data.stage), CharacterState::SpinMelee(_) => Self::SpinMelee, CharacterState::ChargedRanged(_) => Self::ChargedRanged, + CharacterState::GroundShockwave(_) => Self::ChargedRanged, _ => Self::BasicMelee, } } @@ -50,6 +52,7 @@ pub enum CharacterAbility { buildup_duration: Duration, recover_duration: Duration, base_healthchange: i32, + knockback: f32, range: f32, max_angle: f32, }, @@ -62,6 +65,7 @@ pub enum CharacterAbility { projectile_body: Body, projectile_light: Option, projectile_gravity: Option, + projectile_speed: f32, }, Boost { duration: Duration, @@ -104,6 +108,20 @@ pub enum CharacterAbility { recover_duration: Duration, projectile_body: Body, projectile_light: Option, + projectile_gravity: Option, + initial_projectile_speed: f32, + max_projectile_speed: f32, + }, + GroundShockwave { + energy_cost: u32, + buildup_duration: Duration, + recover_duration: Duration, + damage: u32, + knockback: f32, + shockwave_angle: f32, + shockwave_speed: f32, + shockwave_duration: Duration, + requires_ground: bool, }, } @@ -150,6 +168,10 @@ impl CharacterAbility { .energy .try_change_by(-(*energy_cost as i32), EnergySource::Ability) .is_ok(), + CharacterAbility::GroundShockwave { energy_cost, .. } => update + .energy + .try_change_by(-(*energy_cost as i32), EnergySource::Ability) + .is_ok(), _ => true, } } @@ -250,6 +272,7 @@ impl From<&CharacterAbility> for CharacterState { buildup_duration, recover_duration, base_healthchange, + knockback, range, max_angle, energy_cost: _, @@ -258,6 +281,7 @@ impl From<&CharacterAbility> for CharacterState { buildup_duration: *buildup_duration, recover_duration: *recover_duration, base_healthchange: *base_healthchange, + knockback: *knockback, range: *range, max_angle: *max_angle, }), @@ -269,6 +293,7 @@ impl From<&CharacterAbility> for CharacterState { projectile_body, projectile_light, projectile_gravity, + projectile_speed, energy_cost: _, } => CharacterState::BasicRanged(basic_ranged::Data { exhausted: false, @@ -280,6 +305,7 @@ impl From<&CharacterAbility> for CharacterState { projectile_body: *projectile_body, projectile_light: *projectile_light, projectile_gravity: *projectile_gravity, + projectile_speed: *projectile_speed, }), CharacterAbility::Boost { duration, only_up } => CharacterState::Boost(boost::Data { duration: *duration, @@ -363,6 +389,9 @@ impl From<&CharacterAbility> for CharacterState { recover_duration, projectile_body, projectile_light, + projectile_gravity, + initial_projectile_speed, + max_projectile_speed, } => CharacterState::ChargedRanged(charged_ranged::Data { exhausted: false, energy_drain: *energy_drain, @@ -376,6 +405,30 @@ impl From<&CharacterAbility> for CharacterState { recover_duration: *recover_duration, projectile_body: *projectile_body, projectile_light: *projectile_light, + projectile_gravity: *projectile_gravity, + initial_projectile_speed: *initial_projectile_speed, + max_projectile_speed: *max_projectile_speed, + }), + CharacterAbility::GroundShockwave { + energy_cost: _, + buildup_duration, + recover_duration, + damage, + knockback, + shockwave_angle, + shockwave_speed, + shockwave_duration, + requires_ground, + } => CharacterState::GroundShockwave(ground_shockwave::Data { + exhausted: false, + buildup_duration: *buildup_duration, + recover_duration: *recover_duration, + damage: *damage, + knockback: *knockback, + shockwave_angle: *shockwave_angle, + shockwave_speed: *shockwave_speed, + shockwave_duration: *shockwave_duration, + requires_ground: *requires_ground, }), } } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index ec9ba06157..16435dc608 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -263,7 +263,7 @@ impl Body { _ => 1000, }, Body::Object(_) => 10000, - Body::Golem(_) => 1500, + Body::Golem(_) => 2560, Body::Theropod(_) => 50, Body::QuadrupedLow(quadruped_low) => match quadruped_low.species { quadruped_low::Species::Crocodile => 600, @@ -330,7 +330,7 @@ impl Body { _ => 100, }, Body::Object(_) => 10, - Body::Golem(_) => 150, + Body::Golem(_) => 260, Body::Theropod(_) => 20, Body::QuadrupedLow(quadruped_low) => match quadruped_low.species { quadruped_low::Species::Crocodile => 20, @@ -395,7 +395,7 @@ impl Body { _ => 100, }, Body::Object(_) => 1, - Body::Golem(_) => 75, + Body::Golem(_) => 256, Body::Theropod(_) => 2, Body::QuadrupedLow(quadruped_low) => match quadruped_low.species { quadruped_low::Species::Crocodile => 10, @@ -425,7 +425,7 @@ impl Body { Body::FishSmall(_) => 1, Body::BipedLarge(_) => 2, Body::Object(_) => 0, - Body::Golem(_) => 5, + Body::Golem(_) => 12, Body::Theropod(_) => 1, Body::QuadrupedLow(_) => 1, } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 26b490a7f4..e262c86905 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -69,6 +69,8 @@ pub enum CharacterState { SpinMelee(spin_melee::Data), /// A charged ranged attack (e.g. bow) ChargedRanged(charged_ranged::Data), + /// A ground shockwave attack + GroundShockwave(ground_shockwave::Data), } impl CharacterState { @@ -83,6 +85,7 @@ impl CharacterState { | CharacterState::LeapMelee(_) | CharacterState::SpinMelee(_) | CharacterState::ChargedRanged(_) + | CharacterState::GroundShockwave(_) ) } @@ -95,6 +98,7 @@ impl CharacterState { | CharacterState::LeapMelee(_) | CharacterState::SpinMelee(_) | CharacterState::ChargedRanged(_) + | CharacterState::GroundShockwave(_) ) } @@ -107,6 +111,7 @@ impl CharacterState { | CharacterState::BasicBlock | CharacterState::LeapMelee(_) | CharacterState::ChargedRanged(_) + | CharacterState::GroundShockwave(_) ) } diff --git a/common/src/comp/damage.rs b/common/src/comp/damage.rs index 05cd6919a4..07ffebf291 100644 --- a/common/src/comp/damage.rs +++ b/common/src/comp/damage.rs @@ -15,6 +15,7 @@ pub enum DamageSource { Projectile, Explosion, Falling, + Shockwave, } impl Damage { @@ -35,7 +36,9 @@ impl Damage { self.healthchange *= 1.0 - damage_reduction; // Critical damage applies after armor for melee - self.healthchange += critdamage; + if (damage_reduction - 1.0).abs() > f32::EPSILON { + self.healthchange += critdamage; + } // Min damage if (damage_reduction - 1.0).abs() > f32::EPSILON && self.healthchange > -10.0 { @@ -74,6 +77,16 @@ impl Damage { self.healthchange = -10.0; } }, + DamageSource::Shockwave => { + // Armor + let damage_reduction = loadout.get_damage_reduction(); + self.healthchange *= 1.0 - damage_reduction; + + // Min damage + if (damage_reduction - 1.0).abs() > f32::EPSILON && self.healthchange > -10.0 { + self.healthchange = -10.0; + } + }, _ => {}, } } diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 8b36fa8756..58cb8aae6a 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -16,6 +16,7 @@ pub enum ToolKind { Dagger(String), Staff(String), Shield(String), + NpcWeapon(String), Debug(String), Farming(String), /// This is an placeholder item, it is used by non-humanoid npcs to attack @@ -32,6 +33,7 @@ impl ToolKind { ToolKind::Dagger(_) => Hands::OneHand, ToolKind::Staff(_) => Hands::TwoHand, ToolKind::Shield(_) => Hands::OneHand, + ToolKind::NpcWeapon(_) => Hands::TwoHand, ToolKind::Debug(_) => Hands::TwoHand, ToolKind::Farming(_) => Hands::TwoHand, ToolKind::Empty => Hands::OneHand, @@ -53,6 +55,7 @@ pub enum ToolCategory { Dagger, Staff, Shield, + NpcWeapon, Debug, Farming, Empty, @@ -68,6 +71,7 @@ impl From<&ToolKind> for ToolCategory { ToolKind::Dagger(_) => ToolCategory::Dagger, ToolKind::Staff(_) => ToolCategory::Staff, ToolKind::Shield(_) => ToolCategory::Shield, + ToolKind::NpcWeapon(_) => ToolCategory::NpcWeapon, ToolKind::Debug(_) => ToolCategory::Debug, ToolKind::Farming(_) => ToolCategory::Farming, ToolKind::Empty => ToolCategory::Empty, @@ -141,6 +145,7 @@ impl Tool { buildup_duration: Duration::from_millis(700), recover_duration: Duration::from_millis(300), base_healthchange: (-120.0 * self.base_power()) as i32, + knockback: 0.0, range: 3.5, max_angle: 20.0, }, @@ -157,6 +162,7 @@ impl Tool { buildup_duration: Duration::from_millis(700), recover_duration: Duration::from_millis(150), base_healthchange: (-50.0 * self.base_power()) as i32, + knockback: 0.0, range: 3.5, max_angle: 20.0, }], @@ -181,6 +187,7 @@ impl Tool { projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, projectile_gravity: Some(Gravity(0.2)), + projectile_speed: 100.0, }, ChargedRanged { energy_cost: 0, @@ -194,6 +201,9 @@ impl Tool { recover_duration: Duration::from_millis(500), projectile_body: Body::Object(object::Body::MultiArrow), projectile_light: None, + projectile_gravity: Some(Gravity(0.2)), + initial_projectile_speed: 100.0, + max_projectile_speed: 500.0, }, ], Dagger(_) => vec![ @@ -202,6 +212,7 @@ impl Tool { buildup_duration: Duration::from_millis(100), recover_duration: Duration::from_millis(400), base_healthchange: (-50.0 * self.base_power()) as i32, + knockback: 0.0, range: 3.5, max_angle: 20.0, }, @@ -220,6 +231,7 @@ impl Tool { buildup_duration: Duration::from_millis(0), recover_duration: Duration::from_millis(300), base_healthchange: (-10.0 * self.base_power()) as i32, + knockback: 0.0, range: 5.0, max_angle: 20.0, }, @@ -228,6 +240,7 @@ impl Tool { buildup_duration: Duration::from_millis(0), recover_duration: Duration::from_millis(1000), base_healthchange: (150.0 * self.base_power()) as i32, + knockback: 0.0, range: 100.0, max_angle: 90.0, }, @@ -239,6 +252,7 @@ impl Tool { buildup_duration: Duration::from_millis(0), recover_duration: Duration::from_millis(300), base_healthchange: (-10.0 * self.base_power()) as i32, + knockback: 0.0, range: 5.0, max_angle: 20.0, }, @@ -247,6 +261,7 @@ impl Tool { buildup_duration: Duration::from_millis(0), recover_duration: Duration::from_millis(1000), base_healthchange: (350.0 * self.base_power()) as i32, + knockback: 0.0, range: 100.0, max_angle: 90.0, }, @@ -258,6 +273,7 @@ impl Tool { buildup_duration: Duration::from_millis(100), recover_duration: Duration::from_millis(300), base_healthchange: (-40.0 * self.base_power()) as i32, + knockback: 0.0, range: 3.5, max_angle: 20.0, }, @@ -284,6 +300,7 @@ impl Tool { }), projectile_gravity: None, + projectile_speed: 100.0, }, BasicRanged { energy_cost: 400, @@ -314,6 +331,7 @@ impl Tool { }), projectile_gravity: None, + projectile_speed: 100.0, }, ] } @@ -324,11 +342,48 @@ impl Tool { buildup_duration: Duration::from_millis(100), recover_duration: Duration::from_millis(400), base_healthchange: (-40.0 * self.base_power()) as i32, + knockback: 0.0, range: 3.0, max_angle: 120.0, }, BasicBlock, ], + NpcWeapon(kind) => { + if kind == "StoneGolemsFist" { + vec![ + BasicMelee { + energy_cost: 0, + buildup_duration: Duration::from_millis(500), + recover_duration: Duration::from_millis(250), + knockback: 25.0, + base_healthchange: -200, + range: 5.0, + max_angle: 120.0, + }, + GroundShockwave { + energy_cost: 0, + buildup_duration: Duration::from_millis(500), + recover_duration: Duration::from_millis(1000), + damage: 500, + knockback: -40.0, + shockwave_angle: 90.0, + shockwave_speed: 20.0, + shockwave_duration: Duration::from_millis(2000), + requires_ground: true, + }, + ] + } else { + vec![BasicMelee { + energy_cost: 0, + buildup_duration: Duration::from_millis(100), + recover_duration: Duration::from_millis(300), + base_healthchange: -10, + knockback: 0.0, + range: 1.0, + max_angle: 30.0, + }] + } + }, Debug(kind) => { if kind == "Boost" { vec![ @@ -361,6 +416,7 @@ impl Tool { ..Default::default() }), projectile_gravity: None, + projectile_speed: 100.0, }, ] } else { @@ -372,6 +428,7 @@ impl Tool { buildup_duration: Duration::from_millis(0), recover_duration: Duration::from_millis(1000), base_healthchange: -20, + knockback: 0.0, range: 3.5, max_angle: 15.0, }], diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 322d5025e7..bf73199a18 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -16,6 +16,7 @@ mod misc; mod phys; mod player; pub mod projectile; +pub mod shockwave; pub mod skills; mod stats; pub mod visual; @@ -51,6 +52,7 @@ pub use misc::Object; pub use phys::{Collider, ForceUpdate, Gravity, Mass, Ori, PhysicsState, Pos, Scale, Sticky, Vel}; pub use player::{Player, MAX_MOUNT_RANGE_SQR}; pub use projectile::Projectile; +pub use shockwave::Shockwave; pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; pub use stats::{Exp, HealthChange, HealthSource, Level, Stats}; pub use visual::{LightAnimation, LightEmitter}; diff --git a/common/src/comp/shockwave.rs b/common/src/comp/shockwave.rs new file mode 100644 index 0000000000..e53d611716 --- /dev/null +++ b/common/src/comp/shockwave.rs @@ -0,0 +1,36 @@ +use crate::sync::Uid; +use serde::{Deserialize, Serialize}; +use specs::{Component, FlaggedStorage}; +use specs_idvs::IdvStorage; +use std::time::Duration; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Properties { + pub angle: f32, + pub speed: f32, + pub damage: u32, + pub knockback: f32, + pub requires_ground: bool, + pub duration: Duration, + pub owner: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Shockwave { + pub properties: Properties, + #[serde(skip)] + /// Time that the shockwave was created at + /// Used to calculate shockwave propagation + /// Deserialized from the network as `None` + pub creation: Option, +} + +impl Component for Shockwave { + type Storage = FlaggedStorage>; +} + +impl std::ops::Deref for Shockwave { + type Target = Properties; + + fn deref(&self) -> &Properties { &self.properties } +} diff --git a/common/src/event.rs b/common/src/event.rs index 7098522e93..f5bd89c79d 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -1,5 +1,8 @@ use crate::{character::CharacterId, comp, sync::Uid, util::Dir}; -use comp::item::{Item, Reagent}; +use comp::{ + item::{Item, Reagent}, + Ori, Pos, +}; use parking_lot::Mutex; use specs::Entity as EcsEntity; use std::{collections::VecDeque, ops::DerefMut}; @@ -8,8 +11,11 @@ use vek::*; pub enum LocalEvent { /// Applies upward force to entity's `Vel` Jump(EcsEntity), - /// Applies the `force` to `entity`'s `Vel` - ApplyForce { entity: EcsEntity, force: Vec3 }, + /// Applies the `impulse` to `entity`'s `Vel` + ApplyImpulse { + entity: EcsEntity, + impulse: Vec3, + }, /// Applies leaping force to `entity`'s `Vel` away from `wall_dir` direction WallLeap { entity: EcsEntity, @@ -46,6 +52,16 @@ pub enum ServerEvent { light: Option, projectile: comp::Projectile, gravity: Option, + speed: f32, + }, + Shockwave { + properties: comp::shockwave::Properties, + pos: Pos, + ori: Ori, + }, + Knockback { + entity: EcsEntity, + impulse: Vec3, }, LandOnGround { entity: EcsEntity, diff --git a/common/src/loadout_builder.rs b/common/src/loadout_builder.rs index 24d41c39ab..7120b48f8a 100644 --- a/common/src/loadout_builder.rs +++ b/common/src/loadout_builder.rs @@ -1,4 +1,9 @@ -use crate::comp::{item::Item, Body, CharacterAbility, ItemConfig, Loadout}; +use crate::comp::{ + golem, + item::{Item, ItemKind}, + Alignment, Body, CharacterAbility, ItemConfig, Loadout, +}; +use rand::Rng; use std::time::Duration; /// Builder for character Loadouts, containing weapon and armour items belonging @@ -60,6 +65,151 @@ impl LoadoutBuilder { ))) } + /// Builds loadout of creature when spawned + pub fn build_loadout(body: Body, alignment: Alignment, mut main_tool: Option) -> Self { + #![allow(clippy::single_match)] // For when this is done to more than just golems. + match body { + Body::Golem(golem) => match golem.species { + golem::Species::StoneGolem => { + main_tool = Some(Item::new_from_asset_expect( + "common.items.npc_weapons.npcweapon.stone_golems_fist", + )); + }, + }, + _ => {}, + }; + + let active_item = if let Some(ItemKind::Tool(tool)) = main_tool.as_ref().map(|i| i.kind()) { + let mut abilities = tool.get_abilities(); + let mut ability_drain = abilities.drain(..); + + main_tool.map(|item| ItemConfig { + item, + ability1: ability_drain.next(), + ability2: ability_drain.next(), + ability3: ability_drain.next(), + block_ability: None, + dodge_ability: Some(CharacterAbility::Roll), + }) + } else { + Some(ItemConfig { + // We need the empty item so npcs can attack + item: Item::new_from_asset_expect("common.items.weapons.empty.empty"), + ability1: Some(CharacterAbility::BasicMelee { + energy_cost: 0, + buildup_duration: Duration::from_millis(0), + recover_duration: Duration::from_millis(400), + base_healthchange: -40, + knockback: 0.0, + range: 3.5, + max_angle: 15.0, + }), + ability2: None, + ability3: None, + block_ability: None, + dodge_ability: None, + }) + }; + + let loadout = match body { + Body::Humanoid(_) => match alignment { + Alignment::Npc => Loadout { + active_item, + second_item: None, + shoulder: None, + chest: Some(Item::new_from_asset_expect( + match rand::thread_rng().gen_range(0, 10) { + 0 => "common.items.armor.chest.worker_green_0", + 1 => "common.items.armor.chest.worker_green_1", + 2 => "common.items.armor.chest.worker_red_0", + 3 => "common.items.armor.chest.worker_red_1", + 4 => "common.items.armor.chest.worker_purple_0", + 5 => "common.items.armor.chest.worker_purple_1", + 6 => "common.items.armor.chest.worker_yellow_0", + 7 => "common.items.armor.chest.worker_yellow_1", + 8 => "common.items.armor.chest.worker_orange_0", + _ => "common.items.armor.chest.worker_orange_1", + }, + )), + belt: Some(Item::new_from_asset_expect( + "common.items.armor.belt.leather_0", + )), + hand: None, + pants: Some(Item::new_from_asset_expect( + "common.items.armor.pants.worker_blue_0", + )), + foot: Some(Item::new_from_asset_expect( + match rand::thread_rng().gen_range(0, 2) { + 0 => "common.items.armor.foot.leather_0", + _ => "common.items.armor.starter.sandals_0", + }, + )), + back: None, + ring: None, + neck: None, + lantern: None, + glider: None, + head: None, + tabard: None, + }, + Alignment::Enemy => Loadout { + active_item, + second_item: None, + shoulder: Some(Item::new_from_asset_expect( + "common.items.armor.shoulder.cultist_shoulder_purple", + )), + chest: Some(Item::new_from_asset_expect( + "common.items.armor.chest.cultist_chest_purple", + )), + belt: Some(Item::new_from_asset_expect( + "common.items.armor.belt.cultist_belt", + )), + hand: Some(Item::new_from_asset_expect( + "common.items.armor.hand.cultist_hands_purple", + )), + pants: Some(Item::new_from_asset_expect( + "common.items.armor.pants.cultist_legs_purple", + )), + foot: Some(Item::new_from_asset_expect( + "common.items.armor.foot.cultist_boots", + )), + back: Some(Item::new_from_asset_expect( + "common.items.armor.back.dungeon_purple-0", + )), + ring: None, + neck: None, + lantern: Some(Item::new_from_asset_expect("common.items.lantern.black_0")), + glider: None, + head: None, + tabard: None, + }, + _ => LoadoutBuilder::animal(body).build(), + }, + Body::Golem(golem) => match golem.species { + golem::Species::StoneGolem => Loadout { + active_item, + second_item: None, + shoulder: None, + chest: None, + belt: None, + hand: None, + pants: None, + foot: None, + back: None, + ring: None, + neck: None, + lantern: None, + glider: None, + head: None, + tabard: None, + }, + }, + _ => LoadoutBuilder::animal(body).build(), + }; + + Self(loadout) + } + /// Default animal configuration pub fn animal(body: Body) -> Self { Self(Loadout { @@ -70,6 +220,7 @@ impl LoadoutBuilder { buildup_duration: Duration::from_millis(600), recover_duration: Duration::from_millis(100), base_healthchange: -(body.base_dmg() as i32), + knockback: 0.0, range: body.base_range(), max_angle: 20.0, }), diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index 726d67321a..244733612b 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -29,6 +29,7 @@ sum_type! { Pos(comp::Pos), Vel(comp::Vel), Ori(comp::Ori), + Shockwave(comp::Shockwave), } } // Automatically derive From for EcsCompPhantom @@ -56,6 +57,7 @@ sum_type! { Pos(PhantomData), Vel(PhantomData), Ori(PhantomData), + Shockwave(PhantomData), } } impl sync::CompPacket for EcsCompPacket { @@ -83,6 +85,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Shockwave(comp) => sync::handle_insert(comp, entity, world), } } @@ -108,6 +111,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Shockwave(comp) => sync::handle_modify(comp, entity, world), } } @@ -137,6 +141,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Pos(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Vel(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Ori(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::Shockwave(_) => sync::handle_remove::(entity, world), } } } diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 18d4ee6f27..ce86e46c73 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -254,6 +254,7 @@ pub enum ServerMsg { Notification(Notification), SetViewDistance(u32), Outcomes(Vec), + Knockback(Vec3), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/common/src/state.rs b/common/src/state.rs index 78f34068fb..3e8e1e62a2 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -126,6 +126,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); // Register components send from clients -> server ecs.register::(); @@ -389,11 +390,9 @@ impl State { vel.0.z = HUMANOID_JUMP_ACCEL; } }, - LocalEvent::ApplyForce { entity, force } => { - // TODO: this sets the velocity directly to the value of `force`, consider - // renaming the event or changing the behavior + LocalEvent::ApplyImpulse { entity, impulse } => { if let Some(vel) = velocities.get_mut(entity) { - vel.0 = force; + vel.0 = impulse; } }, LocalEvent::WallLeap { entity, wall_dir } => { diff --git a/common/src/states/basic_melee.rs b/common/src/states/basic_melee.rs index caead1a54c..e234668661 100644 --- a/common/src/states/basic_melee.rs +++ b/common/src/states/basic_melee.rs @@ -14,6 +14,8 @@ pub struct Data { pub recover_duration: Duration, /// Base damage (negative) or healing (positive) pub base_healthchange: i32, + /// Knockback + pub knockback: f32, /// Max range pub range: f32, /// Max angle (45.0 will give you a 90.0 angle window) @@ -38,6 +40,7 @@ impl CharacterBehavior for Data { .unwrap_or_default(), recover_duration: self.recover_duration, base_healthchange: self.base_healthchange, + knockback: self.knockback, range: self.range, max_angle: self.max_angle, exhausted: false, @@ -50,13 +53,14 @@ impl CharacterBehavior for Data { max_angle: self.max_angle.to_radians(), applied: false, hit_count: 0, - knockback: 0.0, + knockback: self.knockback, }); update.character = CharacterState::BasicMelee(Data { buildup_duration: self.buildup_duration, recover_duration: self.recover_duration, base_healthchange: self.base_healthchange, + knockback: self.knockback, range: self.range, max_angle: self.max_angle, exhausted: true, @@ -70,6 +74,7 @@ impl CharacterBehavior for Data { .checked_sub(Duration::from_secs_f32(data.dt.0)) .unwrap_or_default(), base_healthchange: self.base_healthchange, + knockback: self.knockback, range: self.range, max_angle: self.max_angle, exhausted: true, diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs index dd7f0ff820..4b0c928c64 100644 --- a/common/src/states/basic_ranged.rs +++ b/common/src/states/basic_ranged.rs @@ -21,6 +21,7 @@ pub struct Data { pub projectile_body: Body, pub projectile_light: Option, pub projectile_gravity: Option, + pub projectile_speed: f32, /// Whether the attack fired already pub exhausted: bool, } @@ -49,6 +50,7 @@ impl CharacterBehavior for Data { projectile_body: self.projectile_body, projectile_light: self.projectile_light, projectile_gravity: self.projectile_gravity, + projectile_speed: self.projectile_speed, exhausted: false, }); } else if !self.exhausted { @@ -62,6 +64,7 @@ impl CharacterBehavior for Data { projectile, light: self.projectile_light, gravity: self.projectile_gravity, + speed: self.projectile_speed, }); update.character = CharacterState::BasicRanged(Data { @@ -73,6 +76,7 @@ impl CharacterBehavior for Data { projectile_body: self.projectile_body, projectile_light: self.projectile_light, projectile_gravity: self.projectile_gravity, + projectile_speed: self.projectile_speed, exhausted: true, }); } else if self.recover_duration != Duration::default() { @@ -89,6 +93,7 @@ impl CharacterBehavior for Data { projectile_body: self.projectile_body, projectile_light: self.projectile_light, projectile_gravity: self.projectile_gravity, + projectile_speed: self.projectile_speed, exhausted: true, }); return update; diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs index 1d9d1e8f72..8f501785d3 100644 --- a/common/src/states/charged_ranged.rs +++ b/common/src/states/charged_ranged.rs @@ -10,9 +10,6 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::time::Duration; -const MAX_GRAVITY: f32 = 0.2; -const MIN_GRAVITY: f32 = 0.05; - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Data { /// Whether the attack fired already @@ -38,6 +35,9 @@ pub struct Data { /// Projectile information pub projectile_body: Body, pub projectile_light: Option, + pub projectile_gravity: Option, + pub initial_projectile_speed: f32, + pub max_projectile_speed: f32, } impl CharacterBehavior for Data { @@ -65,6 +65,9 @@ impl CharacterBehavior for Data { recover_duration: self.recover_duration, projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_gravity: self.projectile_gravity, + initial_projectile_speed: self.initial_projectile_speed, + max_projectile_speed: self.max_projectile_speed, }); } else if data.inputs.secondary.is_pressed() && self.charge_timer < self.charge_duration @@ -87,6 +90,9 @@ impl CharacterBehavior for Data { recover_duration: self.recover_duration, projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_gravity: self.projectile_gravity, + initial_projectile_speed: self.initial_projectile_speed, + max_projectile_speed: self.max_projectile_speed, }); // Consumes energy if there's enough left and RMB is held down @@ -109,6 +115,9 @@ impl CharacterBehavior for Data { recover_duration: self.recover_duration, projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_gravity: self.projectile_gravity, + initial_projectile_speed: self.initial_projectile_speed, + max_projectile_speed: self.max_projectile_speed, }); // Consumes energy if there's enough left and RMB is held down @@ -145,9 +154,9 @@ impl CharacterBehavior for Data { body: self.projectile_body, projectile, light: self.projectile_light, - gravity: Some(Gravity( - MAX_GRAVITY - charge_amount * (MAX_GRAVITY - MIN_GRAVITY), - )), + gravity: self.projectile_gravity, + speed: self.initial_projectile_speed + + charge_amount * (self.max_projectile_speed - self.initial_projectile_speed), }); update.character = CharacterState::ChargedRanged(Data { @@ -163,6 +172,9 @@ impl CharacterBehavior for Data { recover_duration: self.recover_duration, projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_gravity: self.projectile_gravity, + initial_projectile_speed: self.initial_projectile_speed, + max_projectile_speed: self.max_projectile_speed, }); } else if self.recover_duration != Duration::default() { // Recovery @@ -182,6 +194,9 @@ impl CharacterBehavior for Data { .unwrap_or_default(), projectile_body: self.projectile_body, projectile_light: self.projectile_light, + projectile_gravity: self.projectile_gravity, + initial_projectile_speed: self.initial_projectile_speed, + max_projectile_speed: self.max_projectile_speed, }); } else { // Done diff --git a/common/src/states/ground_shockwave.rs b/common/src/states/ground_shockwave.rs new file mode 100644 index 0000000000..81038fc58b --- /dev/null +++ b/common/src/states/ground_shockwave.rs @@ -0,0 +1,107 @@ +use crate::{ + comp::{shockwave, Attacking, CharacterState, StateUpdate}, + event::ServerEvent, + states::utils::*, + sys::character_behavior::*, +}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + /// Whether the attack can deal more damage + pub exhausted: bool, + /// How long until state should deal damage + pub buildup_duration: Duration, + /// How long the state has until exiting + pub recover_duration: Duration, + /// Base damage + pub damage: u32, + /// Knockback + pub knockback: f32, + /// Angle of the shockwave + pub shockwave_angle: f32, + /// Speed of the shockwave + pub shockwave_speed: f32, + /// How long the shockwave travels for + pub shockwave_duration: Duration, + /// Whether the shockwave requires the target to be on the ground + pub requires_ground: bool, +} + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + + handle_move(data, &mut update, 0.05); + + if self.buildup_duration != Duration::default() { + // Build up + update.character = CharacterState::GroundShockwave(Data { + exhausted: self.exhausted, + buildup_duration: self + .buildup_duration + .checked_sub(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + recover_duration: self.recover_duration, + damage: self.damage, + knockback: self.knockback, + shockwave_angle: self.shockwave_angle, + shockwave_speed: self.shockwave_speed, + shockwave_duration: self.shockwave_duration, + requires_ground: self.requires_ground, + }); + } else if !self.exhausted { + // Attack + let properties = shockwave::Properties { + angle: self.shockwave_angle, + speed: self.shockwave_speed, + duration: self.shockwave_duration, + damage: self.damage, + knockback: self.knockback, + requires_ground: self.requires_ground, + owner: Some(*data.uid), + }; + update.server_events.push_front(ServerEvent::Shockwave { + properties, + pos: *data.pos, + ori: *data.ori, + }); + + update.character = CharacterState::GroundShockwave(Data { + exhausted: true, + buildup_duration: self.buildup_duration, + recover_duration: self.recover_duration, + damage: self.damage, + knockback: self.knockback, + shockwave_angle: self.shockwave_angle, + shockwave_speed: self.shockwave_speed, + shockwave_duration: self.shockwave_duration, + requires_ground: self.requires_ground, + }); + } else if self.recover_duration != Duration::default() { + // Recovery + update.character = CharacterState::GroundShockwave(Data { + exhausted: self.exhausted, + buildup_duration: self.buildup_duration, + recover_duration: self + .recover_duration + .checked_sub(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + damage: self.damage, + knockback: self.knockback, + shockwave_angle: self.shockwave_angle, + shockwave_speed: self.shockwave_speed, + shockwave_duration: self.shockwave_duration, + requires_ground: self.requires_ground, + }); + } else { + // Done + update.character = CharacterState::Wielding; + // Make sure attack component is removed + data.updater.remove::(data.entity); + } + + update + } +} diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 5c620b8c6b..435d35683c 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -9,6 +9,7 @@ pub mod dash_melee; pub mod equipping; pub mod glide; pub mod glide_wield; +pub mod ground_shockwave; pub mod idle; pub mod leap_melee; pub mod roll; diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 239905cad0..aae09363b9 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -268,6 +268,7 @@ impl<'a> System<'a> for Sys { Melee, RangedPowerup, Staff, + StoneGolemBoss, } let tactic = match loadout.active_item.as_ref().and_then(|ic| { @@ -279,6 +280,13 @@ impl<'a> System<'a> for Sys { }) { Some(ToolKind::Bow(_)) => Tactic::RangedPowerup, Some(ToolKind::Staff(_)) => Tactic::Staff, + Some(ToolKind::NpcWeapon(kind)) => { + if kind == "StoneGolemsFist" { + Tactic::StoneGolemBoss + } else { + Tactic::Melee + } + }, _ => Tactic::Melee, }; @@ -355,7 +363,9 @@ impl<'a> System<'a> for Sys { * 0.1; match tactic { - Tactic::Melee | Tactic::Staff => inputs.primary.set_state(true), + Tactic::Melee | Tactic::Staff | Tactic::StoneGolemBoss => { + inputs.primary.set_state(true) + }, Tactic::RangedPowerup => inputs.roll.set_state(true), } } else if dist_sqrd < MAX_CHASE_DIST.powf(2.0) @@ -385,6 +395,13 @@ impl<'a> System<'a> for Sys { } inputs.secondary.set_state(true); + } else if let Tactic::StoneGolemBoss = tactic { + if *powerup > 5.0 { + inputs.secondary.set_state(true); + *powerup = 0.0; + } else { + *powerup += dt.0; + } } } diff --git a/common/src/sys/character_behavior.rs b/common/src/sys/character_behavior.rs index 95feb503be..2c4b38186d 100644 --- a/common/src/sys/character_behavior.rs +++ b/common/src/sys/character_behavior.rs @@ -258,6 +258,7 @@ impl<'a> System<'a> for Sys { CharacterState::LeapMelee(data) => data.handle_event(&j, action), CharacterState::SpinMelee(data) => data.handle_event(&j, action), CharacterState::ChargedRanged(data) => data.handle_event(&j, action), + CharacterState::GroundShockwave(data) => data.handle_event(&j, action), }; local_emitter.append(&mut state_update.local_events); server_emitter.append(&mut state_update.server_events); @@ -286,6 +287,7 @@ impl<'a> System<'a> for Sys { CharacterState::LeapMelee(data) => data.behavior(&j), CharacterState::SpinMelee(data) => data.behavior(&j), CharacterState::ChargedRanged(data) => data.behavior(&j), + CharacterState::GroundShockwave(data) => data.behavior(&j), }; local_emitter.append(&mut state_update.local_events); diff --git a/common/src/sys/combat.rs b/common/src/sys/combat.rs index 2de2ea4058..7e022f1463 100644 --- a/common/src/sys/combat.rs +++ b/common/src/sys/combat.rs @@ -33,8 +33,8 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Stats>, ReadStorage<'a, Loadout>, ReadStorage<'a, group::Group>, + ReadStorage<'a, CharacterState>, WriteStorage<'a, Attacking>, - WriteStorage<'a, CharacterState>, ); fn run( @@ -52,14 +52,14 @@ impl<'a> System<'a> for Sys { stats, loadouts, groups, - mut attacking_storage, character_states, + mut attacking_storage, ): Self::SystemData, ) { let start_time = std::time::Instant::now(); span!(_guard, "run", "combat::Sys::run"); let mut server_emitter = server_bus.emitter(); - let mut local_emitter = local_bus.emitter(); + let mut _local_emitter = local_bus.emitter(); // Attacks for (entity, uid, pos, ori, scale_maybe, attack) in ( &entities, @@ -152,11 +152,10 @@ impl<'a> System<'a> for Sys { }, }); } - - if attack.knockback != 0.0 { - local_emitter.emit(LocalEvent::ApplyForce { + if attack.knockback != 0.0 && damage.healthchange != 0.0 { + server_emitter.emit(ServerEvent::Knockback { entity: b, - force: attack.knockback + impulse: attack.knockback * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5), }); } diff --git a/common/src/sys/mod.rs b/common/src/sys/mod.rs index 832dc37d63..c5716b6211 100644 --- a/common/src/sys/mod.rs +++ b/common/src/sys/mod.rs @@ -5,6 +5,7 @@ pub mod controller; mod mount; pub mod phys; mod projectile; +mod shockwave; mod stats; // External @@ -18,6 +19,7 @@ pub const CONTROLLER_SYS: &str = "controller_sys"; pub const MOUNT_SYS: &str = "mount_sys"; pub const PHYS_SYS: &str = "phys_sys"; pub const PROJECTILE_SYS: &str = "projectile_sys"; +pub const SHOCKWAVE_SYS: &str = "shockwave_sys"; pub const STATS_SYS: &str = "stats_sys"; pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { @@ -30,5 +32,6 @@ pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(stats::Sys, STATS_SYS, &[]); dispatch_builder.add(phys::Sys, PHYS_SYS, &[CONTROLLER_SYS, MOUNT_SYS, STATS_SYS]); dispatch_builder.add(projectile::Sys, PROJECTILE_SYS, &[PHYS_SYS]); + dispatch_builder.add(shockwave::Sys, SHOCKWAVE_SYS, &[PHYS_SYS]); dispatch_builder.add(combat::Sys, COMBAT_SYS, &[PROJECTILE_SYS]); } diff --git a/common/src/sys/phys.rs b/common/src/sys/phys.rs index 36a5ed75f6..15b3bb0c54 100644 --- a/common/src/sys/phys.rs +++ b/common/src/sys/phys.rs @@ -173,7 +173,7 @@ impl<'a> System<'a> for Sys { mass_other, collider_other, _, - group, + group_b, ) in ( &entities, &uids, @@ -186,7 +186,7 @@ impl<'a> System<'a> for Sys { ) .join() { - if entity == entity_other || (ignore_group.is_some() && ignore_group == group) { + if entity == entity_other || (ignore_group.is_some() && ignore_group == group_b) { continue; } diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs index a662eeb0f4..c44822c9a3 100644 --- a/common/src/sys/projectile.rs +++ b/common/src/sys/projectile.rs @@ -117,9 +117,9 @@ impl<'a> System<'a> for Sys { if let Some(entity) = uid_allocator.retrieve_entity_internal(other.into()) { - local_emitter.emit(LocalEvent::ApplyForce { + local_emitter.emit(LocalEvent::ApplyImpulse { entity, - force: knockback + impulse: knockback * *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5), }); } diff --git a/common/src/sys/shockwave.rs b/common/src/sys/shockwave.rs new file mode 100644 index 0000000000..ced2d336e1 --- /dev/null +++ b/common/src/sys/shockwave.rs @@ -0,0 +1,339 @@ +use crate::{ + comp::{ + group, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource, Last, + Loadout, Ori, PhysicsState, Pos, Scale, Shockwave, Stats, + }, + event::{EventBus, LocalEvent, ServerEvent}, + state::{DeltaTime, Time}, + sync::{Uid, UidAllocator}, + util::Dir, +}; +use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage}; +use vek::*; + +pub const BLOCK_ANGLE: f32 = 180.0; + +/// This system is responsible for handling accepted inputs like moving or +/// attacking +pub struct Sys; +impl<'a> System<'a> for Sys { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + Read<'a, EventBus>, + Read<'a, EventBus>, + Read<'a, Time>, + Read<'a, DeltaTime>, + Read<'a, UidAllocator>, + ReadStorage<'a, Uid>, + ReadStorage<'a, Pos>, + ReadStorage<'a, Last>, + ReadStorage<'a, Ori>, + ReadStorage<'a, Scale>, + ReadStorage<'a, Body>, + ReadStorage<'a, Stats>, + ReadStorage<'a, Loadout>, + ReadStorage<'a, group::Group>, + ReadStorage<'a, CharacterState>, + ReadStorage<'a, PhysicsState>, + WriteStorage<'a, Shockwave>, + ); + + fn run( + &mut self, + ( + entities, + server_bus, + local_bus, + time, + dt, + uid_allocator, + uids, + positions, + last_positions, + orientations, + scales, + bodies, + stats, + loadouts, + groups, + character_states, + physics_states, + mut shockwaves, + ): Self::SystemData, + ) { + let mut server_emitter = server_bus.emitter(); + let _local_emitter = local_bus.emitter(); + + let time = time.0; + let dt = dt.0; + + // Shockwaves + for (entity, uid, pos, ori, shockwave) in + (&entities, &uids, &positions, &orientations, &shockwaves).join() + { + let creation_time = match shockwave.creation { + Some(time) => time, + // Skip newly created shockwaves + None => continue, + }; + + let end_time = creation_time + shockwave.duration.as_secs_f64(); + + // If shockwave is out of time emit destroy event but still continue since it + // may have traveled and produced effects a bit before reaching it's + // end point + if time > end_time { + server_emitter.emit(ServerEvent::Destroy { + entity, + cause: HealthSource::World, + }); + continue; + } + + // Determine area that was covered by the shockwave in the last tick + let time_since_creation = (time - creation_time) as f32; + let frame_start_dist = (shockwave.speed * (time_since_creation - dt)).max(0.0); + let frame_end_dist = (shockwave.speed * time_since_creation).max(frame_start_dist); + let pos2 = Vec2::from(pos.0); + + // From one frame to the next a shockwave travels over a strip of an arc + // This is used for collision detection + let arc_strip = ArcStrip { + origin: pos2, + // TODO: make sure this is not Vec2::new(0.0, 0.0) + dir: ori.0.xy(), + angle: shockwave.angle, + start: frame_start_dist, + end: frame_end_dist, + }; + + // Group to ignore collisions with + // Might make this more nuanced if shockwaves are used for non damage effects + let group = shockwave + .owner + .and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into())) + .and_then(|e| groups.get(e)); + + // Go through all other effectable entities + for ( + b, + uid_b, + pos_b, + last_pos_b_maybe, + ori_b, + scale_b_maybe, + character_b, + stats_b, + body_b, + physics_state_b, + ) in ( + &entities, + &uids, + &positions, + // TODO: make sure that these are maintained on the client and remove `.maybe()` + last_positions.maybe(), + &orientations, + scales.maybe(), + character_states.maybe(), + &stats, + &bodies, + &physics_states, + ) + .join() + { + // 2D versions + let pos_b2 = pos_b.0.xy(); + let last_pos_b2_maybe = last_pos_b_maybe.map(|p| (p.0).0.xy()); + + // Scales + let scale_b = scale_b_maybe.map_or(1.0, |s| s.0); + let rad_b = body_b.radius() * scale_b; + + // Angle checks + let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z); + let max_angle = 15.0_f32.to_radians(); + + // See if entities are in the same group + let same_group = group + .map(|group_a| Some(group_a) == groups.get(b)) + .unwrap_or(Some(*uid_b) == shockwave.owner); + + // Check if it is a hit + let hit = entity != b + && !stats_b.is_dead + // Collision shapes + && { + // TODO: write code to collide rect with the arc strip so that we can do + // more complete collision detection for rapidly moving entities + arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b)) || last_pos_b2_maybe.map_or(false, |pos| { + arc_strip.collides_with_circle(Disk::new(pos, rad_b)) + }) + } + && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle + && (!shockwave.requires_ground || physics_state_b.on_ground) + && !same_group; + + if hit { + let mut damage = Damage { + healthchange: -(shockwave.damage as f32), + source: DamageSource::Shockwave, + }; + + let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false) + && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0; + + if let Some(loadout) = loadouts.get(b) { + damage.modify_damage(block, loadout); + } + + if damage.healthchange != 0.0 { + let cause = if damage.healthchange < 0.0 { + HealthSource::Attack { + by: shockwave.owner.unwrap_or(*uid), + } + } else { + HealthSource::Healing { + by: Some(shockwave.owner.unwrap_or(*uid)), + } + }; + server_emitter.emit(ServerEvent::Damage { + uid: *uid_b, + change: HealthChange { + amount: damage.healthchange as i32, + cause, + }, + }); + } + if shockwave.knockback != 0.0 && damage.healthchange != 0.0 { + let impulse = if shockwave.knockback < 0.0 { + shockwave.knockback + * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, -1.0)), 0.85) + } else { + shockwave.knockback + * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5) + }; + server_emitter.emit(ServerEvent::Knockback { entity: b, impulse }); + } + } + } + } + + // Set start time on new shockwaves + // This change doesn't need to be recorded as it is not sent to the client + shockwaves.set_event_emission(false); + (&mut shockwaves).join().for_each(|shockwave| { + if shockwave.creation.is_none() { + shockwave.creation = Some(time); + } + }); + shockwaves.set_event_emission(true); + } +} + +#[derive(Clone, Copy)] +struct ArcStrip { + origin: Vec2, + /// Normalizable direction + dir: Vec2, + /// Angle in degrees + angle: f32, + /// Start radius + start: f32, + /// End radius + end: f32, +} + +impl ArcStrip { + fn collides_with_circle(self, d: Disk) -> bool { + // Quit if aabb's don't collide + if (self.origin.x - d.center.x).abs() > self.end + d.radius + || (self.origin.y - d.center.y).abs() > self.end + d.radius + { + return false; + } + + let dist = self.origin.distance(d.center); + let half_angle = self.angle.to_radians() / 2.0; + + if dist > self.end + d.radius || dist + d.radius < self.start { + // Completely inside or outside full ring + return false; + } + + let inside_edge = Disk::new(self.origin, self.start); + let outside_edge = Disk::new(self.origin, self.end); + let inner_corner_in_circle = || { + let midpoint = self.dir.normalized() * self.start; + d.contains_point(midpoint.rotated_z(half_angle) + self.origin) + || d.contains_point(midpoint.rotated_z(-half_angle) + self.origin) + }; + let arc_segment_in_circle = || { + let midpoint = self.dir.normalized(); + let segment_in_circle = |angle| { + let dir = midpoint.rotated_z(angle); + let side = LineSegment2 { + start: dir * self.start + self.origin, + end: dir * self.end + self.origin, + }; + d.contains_point(side.projected_point(d.center)) + }; + segment_in_circle(half_angle) || segment_in_circle(-half_angle) + }; + + if dist > self.end { + // Circle center is outside ring + // Check intersection with line segments + arc_segment_in_circle() || { + // Check angle of intersection points on outside edge of ring + let (p1, p2) = intersection_points(outside_edge, d, dist); + self.dir.angle_between(p1 - self.origin) < half_angle + || self.dir.angle_between(p2 - self.origin) < half_angle + } + } else if dist < self.start { + // Circle center is inside ring + // Check angle of intersection points on inside edge of ring + // Check if circle contains one of the inner points of the arc + inner_corner_in_circle() + || ( + // Check that the circles aren't identical + inside_edge != d && { + let (p1, p2) = intersection_points(inside_edge, d, dist); + self.dir.angle_between(p1 - self.origin) < half_angle + || self.dir.angle_between(p2 - self.origin) < half_angle + } + ) + } else if d.radius > dist { + // Circle center inside ring + // but center of ring is inside the circle so we can't calculate the angle + inner_corner_in_circle() + } else { + // Circle center inside ring + // Calculate extra angle to account for circle radius + let extra_angle = (d.radius / dist).asin(); + self.dir.angle_between(d.center - self.origin) < half_angle + extra_angle + } + } +} + +// Assumes an intersection is occuring at 2 points +// Uses precalculated distance +// https://www.xarg.org/2016/07/calculate-the-intersection-points-of-two-circles/ +fn intersection_points( + disk1: Disk, + disk2: Disk, + dist: f32, +) -> (Vec2, Vec2) { + let e = (disk2.center - disk1.center) / dist; + + let x = (disk1.radius.powi(2) - disk2.radius.powi(2) + dist.powi(2)) / (2.0 * dist); + let y = (disk1.radius.powi(2) - x.powi(2)).sqrt(); + + let pxe = disk1.center + x * e; + let eyx = e.yx(); + + let p1 = pxe + Vec2::new(-y, y) * eyx; + let p2 = pxe + Vec2::new(y, -y) * eyx; + + (p1, p2) +} diff --git a/common/src/sys/stats.rs b/common/src/sys/stats.rs index eafa9c8e3d..e7b26d9436 100644 --- a/common/src/sys/stats.rs +++ b/common/src/sys/stats.rs @@ -112,7 +112,8 @@ impl<'a> System<'a> for Sys { | CharacterState::SpinMelee { .. } | CharacterState::TripleStrike { .. } | CharacterState::BasicRanged { .. } - | CharacterState::ChargedRanged { .. } => { + | CharacterState::ChargedRanged { .. } + | CharacterState::GroundShockwave { .. } => { if energy.get_unchecked().regen_rate != 0.0 { energy.get_mut_unchecked().regen_rate = 0.0 } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 7a8057a639..e1799417c6 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -582,7 +582,7 @@ fn handle_spawn( .create_npc( pos, comp::Stats::new(get_npc_name(id).into(), body), - LoadoutBuilder::animal(body).build(), + LoadoutBuilder::build_loadout(body, alignment, None).build(), body, ) .with(comp::Vel(vel)) diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 28034143a8..4c9e3d048a 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -2,8 +2,9 @@ use crate::{sys, Server, StateExt}; use common::{ character::CharacterId, comp::{ - self, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, Agent, Alignment, Body, Gravity, Item, - ItemDrop, LightEmitter, Loadout, Pos, Projectile, Scale, Stats, Vel, WaypointArea, + self, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, shockwave, Agent, Alignment, Body, Gravity, + Item, ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, Vel, + WaypointArea, }, outcome::Outcome, util::Dir, @@ -79,6 +80,7 @@ pub fn handle_create_npc( entity.build(); } +#[allow(clippy::too_many_arguments)] pub fn handle_shoot( server: &mut Server, entity: EcsEntity, @@ -87,6 +89,7 @@ pub fn handle_shoot( light: Option, projectile: Projectile, gravity: Option, + speed: f32, ) { let state = server.state_mut(); @@ -97,7 +100,7 @@ pub fn handle_shoot( .expect("Failed to fetch entity") .0; - let vel = *dir * 100.0; + let vel = *dir * speed; // Add an outcome state @@ -123,6 +126,16 @@ pub fn handle_shoot( builder.build(); } +pub fn handle_shockwave( + server: &mut Server, + properties: shockwave::Properties, + pos: Pos, + ori: Ori, +) { + let state = server.state_mut(); + state.create_shockwave(properties, pos, ori).build(); +} + pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { server .state diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 88428fc3d7..ea1cdabb6b 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -32,6 +32,18 @@ pub fn handle_damage(server: &Server, uid: Uid, change: HealthChange) { } } +pub fn handle_knockback(server: &Server, entity: EcsEntity, impulse: Vec3) { + let state = &server.state; + let mut velocities = state.ecs().write_storage::(); + if let Some(vel) = velocities.get_mut(entity) { + vel.0 = impulse; + } + let mut clients = state.ecs().write_storage::(); + if let Some(client) = clients.get_mut(entity) { + client.notify(ServerMsg::Knockback(impulse)); + } +} + /// Handle an entity dying. If it is a player, it will send a message to all /// other players. If the entity that killed it had stats, then give it exp for /// the kill. Experience given is equal to the level of the entity that was diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index d56c4fa8c4..8bbcf9926e 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -5,11 +5,11 @@ use common::{ }; use entity_creation::{ handle_create_npc, handle_create_waypoint, handle_initialize_character, - handle_loaded_character_data, handle_shoot, + handle_loaded_character_data, handle_shockwave, handle_shoot, }; use entity_manipulation::{ - handle_damage, handle_destroy, handle_explosion, handle_land_on_ground, handle_level_up, - handle_respawn, + handle_damage, handle_destroy, handle_explosion, handle_knockback, handle_land_on_ground, + handle_level_up, handle_respawn, }; use group_manip::handle_group; use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; @@ -68,7 +68,16 @@ impl Server { light, projectile, gravity, - } => handle_shoot(self, entity, dir, body, light, projectile, gravity), + speed, + } => handle_shoot(self, entity, dir, body, light, projectile, gravity, speed), + ServerEvent::Shockwave { + properties, + pos, + ori, + } => handle_shockwave(self, properties, pos, ori), + ServerEvent::Knockback { entity, impulse } => { + handle_knockback(&self, entity, impulse) + }, ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change), ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause), ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip), diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 97c3be95ec..bb577f2f1b 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -39,6 +39,13 @@ pub trait StateExt { body: comp::Body, projectile: comp::Projectile, ) -> EcsEntityBuilder; + /// Build a shockwave entity + fn create_shockwave( + &mut self, + properties: comp::shockwave::Properties, + pos: comp::Pos, + ori: comp::Ori, + ) -> EcsEntityBuilder; /// Insert common/default components for a new character joining the server fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId); /// Update the components associated with the entity's current character. @@ -134,6 +141,22 @@ impl StateExt for State { .with(comp::Sticky) } + fn create_shockwave( + &mut self, + properties: comp::shockwave::Properties, + pos: comp::Pos, + ori: comp::Ori, + ) -> EcsEntityBuilder { + self.ecs_mut() + .create_entity_synced() + .with(pos) + .with(ori) + .with(comp::Shockwave { + properties, + creation: None, + }) + } + fn initialize_character_data(&mut self, entity: EcsEntity, character_id: CharacterId) { let spawn_point = self.ecs().read_resource::().0; diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 5fb64ddd5f..ea208acd1b 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -2,7 +2,8 @@ use super::SysTimer; use common::{ comp::{ Body, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, LightEmitter, - Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel, + Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave, Stats, Sticky, + Vel, }, msg::EcsCompPacket, span, @@ -57,6 +58,7 @@ pub struct TrackedComps<'a> { pub gravity: ReadStorage<'a, Gravity>, pub loadout: ReadStorage<'a, Loadout>, pub character_state: ReadStorage<'a, CharacterState>, + pub shockwave: ReadStorage<'a, Shockwave>, } impl<'a> TrackedComps<'a> { pub fn create_entity_package( @@ -132,6 +134,11 @@ impl<'a> TrackedComps<'a> { .get(entity) .cloned() .map(|c| comps.push(c.into())); + self.shockwave + .get(entity) + .cloned() + .map(|c| comps.push(c.into())); + // Add untracked comps // Add untracked comps pos.map(|c| comps.push(c.into())); vel.map(|c| comps.push(c.into())); @@ -160,6 +167,7 @@ pub struct ReadTrackers<'a> { pub gravity: ReadExpect<'a, UpdateTracker>, pub loadout: ReadExpect<'a, UpdateTracker>, pub character_state: ReadExpect<'a, UpdateTracker>, + pub shockwave: ReadExpect<'a, UpdateTracker>, } impl<'a> ReadTrackers<'a> { pub fn create_sync_packages( @@ -197,7 +205,8 @@ impl<'a> ReadTrackers<'a> { &*self.character_state, &comps.character_state, filter, - ); + ) + .with_component(&comps.uid, &*self.shockwave, &comps.shockwave, filter); (entity_sync_package, comp_sync_package) } @@ -223,6 +232,7 @@ pub struct WriteTrackers<'a> { gravity: WriteExpect<'a, UpdateTracker>, loadout: WriteExpect<'a, UpdateTracker>, character_state: WriteExpect<'a, UpdateTracker>, + shockwave: WriteExpect<'a, UpdateTracker>, } fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { @@ -247,6 +257,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers .character_state .record_changes(&comps.character_state); + trackers.shockwave.record_changes(&comps.shockwave); // Debug how many updates are being sent /* macro_rules! log_counts { @@ -278,6 +289,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { log_counts!(gravity, "Gravitys"); log_counts!(loadout, "Loadouts"); log_counts!(character_state, "Character States"); + log_counts!(shockwave, "Shockwaves"); */ } @@ -300,6 +312,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); + world.register_tracker::(); } /// Deleted entities grouped by region diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 6ba5401ac7..4329b48550 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -1,11 +1,7 @@ use super::SysTimer; use crate::{chunk_generator::ChunkGenerator, client::Client, Tick}; use common::{ - comp::{ - self, bird_medium, - item::{self}, - Alignment, CharacterAbility, ItemConfig, Player, Pos, - }, + comp::{self, bird_medium, Alignment, CharacterAbility, Player, Pos}, event::{EventBus, ServerEvent}, generation::get_npc_name, msg::ServerMsg, @@ -124,119 +120,7 @@ impl<'a> System<'a> for Sys { // let damage = stats.level.level() as i32; TODO: Make NPC base damage // non-linearly depend on their level - let active_item = if let Some(item::ItemKind::Tool(tool)) = - main_tool.as_ref().map(|i| i.kind()) - { - let mut abilities = tool.get_abilities(); - let mut ability_drain = abilities.drain(..); - - main_tool.map(|item| comp::ItemConfig { - item, - ability1: ability_drain.next(), - ability2: ability_drain.next(), - ability3: ability_drain.next(), - block_ability: None, - dodge_ability: Some(comp::CharacterAbility::Roll), - }) - } else { - Some(ItemConfig { - // We need the empty item so npcs can attack - item: comp::Item::new_from_asset_expect("common.items.weapons.empty.empty"), - ability1: Some(CharacterAbility::BasicMelee { - energy_cost: 0, - buildup_duration: Duration::from_millis(0), - recover_duration: Duration::from_millis(400), - base_healthchange: -40, - range: 3.5, - max_angle: 15.0, - }), - ability2: None, - ability3: None, - block_ability: None, - dodge_ability: None, - }) - }; - - let mut loadout = match alignment { - comp::Alignment::Npc => comp::Loadout { - active_item, - second_item: None, - shoulder: None, - chest: Some(comp::Item::new_from_asset_expect( - match rand::thread_rng().gen_range(0, 10) { - 0 => "common.items.npc_armor.chest.worker_green_0", - 1 => "common.items.npc_armor.chest.worker_green_1", - 2 => "common.items.npc_armor.chest.worker_red_0", - 3 => "common.items.npc_armor.chest.worker_red_1", - 4 => "common.items.npc_armor.chest.worker_purple_0", - 5 => "common.items.npc_armor.chest.worker_purple_1", - 6 => "common.items.npc_armor.chest.worker_yellow_0", - 7 => "common.items.npc_armor.chest.worker_yellow_1", - 8 => "common.items.npc_armor.chest.worker_orange_0", - _ => "common.items.npc_armor.chest.worker_orange_1", - }, - )), - belt: Some(comp::Item::new_from_asset_expect( - "common.items.armor.belt.leather_0", - )), - hand: None, - pants: Some(comp::Item::new_from_asset_expect( - "common.items.armor.pants.worker_blue_0", - )), - foot: Some(comp::Item::new_from_asset_expect( - match rand::thread_rng().gen_range(0, 2) { - 0 => "common.items.armor.foot.leather_0", - _ => "common.items.armor.starter.sandals_0", - }, - )), - back: None, - ring: None, - neck: None, - lantern: None, - glider: None, - head: None, - tabard: None, - }, - comp::Alignment::Enemy => comp::Loadout { - active_item, - second_item: None, - shoulder: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.shoulder.cultist_shoulder_purple", - )), - chest: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.chest.cultist_chest_purple", - )), - belt: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.belt.cultist_belt", - )), - hand: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.hand.cultist_hands_purple", - )), - pants: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.pants.cultist_legs_purple", - )), - foot: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.foot.cultist_boots", - )), - back: Some(comp::Item::new_from_asset_expect( - "common.items.npc_armor.back.dungeon_purple-0", - )), - ring: None, - neck: None, - lantern: Some(comp::Item::new_from_asset_expect( - "common.items.lantern.black_0", - )), - glider: None, - head: None, - tabard: None, - }, - _ => LoadoutBuilder::animal(entity.body).build(), - }; - - loadout = match body { - comp::Body::Humanoid(_) => loadout, - _ => LoadoutBuilder::animal(entity.body).build(), - }; + let mut loadout = LoadoutBuilder::build_loadout(body, alignment, main_tool).build(); let mut scale = entity.scale; @@ -277,6 +161,7 @@ impl<'a> System<'a> for Sys { buildup_duration: Duration::from_millis(800), recover_duration: Duration::from_millis(200), base_healthchange: -100, + knockback: 0.0, range: 3.5, max_angle: 60.0, }), diff --git a/tools/src/main.rs b/tools/src/main.rs index 37ee04f4c0..c5b2455f97 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -79,6 +79,7 @@ fn get_tool_kind(kind: &ToolKind) -> String { ToolKind::Shield(_) => "Shield".to_string(), ToolKind::Debug(_) => "Debug".to_string(), ToolKind::Farming(_) => "Farming".to_string(), + ToolKind::NpcWeapon(_) => "NpcWeapon".to_string(), ToolKind::Empty => "Empty".to_string(), } } @@ -94,6 +95,7 @@ fn get_tool_kind_kind(kind: &ToolKind) -> String { ToolKind::Shield(x) => x.clone(), ToolKind::Debug(x) => x.clone(), ToolKind::Farming(x) => x.clone(), + ToolKind::NpcWeapon(x) => x.clone(), ToolKind::Empty => "".to_string(), } } diff --git a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs index 8dbb73ce5c..457908ef38 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs @@ -79,6 +79,7 @@ fn maps_basic_melee() { &CharacterState::BasicMelee(states::basic_melee::Data { buildup_duration: Duration::default(), recover_duration: Duration::default(), + knockback: 0.0, base_healthchange: 10, range: 1.0, max_angle: 1.0, diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index 2847cb221d..8b23ca3d6c 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -75,6 +75,7 @@ fn tool_desc(tool: &Tool, desc: &str) -> String { ToolKind::Dagger(_) => "Dagger", ToolKind::Staff(_) => "Staff", ToolKind::Shield(_) => "Shield", + ToolKind::NpcWeapon(_) => "Npc Weapon", ToolKind::Debug(_) => "Debug", ToolKind::Farming(_) => "Farming Tool", ToolKind::Empty => "Empty", diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index 5854009517..ba55782e50 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -105,6 +105,7 @@ pub enum ParticleMode { Leaf = 9, Firefly = 10, Bee = 11, + GroundShockwave = 12, } impl ParticleMode { diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 7a6c93c8be..93793d9819 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -2115,6 +2115,15 @@ impl FigureMgr { skeleton_attr, ) }, + CharacterState::GroundShockwave(_) => { + anim::golem::ShockwaveAnimation::update_skeleton( + &target_base, + (vel.0.magnitude(), time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + }, // TODO! _ => target_base, }; diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 127530d541..a5c45fb843 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -8,7 +8,7 @@ use crate::{ }; use common::{ assets::Asset, - comp::{item::Reagent, object, Body, CharacterState, Pos}, + comp::{item::Reagent, object, Body, CharacterState, Ori, Pos, Shockwave}, figure::Segment, outcome::Outcome, span, @@ -113,6 +113,7 @@ impl ParticleMgr { self.maintain_body_particles(scene_data); self.maintain_boost_particles(scene_data); self.maintain_block_particles(scene_data, terrain); + self.maintain_shockwave_particles(scene_data); } else { // remove all particle lifespans self.particles.clear(); @@ -420,6 +421,65 @@ impl ParticleMgr { } } + fn maintain_shockwave_particles(&mut self, scene_data: &SceneData) { + let state = scene_data.state; + let ecs = state.ecs(); + let time = state.get_time(); + + for (_i, (_entity, pos, ori, shockwave)) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .enumerate() + { + let elapsed = time - shockwave.creation.unwrap_or_default(); + + let distance = shockwave.properties.speed * elapsed as f32; + + let radians = shockwave.properties.angle.to_radians(); + + let theta = ori.0.y.atan2(ori.0.x); + let dtheta = radians / distance; + + // 1 / 3 the size of terrain voxel + let scale = 1.0 / 3.0; + + let scaled_speed = shockwave.properties.speed * scale; + + let heartbeats = self + .scheduler + .heartbeats(Duration::from_millis(scaled_speed as u64)); + let new_particle_count = distance / scale * heartbeats as f32; + self.particles.reserve(new_particle_count as usize); + + for heartbeat in 0..heartbeats { + let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32; + + let distance = + shockwave.properties.speed * (elapsed as f32 - sub_tick_interpolation); + + for d in 0..((distance / scale) as i32) { + let arc_position = theta - radians / 2.0 + dtheta * d as f32 * scale; + + let position = + pos.0 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0); + + let position_snapped = ((position / scale).floor() + 0.5) * scale; + + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::GroundShockwave, + position_snapped, + )); + } + } + } + } + fn upload_particles(&mut self, renderer: &mut Renderer) { span!(_guard, "upload_particles", "ParticleMgr::upload_particles"); let all_cpu_instances = self diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index db9817404b..a3a2890166 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -13,7 +13,6 @@ use common::{ comp::{self}, generation::{ChunkSupplement, EntityInfo}, lottery::Lottery, - npc, store::{Id, Store}, terrain::{Block, BlockKind, Structure, TerrainChunkSize}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, @@ -509,22 +508,13 @@ impl Floor { ); let chosen = chosen.choose(); let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32)) - .with_scale(4.0) - .with_level(rng.gen_range(75, 100)) + .with_level(rng.gen_range(1, 5)) .with_alignment(comp::Alignment::Enemy) - .with_body(comp::Body::Humanoid(comp::humanoid::Body::random())) - .with_name(format!( - "Cult Leader {}", - npc::get_npc_name(npc::NpcKind::Humanoid) - )) - .with_main_tool(comp::Item::new_from_asset_expect( - match rng.gen_range(0, 1) { - //Add more possible cult leader npc_weapons here - _ => { - "common.items.npc_weapons.sword.cultist_purp_2h_boss-0" - }, - }, - )) + .with_body(comp::Body::Golem(comp::golem::Body::random_with( + rng, + &comp::golem::Species::StoneGolem, + ))) + .with_name("Stonework Defender".to_string()) .with_loot_drop(comp::Item::new_from_asset_expect(chosen)); supplement.add_entity(entity);