Initial broken implementation of poise

Character movement no longer broken.
This commit is contained in:
jiminycrick 2020-12-05 10:23:45 -08:00
parent 8b97199245
commit c83296a4ec
81 changed files with 965 additions and 91 deletions

286
4 Normal file
View File

@ -0,0 +1,286 @@
use crate::{
comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Stage<T> {
/// Specifies which stage the combo attack is in
pub stage: u32,
/// Initial damage of stage
pub base_damage: u32,
/// Max damage of stage
pub max_damage: u32,
/// Damage scaling per combo
pub damage_increase: u32,
/// Initial poise damage of stage
pub base_poise_damage: u32,
/// Knockback of stage
pub knockback: f32,
/// Range of attack
pub range: f32,
/// Angle of attack
pub angle: f32,
/// Initial buildup duration of stage (how long until state can deal damage)
pub base_buildup_duration: T,
/// Duration of stage spent in swing (controls animation stuff, and can also
/// be used to handle movement separately to buildup)
pub base_swing_duration: T,
/// Initial recover duration of stage (how long until character exits state)
pub base_recover_duration: T,
/// How much forward movement there is in the swing portion of the stage
pub forward_movement: f32,
}
impl Stage<u64> {
pub fn to_duration(self) -> Stage<Duration> {
Stage::<Duration> {
stage: self.stage,
base_damage: self.base_damage,
max_damage: self.max_damage,
damage_increase: self.damage_increase,
base_poise_damage: self.base_poise_damage,
knockback: self.knockback,
range: self.range,
angle: self.angle,
base_buildup_duration: Duration::from_millis(self.base_buildup_duration),
base_swing_duration: Duration::from_millis(self.base_swing_duration),
base_recover_duration: Duration::from_millis(self.base_recover_duration),
forward_movement: self.forward_movement,
}
}
pub fn adjusted_by_stats(mut self, power: f32, speed: f32) -> Self {
self.base_damage = (self.base_damage as f32 * power) as u32;
self.max_damage = (self.max_damage as f32 * power) as u32;
self.damage_increase = (self.damage_increase as f32 * power) as u32;
self.base_buildup_duration = (self.base_buildup_duration as f32 / speed) as u64;
self.base_swing_duration = (self.base_swing_duration as f32 / speed) as u64;
self.base_recover_duration = (self.base_recover_duration as f32 / speed) as u64;
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// Separated out to condense update portions of character state
pub struct StaticData {
/// Indicates number of stages in combo
pub num_stages: u32,
/// Data for each stage
pub stage_data: Vec<Stage<Duration>>,
/// Initial energy gain per strike
pub initial_energy_gain: u32,
/// Max energy gain per strike
pub max_energy_gain: u32,
/// Energy gain increase per combo
pub energy_increase: u32,
/// (100% - speed_increase) is percentage speed increases from current to
/// max when combo increases
pub speed_increase: f32,
/// (100% + max_speed_increase) is the max attack speed
pub max_speed_increase: f32,
/// Whether the state can be interrupted by other abilities
pub is_interruptible: bool,
/// What key is used to press ability
pub ability_key: AbilityKey,
}
/// A sequence of attacks that can incrementally become faster and more
/// damaging.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data {
/// Struct containing data that does not change over the course of the
/// character state
pub static_data: StaticData,
/// Indicates what stage the combo is in
pub stage: u32,
/// Number of consecutive strikes
pub combo: u32,
/// Timer for each stage
pub timer: Duration,
/// Checks what section a stage is in
pub stage_section: StageSection,
/// Whether the state should go onto the next stage
pub next_stage: bool,
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
let mut update = StateUpdate::from(data);
handle_orientation(data, &mut update, 1.0);
handle_move(data, &mut update, 0.3);
if !ability_key_is_pressed(data, self.static_data.ability_key) {
handle_interrupt(data, &mut update, self.static_data.is_interruptible);
if let CharacterState::Roll(roll) = &mut update.character {
roll.was_combo = Some((self.stage, self.combo));
}
match update.character {
CharacterState::ComboMelee(_) => {},
_ => {
return update;
},
}
}
let stage_index = (self.stage - 1) as usize;
let speed_modifer = 1.0
+ self.static_data.max_speed_increase
* (1.0 - self.static_data.speed_increase.powi(self.combo as i32));
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.stage_data[stage_index].base_buildup_duration {
// Build up
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0 * speed_modifer))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to swing section of stage
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
timer: Duration::default(),
stage_section: StageSection::Swing,
..*self
});
// Hit attempt
let damage = self.static_data.stage_data[stage_index].max_damage.min(
self.static_data.stage_data[stage_index].base_damage
+ self.combo / self.static_data.num_stages
* self.static_data.stage_data[stage_index].damage_increase,
);
let poise_damage = self.static_data.stage_data[stage_index].base_poise_damage;
data.updater.insert(data.entity, Attacking {
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: damage as f32,
poise_damage: poise_damage as f32,
})],
range: self.static_data.stage_data[stage_index].range,
max_angle: self.static_data.stage_data[stage_index].angle.to_radians(),
applied: false,
hit_count: 0,
knockback: Knockback::Away(
self.static_data.stage_data[stage_index].knockback,
),
});
}
},
StageSection::Swing => {
if self.timer < self.static_data.stage_data[stage_index].base_swing_duration {
// Forward movement
handle_forced_movement(
data,
&mut update,
ForcedMovement::Forward {
strength: self.static_data.stage_data[stage_index].forward_movement,
},
0.3,
);
// Swings
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0 * speed_modifer))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to recover section of stage
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.stage_data[stage_index].base_recover_duration {
// Recovers
if ability_key_is_pressed(data, self.static_data.ability_key) {
// Checks if state will transition to next stage after recover
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0 * speed_modifer))
.unwrap_or_default(),
next_stage: true,
..*self
});
} else {
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0 * speed_modifer))
.unwrap_or_default(),
..*self
});
}
} else if self.next_stage {
// Transitions to buildup section of next stage
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
stage: (self.stage % self.static_data.num_stages) + 1,
timer: Duration::default(),
stage_section: StageSection::Buildup,
next_stage: false,
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
}
},
_ => {
// If it somehow ends up in an incorrect stage section
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
},
}
// Grant energy on successful hit
if let Some(attack) = data.attacking {
if attack.applied && attack.hit_count > 0 {
let energy = self.static_data.max_energy_gain.min(
self.static_data.initial_energy_gain
+ self.combo * self.static_data.energy_increase,
) as i32;
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
stage: self.stage,
combo: self.combo + 1,
timer: self.timer,
stage_section: self.stage_section,
next_stage: self.next_stage,
});
data.updater.remove::<Attacking>(data.entity);
update.energy.change_by(EnergyChange {
amount: energy,
source: EnergySource::HitEnemy,
});
}
}
update
}
}

View File

@ -3,6 +3,8 @@ ComboMelee(
(
stage: 1,
base_damage: 90,
max_damage: 110,
base_poise_damage: 40,
damage_increase: 10,
knockback: 8.0,
range: 3.5,
@ -15,6 +17,8 @@ ComboMelee(
(
stage: 2,
base_damage: 130,
max_damage: 160,
base_poise_damage: 40,
damage_increase: 15,
knockback: 12.0,
range: 3.5,
@ -32,4 +36,4 @@ ComboMelee(
max_speed_increase: 0.6,
scales_from_combo: 2,
is_interruptible: false,
)
)

View File

@ -5,9 +5,10 @@ LeapMelee(
swing_duration: 200,
recover_duration: 200,
base_damage: 240,
base_poise_damage: 70,
knockback: 12.0,
range: 4.5,
max_angle: 30.0,
forward_leap_strength: 28.0,
vertical_leap_strength: 8.0,
)
)

View File

@ -3,6 +3,7 @@ SpinMelee(
swing_duration: 400,
recover_duration: 200,
base_damage: 60,
base_poise_damage: 20,
knockback: 0.0,
range: 3.5,
energy_cost: 100,
@ -11,4 +12,4 @@ SpinMelee(
is_interruptible: false,
forward_speed: 0.0,
num_spins: 1,
)
)

View File

@ -3,6 +3,7 @@ ChargedRanged(
energy_drain: 300,
initial_damage: 10,
scaled_damage: 190,
initial_poise_damage: 10,
initial_knockback: 10.0,
scaled_knockback: 10.0,
speed: 1.0,

View File

@ -4,7 +4,8 @@ BasicMelee(
swing_duration: 100,
recover_duration: 300,
base_damage: 50,
base_poise_damage: 10,
knockback: 0.0,
range: 3.5,
max_angle: 20.0,
)
)

View File

@ -4,7 +4,8 @@ BasicMelee(
swing_duration: 100,
recover_duration: 900,
base_damage: 20,
base_poise_damage: 5,
knockback: 0.0,
range: 3.5,
max_angle: 15.0,
)
)

View File

@ -4,7 +4,8 @@ BasicMelee(
swing_duration: 100,
recover_duration: 150,
base_damage: 50,
base_poise_damage: 5,
knockback: 0.0,
range: 3.5,
max_angle: 20.0,
)
)

View File

@ -3,6 +3,7 @@ ChargedMelee(
energy_drain: 300,
initial_damage: 10,
scaled_damage: 160,
initial_poise_damage: 10,
initial_knockback: 10.0,
scaled_knockback: 50.0,
range: 3.5,

View File

@ -5,9 +5,10 @@ LeapMelee(
swing_duration: 150,
recover_duration: 200,
base_damage: 240,
base_poise_damage: 60,
knockback: 25.0,
range: 4.5,
max_angle: 360.0,
forward_leap_strength: 28.0,
vertical_leap_strength: 8.0,
)
)

View File

@ -3,7 +3,8 @@ ComboMelee(
stage: 1,
base_damage: 130,
damage_increase: 10,
knockback: 10.0,
base_poise_damage: 30,
knockback: 0.0,
range: 4.5,
angle: 50.0,
base_buildup_duration: 600,
@ -18,4 +19,4 @@ ComboMelee(
max_speed_increase: 0.4,
scales_from_combo: 2,
is_interruptible: false,
)
)

View File

@ -4,7 +4,8 @@ BasicMelee(
swing_duration: 100,
recover_duration: 300,
base_damage: 40,
base_poise_damage: 20,
knockback: 0.0,
range: 3.0,
max_angle: 120.0,
)
)

View File

@ -4,6 +4,7 @@ Shockwave(
swing_duration: 100,
recover_duration: 300,
damage: 200,
poise_damage: 10,
knockback: Away(25.0),
shockwave_angle: 360.0,
shockwave_vertical_angle: 90.0,
@ -11,4 +12,4 @@ Shockwave(
shockwave_duration: 500,
requires_ground: false,
move_efficiency: 0.1,
)
)

View File

@ -2,6 +2,7 @@ DashMelee(
energy_cost: 100,
base_damage: 80,
scaled_damage: 160,
base_poise_damage: 20,
base_knockback: 8.0,
scaled_knockback: 7.0,
range: 5.0,

View File

@ -3,6 +3,7 @@ SpinMelee(
swing_duration: 400,
recover_duration: 500,
base_damage: 160,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
energy_cost: 150,
@ -11,4 +12,4 @@ SpinMelee(
is_interruptible: true,
forward_speed: 1.0,
num_spins: 3,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 10,
base_poise_damage: 10,
knockback: 10.0,
range: 4.0,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 80,
damage_increase: 15,
base_poise_damage: 10,
knockback: 12.0,
range: 3.5,
angle: 180.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 130,
damage_increase: 20,
base_poise_damage: 10,
knockback: 14.0,
range: 6.0,
angle: 10.0,
@ -44,4 +47,4 @@ ComboMelee(
max_speed_increase: 0.8,
scales_from_combo: 2,
is_interruptible: true,
)
)

View File

@ -5,6 +5,7 @@ BasicMelee(
recover_duration: 250,
knockback: 25.0,
base_damage: 200,
base_poise_damage: 30,
range: 5.0,
max_angle: 120.0,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 0,
base_poise_damage: 30,
knockback: 5.0,
range: 3.5,
angle: 60.0,
@ -20,4 +21,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 120,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 80,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 130,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -44,4 +47,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -2,6 +2,7 @@ DashMelee(
energy_cost: 0,
base_damage: 150,
scaled_damage: 110,
base_poise_damage: 60,
base_knockback: 8.0,
scaled_knockback: 17.0,
range: 5.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 4.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 80,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 130,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,

View File

@ -2,6 +2,7 @@ DashMelee(
energy_cost: 0,
base_damage: 30,
scaled_damage: 10,
base_poise_damage: 30,
base_knockback: 8.0,
scaled_knockback: 7.0,
range: 2.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 0,
base_poise_damage: 10,
knockback: 2.0,
range: 3.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 130,
damage_increase: 0,
base_poise_damage: 10,
knockback: 2.0,
range: 3.5,
angle: 30.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 130,
damage_increase: 0,
base_poise_damage: 10,
knockback: 2.0,
range: 3.5,
angle: 30.0,
@ -40,6 +43,7 @@ ComboMelee(
stage: 4,
base_damage: 130,
damage_increase: 0,
base_poise_damage: 10,
knockback: 8.0,
range: 3.5,
angle: 30.0,
@ -56,4 +60,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 60,
damage_increase: 0,
base_poise_damage: 30,
knockback: 5.0,
range: 3.5,
angle: 60.0,
@ -20,4 +21,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -3,6 +3,7 @@ ChargedMelee(
energy_drain: 0,
initial_damage: 160,
scaled_damage: 40,
initial_poise_damage: 50,
initial_knockback: 10.0,
scaled_knockback: 20.0,
range: 6.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 120,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 130,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -44,4 +47,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 120,
damage_increase: 0,
base_poise_damage: 10,
knockback: 5.0,
range: 3.5,
angle: 60.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 120,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 120,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 120,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,

View File

@ -2,6 +2,7 @@ DashMelee(
energy_cost: 0,
base_damage: 150,
scaled_damage: 40,
base_poise_damage: 40,
base_knockback: 8.0,
scaled_knockback: 17.0,
range: 4.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 80,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 3.5,
angle: 30.0,

View File

@ -4,7 +4,8 @@ BasicMelee(
swing_duration: 500,
recover_duration: 350,
base_damage: 130,
base_poise_damage: 70,
knockback: 25.0,
range: 3.0,
max_angle: 120.0,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 100,
damage_increase: 0,
base_poise_damage: 10,
knockback: 8.0,
range: 3.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 80,
damage_increase: 0,
base_poise_damage: 10,
knockback: 8.0,
range: 3.5,
angle: 30.0,

View File

@ -5,6 +5,7 @@ LeapMelee(
swing_duration: 75,
recover_duration: 200,
base_damage: 240,
base_poise_damage: 15,
knockback: 12.0,
range: 4.5,
max_angle: 180.0,

View File

@ -5,6 +5,7 @@ LeapMelee(
swing_duration: 75,
recover_duration: 125,
base_damage: 120,
base_poise_damage: 15,
knockback: 7.0,
range: 4.5,
max_angle: 180.0,

View File

@ -2,6 +2,7 @@ DashMelee(
energy_cost: 0,
base_damage: 130,
scaled_damage: 20,
base_poise_damage: 30,
base_knockback: 8.0,
scaled_knockback: 7.0,
range: 2.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 150,
damage_increase: 0,
base_poise_damage: 10,
knockback: 5.0,
range: 3.5,
angle: 60.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 150,
damage_increase: 0,
base_poise_damage: 10,
knockback: 5.0,
range: 3.5,
angle: 60.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 150,
damage_increase: 0,
base_poise_damage: 10,
knockback: 5.0,
range: 3.5,
angle: 60.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 30,
damage_increase: 0,
base_poise_damage: 10,
knockback: 5.0,
range: 3.5,
angle: 60.0,
@ -20,4 +21,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -5,6 +5,7 @@ BasicMelee(
recover_duration: 250,
knockback: 25.0,
base_damage: 200,
base_poise_damage: 80,
range: 5.0,
max_angle: 120.0,
)
)

View File

@ -4,6 +4,7 @@ Shockwave(
swing_duration: 200,
recover_duration: 800,
damage: 500,
poise_damage: 50,
knockback: TowardsUp(40.0),
shockwave_angle: 90.0,
shockwave_vertical_angle: 90.0,
@ -11,4 +12,4 @@ Shockwave(
shockwave_duration: 1000,
requires_ground: true,
move_efficiency: 0.05,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 150,
damage_increase: 0,
base_poise_damage: 15,
knockback: 5.0,
range: 7.5,
angle: 60.0,
@ -20,4 +21,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 170,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 7.5,
angle: 30.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 190,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 5.5,
angle: 30.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 230,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 5.5,
angle: 30.0,

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 150,
damage_increase: 0,
base_poise_damage: 10,
knockback: 5.0,
range: 5.5,
angle: 5.0,
@ -20,4 +21,4 @@ ComboMelee(
max_speed_increase: 0.0,
scales_from_combo: 0,
is_interruptible: false,
)
)

View File

@ -4,6 +4,7 @@ ComboMelee(
stage: 1,
base_damage: 170,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 4.5,
angle: 5.0,
@ -16,6 +17,7 @@ ComboMelee(
stage: 2,
base_damage: 190,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 4.0,
angle: 10.0,
@ -28,6 +30,7 @@ ComboMelee(
stage: 3,
base_damage: 230,
damage_increase: 0,
base_poise_damage: 10,
knockback: 10.0,
range: 4.0,
angle: 10.0,

View File

@ -18,6 +18,7 @@ sum_type! {
Auras(comp::Auras),
Energy(comp::Energy),
Health(comp::Health),
Poise(comp::Poise),
LightEmitter(comp::LightEmitter),
Inventory(comp::Inventory),
Item(comp::Item),
@ -50,6 +51,7 @@ sum_type! {
Auras(PhantomData<comp::Auras>),
Energy(PhantomData<comp::Energy>),
Health(PhantomData<comp::Health>),
Poise(PhantomData<comp::Poise>),
LightEmitter(PhantomData<comp::LightEmitter>),
Inventory(PhantomData<comp::Inventory>),
Item(PhantomData<comp::Item>),
@ -82,6 +84,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Auras(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Health(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Poise(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world),
@ -112,6 +115,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Auras(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Health(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Poise(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world),
@ -142,6 +146,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::Auras(_) => sync::handle_remove::<comp::Auras>(entity, world),
EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world),
EcsCompPhantom::Health(_) => sync::handle_remove::<comp::Health>(entity, world),
EcsCompPhantom::Poise(_) => sync::handle_remove::<comp::Poise>(entity, world),
EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world)
},

View File

@ -40,6 +40,7 @@ pub enum DamageSource {
pub struct Damage {
pub source: DamageSource,
pub value: f32,
pub poise_damage: f32,
}
impl Damage {
@ -68,7 +69,6 @@ impl Damage {
pub fn modify_damage(self, inventory: Option<&Inventory>, uid: Option<Uid>) -> HealthChange {
let mut damage = self.value;
let damage_reduction = inventory.map_or(0.0, |inv| Damage::compute_damage_reduction(inv));
match self.source {
DamageSource::Melee => {
// Critical hit
@ -84,13 +84,16 @@ impl Damage {
damage += critdamage;
}
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
(
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
},
}
-poise_damage as i32,
)
},
DamageSource::Projectile => {
// Critical hit
@ -100,63 +103,82 @@ impl Damage {
// Armor
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
(
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
},
}
-poise_damage as i32,
)
},
DamageSource::Explosion => {
// Armor
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
(
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
},
}
-poise_damage as i32,
)
},
DamageSource::Shockwave => {
// Armor
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
(
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
},
}
-poise_damage as i32,
)
},
DamageSource::Energy => {
// Armor
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
(
HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
},
}
},
DamageSource::Healing => HealthChange {
amount: damage as i32,
cause: HealthSource::Heal { by: uid },
-poise_damage as i32,
)
},
DamageSource::Healing => (
HealthChange {
amount: damage as i32,
cause: HealthSource::Heal { by: uid },
},
0,
),
DamageSource::Falling => {
// Armor
if (damage_reduction - 1.0).abs() < f32::EPSILON {
damage = 0.0;
poise_damage = 0.0;
}
HealthChange {
amount: -damage as i32,
cause: HealthSource::World,
}
(
HealthChange {
amount: -damage as i32,
cause: HealthSource::World,
},
-poise_damage as i32,
)
},
DamageSource::Buff(_) => HealthChange {
amount: -damage as i32,
@ -171,7 +193,8 @@ impl Damage {
kind: self.source,
by: uid,
},
},
-poise_damage as i32,
),
}
}

View File

@ -61,6 +61,7 @@ pub enum CharacterAbility {
swing_duration: u64,
recover_duration: u64,
base_damage: u32,
base_poise_damage: u32,
knockback: f32,
range: f32,
max_angle: f32,
@ -98,6 +99,7 @@ pub enum CharacterAbility {
energy_cost: u32,
base_damage: u32,
scaled_damage: u32,
base_poise_damage: u32,
base_knockback: f32,
scaled_knockback: f32,
range: f32,
@ -137,6 +139,7 @@ pub enum CharacterAbility {
swing_duration: u64,
recover_duration: u64,
base_damage: u32,
base_poise_damage: u32,
range: f32,
max_angle: f32,
knockback: f32,
@ -148,6 +151,7 @@ pub enum CharacterAbility {
swing_duration: u64,
recover_duration: u64,
base_damage: u32,
base_poise_damage: u32,
knockback: f32,
range: f32,
energy_cost: u32,
@ -162,6 +166,7 @@ pub enum CharacterAbility {
energy_drain: u32,
initial_damage: u32,
scaled_damage: u32,
initial_poise_damage: u32,
initial_knockback: f32,
scaled_knockback: f32,
range: f32,
@ -177,6 +182,7 @@ pub enum CharacterAbility {
energy_drain: u32,
initial_damage: u32,
scaled_damage: u32,
initial_poise_damage: u32,
initial_knockback: f32,
scaled_knockback: f32,
speed: f32,
@ -196,6 +202,7 @@ pub enum CharacterAbility {
swing_duration: u64,
recover_duration: u64,
damage: u32,
poise_damage: u32,
knockback: Knockback,
shockwave_angle: f32,
shockwave_vertical_angle: f32,
@ -228,6 +235,7 @@ impl Default for CharacterAbility {
swing_duration: 250,
recover_duration: 500,
base_damage: 10,
base_poise_damage: 10,
knockback: 0.0,
range: 3.5,
max_angle: 15.0,
@ -1071,6 +1079,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration,
recover_duration,
base_damage,
base_poise_damage,
knockback,
range,
max_angle,
@ -1081,6 +1090,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration: Duration::from_millis(*swing_duration),
recover_duration: Duration::from_millis(*recover_duration),
base_damage: *base_damage,
base_poise_damage: *base_poise_damage,
knockback: *knockback,
range: *range,
max_angle: *max_angle,
@ -1131,6 +1141,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
energy_cost: _,
base_damage,
scaled_damage,
base_poise_damage,
base_knockback,
scaled_knockback,
range,
@ -1147,6 +1158,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
static_data: dash_melee::StaticData {
base_damage: *base_damage,
scaled_damage: *scaled_damage,
base_poise_damage: *base_poise_damage,
base_knockback: *base_knockback,
scaled_knockback: *scaled_knockback,
range: *range,
@ -1224,6 +1236,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration,
recover_duration,
base_damage,
base_poise_damage,
knockback,
range,
max_angle,
@ -1236,6 +1249,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration: Duration::from_millis(*swing_duration),
recover_duration: Duration::from_millis(*recover_duration),
base_damage: *base_damage,
base_poise_damage: *base_poise_damage,
knockback: *knockback,
range: *range,
max_angle: *max_angle,
@ -1252,6 +1266,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration,
recover_duration,
base_damage,
base_poise_damage,
knockback,
range,
energy_cost,
@ -1266,6 +1281,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration: Duration::from_millis(*swing_duration),
recover_duration: Duration::from_millis(*recover_duration),
base_damage: *base_damage,
base_poise_damage: *base_damage,
knockback: *knockback,
range: *range,
energy_cost: *energy_cost,
@ -1286,6 +1302,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
energy_drain,
initial_damage,
scaled_damage,
initial_poise_damage,
initial_knockback,
scaled_knockback,
speed,
@ -1301,6 +1318,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
energy_drain: *energy_drain,
initial_damage: *initial_damage,
scaled_damage: *scaled_damage,
initial_poise_damage: *initial_poise_damage,
initial_knockback: *initial_knockback,
scaled_knockback: *scaled_knockback,
speed: *speed,
@ -1322,6 +1340,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
energy_drain,
initial_damage,
scaled_damage,
initial_poise_damage,
initial_knockback,
scaled_knockback,
speed,
@ -1342,6 +1361,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
energy_drain: *energy_drain,
initial_damage: *initial_damage,
scaled_damage: *scaled_damage,
initial_poise_damage: *initial_poise_damage,
speed: *speed,
initial_knockback: *initial_knockback,
scaled_knockback: *scaled_knockback,
@ -1394,6 +1414,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration,
recover_duration,
damage,
poise_damage,
knockback,
shockwave_angle,
shockwave_vertical_angle,
@ -1407,6 +1428,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration: Duration::from_millis(*swing_duration),
recover_duration: Duration::from_millis(*recover_duration),
damage: *damage,
poise_damage: *poise_damage,
knockback: *knockback,
shockwave_angle: *shockwave_angle,
shockwave_vertical_angle: *shockwave_vertical_angle,

View File

@ -444,6 +444,14 @@ impl Body {
// TODO: Match on species
pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) = self { 0.0 } else { 1.0 } }
pub fn base_poise(&self) -> u32 {
match self {
Body::Humanoid(_) => 100,
Body::BipedLarge(_) => 200,
_ => 100,
}
}
#[allow(unreachable_patterns)]
pub fn base_exp(&self) -> u32 {
match self {
@ -602,6 +610,13 @@ impl Body {
}
}
pub fn base_poise_dmg(&self) -> u32 {
match self {
Body::Humanoid(_) => 100,
_ => 50,
}
}
pub fn base_range(&self) -> f32 {
match self {
Body::Humanoid(_) => 5.0,

View File

@ -44,6 +44,10 @@ pub enum CharacterState {
Sneak,
Glide,
GlideWield,
/// A stunned state
Stunned(stunned::Data),
/// A stagger state similar to a stun but knocked to the ground
Staggered(staggered::Data),
/// A basic blocking state
BasicBlock,
/// Player is busy equipping or unequipping weapons
@ -104,6 +108,10 @@ impl CharacterState {
matches!(self, CharacterState::Sneak | CharacterState::Roll(_))
}
pub fn is_stunned(&self) -> bool {
matches!(self, CharacterState::Stunned { .. } | CharacterState::Staggered { .. })
}
pub fn is_attack(&self) -> bool {
matches!(
self,

View File

@ -19,6 +19,7 @@ mod location;
mod misc;
mod phys;
mod player;
pub mod poise;
pub mod projectile;
pub mod shockwave;
pub mod skills;
@ -65,6 +66,7 @@ pub use phys::{
Sticky, Vel,
};
pub use player::Player;
pub use poise::{Poise, PoiseState};
pub use projectile::{Projectile, ProjectileConstructor};
pub use shockwave::{Shockwave, ShockwaveHitEntities};
pub use skills::{Skill, SkillGroup, SkillGroupKind, SkillSet};

106
common/src/comp/poise.rs Normal file
View File

@ -0,0 +1,106 @@
use crate::{comp::Body, sync::Uid, DamageSource};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Poise {
base_max: u32,
current: u32,
maximum: u32,
pub is_interrupted: bool,
pub is_stunned: bool,
pub is_dazed: bool,
pub is_knockeddown: bool,
}
impl Default for Poise {
fn default() -> Self {
Self {
current: 0,
maximum: 0,
base_max: 0,
is_interrupted: false,
is_stunned: false,
is_dazed: false,
is_knockeddown: false,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum PoiseState {
Normal,
Interrupted,
Stunned,
Dazed,
KnockedDown,
}
impl Poise {
pub fn new(body: Body) -> Self {
let mut poise = Poise::default();
poise.update_max_poise(Some(body));
poise.set_to(poise.maximum());
poise
}
pub fn poise_state(&self) -> PoiseState {
if self.current >= 5 * self.maximum / 10 {
PoiseState::Normal
} else if self.current >= 4 * self.maximum / 10 {
PoiseState::Interrupted
} else if self.current >= 3 * self.maximum / 10 {
PoiseState::Stunned
} else if self.current >= 2 * self.maximum / 10 {
PoiseState::Dazed
} else {
PoiseState::KnockedDown
}
}
pub fn current(&self) -> u32 { self.current }
pub fn maximum(&self) -> u32 { self.maximum }
pub fn set_to(&mut self, amount: u32) {
let amount = amount.min(self.maximum);
self.current = amount;
}
pub fn change_by(&mut self, change: i32) {
self.current = ((self.current as i32 + change).max(0) as u32).min(self.maximum);
}
pub fn reset(&mut self) { self.current = self.maximum; }
pub fn set_maximum(&mut self, amount: u32) {
self.maximum = amount;
self.current = self.current.min(self.maximum);
}
fn set_base_max(&mut self, amount: u32) {
self.base_max = amount;
self.current = self.current.min(self.maximum);
}
pub fn reset_max(&mut self) { self.maximum = self.base_max; }
pub fn update_max_poise(&mut self, body: Option<Body>) {
if let Some(body) = body {
self.set_base_max(body.base_poise());
self.set_maximum(body.base_poise());
}
}
pub fn with_max_poise(mut self, amount: u32) -> Self {
self.maximum = amount;
self.current = amount;
self
}
}
impl Component for Poise {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -88,6 +88,7 @@ impl ProjectileConstructor {
Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Projectile,
value: damage,
poise_damage: 0.0,
}),
Effect::Knockback(Knockback::Away(knockback)),
Effect::RewardEnergy(energy_regen),
@ -115,6 +116,7 @@ impl ProjectileConstructor {
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
poise_damage: 10.0,
}),
),
RadiusEffect::TerrainDestruction(2.0),
@ -131,6 +133,7 @@ impl ProjectileConstructor {
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
poise_damage: 10.0,
}),
)],
radius,
@ -172,6 +175,7 @@ impl ProjectileConstructor {
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
poise_damage: 0.0,
}),
),
RadiusEffect::Entity(
@ -179,6 +183,7 @@ impl ProjectileConstructor {
effect::Effect::Damage(Damage {
source: DamageSource::Healing,
value: heal,
poise_damage: 0.0,
}),
),
],
@ -195,6 +200,7 @@ impl ProjectileConstructor {
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
poise_damage: 0.0,
}),
),
RadiusEffect::Entity(
@ -202,6 +208,7 @@ impl ProjectileConstructor {
effect::Effect::Damage(Damage {
source: DamageSource::Healing,
value: heal,
poise_damage: 0.0,
}),
),
],

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Effect {
Health(comp::HealthChange),
Poise(i32),
Damage(combat::Damage),
Buff(BuffEffect),
}
@ -21,6 +22,7 @@ impl Effect {
pub fn info(&self) -> String {
match self {
Effect::Health(c) => format!("{:+} health", c.amount),
Effect::Poise(c) => format!("{:+} poise", c),
Effect::Damage(d) => format!("{:+}", d.value),
Effect::Buff(e) => format!("{:?} buff", e),
}
@ -31,6 +33,9 @@ impl Effect {
Effect::Health(change) => {
change.amount = (change.amount as f32 * modifier) as i32;
},
Effect::Poise(change) => {
*change = (*change as f32 * modifier) as i32;
},
Effect::Damage(damage) => {
damage.interpolate_damage(modifier, 0.0);
},

View File

@ -34,7 +34,7 @@ pub enum ServerEvent {
},
Damage {
entity: EcsEntity,
change: comp::HealthChange,
change: (comp::HealthChange, i32),
},
Delete(EcsEntity),
Destroy {
@ -98,6 +98,7 @@ pub enum ServerEvent {
pos: comp::Pos,
stats: comp::Stats,
health: comp::Health,
poise: comp::Poise,
loadout: comp::inventory::loadout::Loadout,
body: comp::Body,
agent: Option<comp::Agent>,

View File

@ -122,10 +122,12 @@ impl CharacterBehavior for Data {
let damage = Damage {
source: DamageSource::Energy,
value: self.static_data.base_dps as f32 / self.static_data.tick_rate,
poise_damage: 0.0,
};
let heal = Damage {
source: DamageSource::Healing,
value: self.static_data.base_hps as f32 / self.static_data.tick_rate,
poise_damage: 0.0,
};
let speed =
self.static_data.range / self.static_data.beam_duration.as_secs_f32();

View File

@ -20,6 +20,8 @@ pub struct StaticData {
pub recover_duration: Duration,
/// Base damage
pub base_damage: u32,
/// Base poise reduction
pub base_poise_damage: u32,
/// Knockback
pub knockback: f32,
/// Max range
@ -92,6 +94,7 @@ impl CharacterBehavior for Data {
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
poise_damage: self.static_data.base_poise_damage as f32,
})],
range: self.static_data.range,
max_angle: 180_f32.to_radians(),

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
Attacking, Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy,
Health, Inventory, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel,
Health, Inventory, Ori, PhysicsState, Poise, Pos, StateUpdate, Stats, Vel,
},
resources::DeltaTime,
uid::Uid,
@ -51,6 +51,7 @@ pub struct JoinData<'a> {
pub controller: &'a Controller,
pub inputs: &'a ControllerInputs,
pub health: &'a Health,
pub poise: Option<&'a Poise>,
pub energy: &'a Energy,
pub inventory: &'a Inventory,
pub body: &'a Body,
@ -80,6 +81,7 @@ pub type JoinTuple<'a> = (
RestrictedMut<'a, Inventory>,
&'a mut Controller,
&'a Health,
Option<&'a Poise>,
&'a Body,
&'a PhysicsState,
Option<&'a Attacking>,
@ -101,10 +103,11 @@ impl<'a> JoinData<'a> {
controller: j.8,
inputs: &j.8.inputs,
health: j.9,
body: j.10,
physics: j.11,
attacking: j.12,
stats: j.14,
poise: j.10,
body: j.11,
physics: j.12,
attacking: j.13,
stats: j.15,
updater,
dt,
}

View File

@ -24,6 +24,8 @@ pub struct StaticData {
pub initial_knockback: f32,
/// How much the knockback is scaled by
pub scaled_knockback: f32,
/// Initial poise damage
pub initial_poise_damage: u32,
/// Max range
pub range: f32,
/// Max angle (45.0 will give you a 90.0 angle window)
@ -152,6 +154,7 @@ impl CharacterBehavior for Data {
source: DamageSource::Melee,
value: self.static_data.initial_damage as f32
+ self.charge_amount * self.static_data.scaled_damage as f32,
poise_damage: self.static_data.initial_poise_damage as f32,
};
let knockback = self.static_data.initial_knockback
+ self.charge_amount * self.static_data.scaled_knockback;

View File

@ -30,6 +30,8 @@ pub struct StaticData {
pub initial_damage: u32,
/// How much the damage scales as it is charged
pub scaled_damage: u32,
/// Initial poise damage
pub initial_poise_damage: u32,
/// How much knockback there is with no charge
pub initial_knockback: f32,
/// How much the knockback scales as it is charged
@ -106,6 +108,7 @@ impl CharacterBehavior for Data {
source: DamageSource::Projectile,
value: self.static_data.initial_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
poise_damage: self.static_data.initial_poise_damage as f32,
};
let knockback = self.static_data.initial_knockback
+ charge_frac * self.static_data.scaled_knockback;

View File

@ -17,6 +17,8 @@ pub struct Stage<T> {
pub base_damage: u32,
/// Damage scaling per combo
pub damage_increase: u32,
/// Initial poise damage of stage
pub base_poise_damage: u32,
/// Knockback of stage
pub knockback: f32,
/// Range of attack
@ -40,6 +42,7 @@ impl Stage<u64> {
stage: self.stage,
base_damage: self.base_damage,
damage_increase: self.damage_increase,
base_poise_damage: self.base_poise_damage,
knockback: self.knockback,
range: self.range,
angle: self.angle,
@ -163,10 +166,12 @@ impl CharacterBehavior for Data {
.scales_from_combo
.min(self.combo / self.static_data.num_stages)
* self.static_data.stage_data[stage_index].damage_increase;
let poise_damage = self.static_data.stage_data[stage_index].base_poise_damage;
data.updater.insert(data.entity, Attacking {
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: damage as f32,
poise_damage: poise_damage as f32,
})],
range: self.static_data.stage_data[stage_index].range,
max_angle: self.static_data.stage_data[stage_index].angle.to_radians(),

View File

@ -17,6 +17,8 @@ pub struct StaticData {
pub base_damage: u32,
/// How much the attack scales in damage
pub scaled_damage: u32,
/// Initial poise damage
pub base_poise_damage: u32,
/// How much the attack knocks the target back initially
pub base_knockback: f32,
/// How much the attack scales in knockback
@ -129,6 +131,7 @@ impl CharacterBehavior for Data {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
poise_damage: self.static_data.base_poise_damage as f32,
};
let knockback = self.static_data.base_knockback
+ charge_frac * self.static_data.scaled_knockback;

View File

@ -22,6 +22,8 @@ pub struct StaticData {
pub recover_duration: Duration,
/// Base damage
pub base_damage: u32,
/// Base poise damage
pub base_poise_damage: u32,
/// Knockback
pub knockback: f32,
/// Max range
@ -150,6 +152,7 @@ impl CharacterBehavior for Data {
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
poise_damage: self.static_data.base_poise_damage as f32,
})],
range: self.static_data.range,
max_angle: self.static_data.max_angle.to_radians(),

View File

@ -21,5 +21,7 @@ pub mod shockwave;
pub mod sit;
pub mod sneak;
pub mod spin_melee;
pub mod staggered;
pub mod stunned;
pub mod utils;
pub mod wielding;

View File

@ -21,6 +21,8 @@ pub struct StaticData {
pub recover_duration: Duration,
/// Base damage
pub damage: u32,
/// Base poise damage
pub poise_damage: u32,
/// Knockback
pub knockback: Knockback,
/// Angle of the shockwave
@ -86,6 +88,7 @@ impl CharacterBehavior for Data {
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Shockwave,
value: self.static_data.damage as f32,
poise_damage: self.static_data.poise_damage as f32,
})],
knockback: self.static_data.knockback,
requires_ground: self.static_data.requires_ground,

View File

@ -22,6 +22,8 @@ pub struct StaticData {
pub recover_duration: Duration,
/// Base damage
pub base_damage: u32,
/// Base poise damage
pub base_poise_damage: u32,
/// Knockback
pub knockback: f32,
/// Range
@ -114,6 +116,7 @@ impl CharacterBehavior for Data {
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
poise_damage: self.static_data.base_damage as f32,
})],
range: self.static_data.range,
max_angle: 180_f32.to_radians(),

View File

@ -0,0 +1,89 @@
use super::utils::*;
use crate::{
comp::{CharacterState, StateUpdate},
states::behavior::{CharacterBehavior, JoinData},
Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticData {
/// How long until state begins to exit
pub buildup_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// Knockback
pub knockback: Knockback,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data {
/// Struct containing data that does not change over the course of the
/// character state
pub static_data: StaticData,
/// Timer for each stage
pub timer: Duration,
/// What section the character stage is in
pub stage_section: StageSection,
/// Whether the character was wielding or not
pub was_wielded: bool,
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
println!("staggered");
let mut update = StateUpdate::from(data);
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.buildup_duration {
// Build up
update.character = CharacterState::Staggered(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to recovery section of stage
update.character = CharacterState::Staggered(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::Staggered(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
if self.was_wielded {
update.character = CharacterState::Wielding;
} else {
update.character = CharacterState::Idle;
}
}
},
_ => {
// If it somehow ends up in an incorrect stage section
if self.was_wielded {
update.character = CharacterState::Wielding;
} else {
update.character = CharacterState::Idle;
}
},
}
update
}
}

View File

@ -0,0 +1,89 @@
use super::utils::*;
use crate::{
comp::{CharacterState, StateUpdate},
states::behavior::{CharacterBehavior, JoinData},
Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticData {
/// How long until state begins to exit
pub buildup_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// Knockback
pub knockback: Knockback,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Data {
/// Struct containing data that does not change over the course of the
/// character state
pub static_data: StaticData,
/// Timer for each stage
pub timer: Duration,
/// What section the character stage is in
pub stage_section: StageSection,
/// Whether the character was wielding or not
pub was_wielded: bool,
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
println!("stunned");
let mut update = StateUpdate::from(data);
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.buildup_duration {
// Build up
update.character = CharacterState::Stunned(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to recovery section of stage
update.character = CharacterState::Stunned(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::Stunned(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
if self.was_wielded {
update.character = CharacterState::Wielding;
} else {
update.character = CharacterState::Idle;
}
}
},
_ => {
// If it somehow ends up in an incorrect stage section
if self.was_wielded {
update.character = CharacterState::Wielding;
} else {
update.character = CharacterState::Idle;
}
},
}
update
}
}

View File

@ -174,14 +174,17 @@ impl<'a> System<'a> for Sys {
if let Some(entity) = beam_owner {
server_emitter.emit(ServerEvent::Damage {
entity,
change: HealthChange {
amount: (-change.amount as f32
* beam_segment.lifesteal_eff)
as i32,
cause: HealthSource::Heal {
by: beam_segment.owner,
change: (
HealthChange {
amount: (-change.0.amount as f32
* beam_segment.lifesteal_eff)
as i32,
cause: HealthSource::Heal {
by: beam_segment.owner,
},
},
},
0,
),
});
server_emitter.emit(ServerEvent::EnergyChange {
entity,

View File

@ -114,7 +114,7 @@ impl<'a> System<'a> for Sys {
};
server_emitter.emit(ServerEvent::Damage {
entity,
change: HealthChange { amount, cause },
change: (HealthChange { amount, cause }, 0),
});
*accumulated = 0.0;
};

View File

@ -4,7 +4,7 @@ use common::{
comp::{
inventory::slot::{EquipSlot, Slot},
Attacking, Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Mounting,
Ori, PhysicsState, Pos, StateUpdate, Stats, Vel,
Ori, PhysicsState, Poise, Pos, StateUpdate, Stats, Vel,
},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
@ -65,6 +65,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Inventory>,
WriteStorage<'a, Controller>,
ReadStorage<'a, Health>,
ReadStorage<'a, Poise>,
ReadStorage<'a, Body>,
ReadStorage<'a, PhysicsState>,
ReadStorage<'a, Attacking>,
@ -93,6 +94,7 @@ impl<'a> System<'a> for Sys {
mut inventories,
mut controllers,
healths,
poises,
bodies,
physics_states,
attacking_storage,
@ -118,6 +120,7 @@ impl<'a> System<'a> for Sys {
&mut inventories.restrict_mut(),
&mut controllers,
&healths,
poises.maybe(),
&bodies,
&physics_states,
attacking_storage.maybe(),
@ -151,6 +154,8 @@ impl<'a> System<'a> for Sys {
CharacterState::GlideWield => {
states::glide_wield::Data.handle_event(&j, action)
},
CharacterState::Stunned(data) => data.handle_event(&j, action),
CharacterState::Staggered(data) => data.handle_event(&j, action),
CharacterState::Sit => {
states::sit::Data::handle_event(&states::sit::Data, &j, action)
},
@ -191,6 +196,8 @@ impl<'a> System<'a> for Sys {
CharacterState::Climb => states::climb::Data.behavior(&j),
CharacterState::Glide => states::glide::Data.behavior(&j),
CharacterState::GlideWield => states::glide_wield::Data.behavior(&j),
CharacterState::Stunned(data) => data.behavior(&j),
CharacterState::Staggered(data) => data.behavior(&j),
CharacterState::Sit => states::sit::Data::behavior(&states::sit::Data, &j),
CharacterState::Dance => states::dance::Data::behavior(&states::dance::Data, &j),
CharacterState::Sneak => states::sneak::Data::behavior(&states::sneak::Data, &j),

View File

@ -132,14 +132,14 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Damage { entity: b, change });
// Apply bleeding buff on melee hits with 10% chance
// TODO: Don't have buff uniformly applied on all melee attacks
if change.amount < 0 && thread_rng().gen::<f32>() < 0.1 {
if change.0.amount < 0 && thread_rng().gen::<f32>() < 0.1 {
use buff::*;
server_emitter.emit(ServerEvent::Buff {
entity: b,
buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding,
BuffData {
strength: -change.amount as f32 / 10.0,
strength: -change.0.amount as f32 / 10.0,
duration: Some(Duration::from_secs(10)),
},
vec![BuffCategory::Physical],

View File

@ -116,6 +116,7 @@ impl State {
ecs.register::<comp::Auras>();
ecs.register::<comp::Energy>();
ecs.register::<comp::Health>();
ecs.register::<comp::Poise>();
ecs.register::<comp::CanBuild>();
ecs.register::<comp::LightEmitter>();
ecs.register::<comp::Item>();

View File

@ -214,7 +214,10 @@ impl<'a> System<'a> for Sys {
},
// Non-combat abilities that consume energy;
// temporarily stall energy gain, but preserve regen_rate.
CharacterState::Roll { .. } | CharacterState::Climb { .. } => {},
CharacterState::Roll { .. }
| CharacterState::Climb { .. }
| CharacterState::Stunned { .. }
| CharacterState::Staggered { .. } => {},
}
}
sys_metrics.stats_ns.store(

View File

@ -825,6 +825,7 @@ fn handle_spawn(
npc::BodyType::from_body(body),
)),
comp::Health::new(body, 1),
comp::Poise::new(body),
inventory,
body,
)
@ -932,10 +933,11 @@ fn handle_spawn_training_dummy(
let stats = comp::Stats::new("Training Dummy".to_string());
let health = comp::Health::new(body, 0);
let poise = comp::Poise::new(body);
server
.state
.create_npc(pos, stats, health, Inventory::new_empty(), body)
.create_npc(pos, stats, health, poise, Inventory::new_empty(), body)
.with(comp::Vel(vel))
.with(comp::MountState::Unmounted)
.build();
@ -1363,6 +1365,7 @@ fn handle_explosion(
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * power,
poise_damage: 100.0,
}),
),
RadiusEffect::TerrainDestruction(power),

View File

@ -9,7 +9,7 @@ use common::{
group,
inventory::loadout::Loadout,
shockwave, Agent, Alignment, Body, Gravity, Health, HomeChunk, Inventory, Item, ItemDrop,
LightEmitter, Ori, Pos, Projectile, Scale, Stats, Vel, WaypointArea,
LightEmitter, Ori, Poise, Pos, Projectile, Scale, Stats, Vel, WaypointArea,
},
outcome::Outcome,
rtsim::RtSimEntity,
@ -49,6 +49,7 @@ pub fn handle_create_npc(
pos: Pos,
stats: Stats,
health: Health,
poise: Poise,
loadout: Loadout,
body: Body,
agent: impl Into<Option<Agent>>,
@ -71,7 +72,7 @@ pub fn handle_create_npc(
let entity = server
.state
.create_npc(pos, stats, health, inventory, body)
.create_npc(pos, stats, health, poise, inventory, body)
.with(scale)
.with(alignment);

View File

@ -14,7 +14,7 @@ use common::{
self, aura, buff,
chat::{KillSource, KillType},
object, Alignment, Body, Energy, EnergyChange, Group, Health, HealthChange, HealthSource,
Inventory, Item, Player, Pos, Stats,
Inventory, Item, Player, Poise, PoiseState, Pos, Stats,
},
effect::Effect,
lottery::Lottery,
@ -23,7 +23,7 @@ use common::{
terrain::{Block, TerrainGrid},
uid::{Uid, UidAllocator},
vol::ReadVol,
Damage, DamageSource, Explosion, GroupTarget, RadiusEffect,
Damage, DamageSource, Explosion, GroupTarget, Knockback, RadiusEffect,
};
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use common_sys::state::BlockChange;
@ -31,13 +31,89 @@ use comp::item::Reagent;
use hashbrown::HashSet;
use rand::prelude::*;
use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt};
use std::time::Duration;
use tracing::error;
use vek::Vec3;
pub fn handle_damage(server: &Server, entity: EcsEntity, change: HealthChange) {
pub fn handle_damage(server: &Server, entity: EcsEntity, change: (HealthChange, i32)) {
let ecs = &server.state.ecs();
if let Some(mut health) = ecs.write_storage::<Health>().get_mut(entity) {
health.change_by(change);
if let Some(poise) = ecs.write_storage::<Poise>().get_mut(entity) {
poise.change_by(change.1);
let was_wielded =
if let Some(character_state) = ecs.read_storage::<comp::CharacterState>().get(entity) {
character_state.is_wield()
} else {
false
};
match poise.poise_state() {
PoiseState::Normal => {},
PoiseState::Interrupted => {
poise.reset();
let _ = ecs.write_storage::<comp::CharacterState>().insert(
entity,
comp::CharacterState::Stunned(common::states::stunned::Data {
static_data: common::states::stunned::StaticData {
buildup_duration: Duration::from_millis(250),
recover_duration: Duration::from_millis(250),
knockback: Knockback::Away(0.0),
},
timer: Duration::default(),
stage_section: common::states::utils::StageSection::Buildup,
was_wielded,
}),
);
},
PoiseState::Stunned => {
poise.reset();
let _ = ecs.write_storage::<comp::CharacterState>().insert(
entity,
comp::CharacterState::Stunned(common::states::stunned::Data {
static_data: common::states::stunned::StaticData {
buildup_duration: Duration::from_millis(250),
recover_duration: Duration::from_millis(250),
knockback: Knockback::Away(0.0),
},
timer: Duration::default(),
stage_section: common::states::utils::StageSection::Buildup,
was_wielded,
}),
);
},
PoiseState::Dazed => {
poise.reset();
let _ = ecs.write_storage::<comp::CharacterState>().insert(
entity,
comp::CharacterState::Stunned(common::states::stunned::Data {
static_data: common::states::stunned::StaticData {
buildup_duration: Duration::from_millis(250),
recover_duration: Duration::from_millis(250),
knockback: Knockback::Away(0.0),
},
timer: Duration::default(),
stage_section: common::states::utils::StageSection::Buildup,
was_wielded,
}),
);
},
PoiseState::KnockedDown => {
poise.reset();
let _ = ecs.write_storage::<comp::CharacterState>().insert(
entity,
comp::CharacterState::Stunned(common::states::stunned::Data {
static_data: common::states::stunned::StaticData {
buildup_duration: Duration::from_millis(250),
recover_duration: Duration::from_millis(250),
knockback: Knockback::Away(0.0),
},
timer: Duration::default(),
stage_section: common::states::utils::StageSection::Buildup,
was_wielded,
}),
);
},
}
}
}
@ -487,6 +563,7 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
let damage = Damage {
source: DamageSource::Falling,
value: falldmg,
poise_damage: 70.0,
};
let inventories = state.ecs().read_storage::<Inventory>();
let change = damage.modify_damage(inventories.get(entity), None);

View File

@ -110,6 +110,7 @@ impl Server {
pos,
stats,
health,
poise,
loadout,
body,
agent,
@ -123,6 +124,7 @@ impl Server {
pos,
stats,
health,
poise,
loadout,
body,
agent,

View File

@ -103,6 +103,7 @@ impl<'a> System<'a> for Sys {
comp::Body::Humanoid(_) => entity.get_loadout(),
_ => LoadoutBuilder::new().build(),
},
poise: comp::Poise::new(body),
body,
agent: Some(comp::Agent::new(
None,

View File

@ -36,6 +36,7 @@ pub trait StateExt {
stats: comp::Stats,
health: comp::Health,
inventory: comp::Inventory,
poise: comp::Poise,
body: comp::Body,
) -> EcsEntityBuilder;
/// Build a static object entity
@ -95,6 +96,12 @@ impl StateExt for State {
.get_mut(entity)
.map(|mut health| health.change_by(change));
},
Effect::Poise(change) => {
self.ecs()
.write_storage::<comp::Poise>()
.get_mut(entity)
.map(|poise| poise.change_by(change));
},
Effect::Buff(buff) => {
self.ecs()
.write_storage::<comp::Buffs>()
@ -117,6 +124,7 @@ impl StateExt for State {
stats: comp::Stats,
health: comp::Health,
inventory: comp::Inventory,
poise: comp::Poise,
body: comp::Body,
) -> EcsEntityBuilder {
self.ecs_mut()
@ -149,6 +157,7 @@ impl StateExt for State {
))
.with(stats)
.with(health)
.with(poise)
.with(comp::Alignment::Npc)
.with(comp::Gravity(1.0))
.with(comp::CharacterState::default())
@ -292,6 +301,7 @@ impl StateExt for State {
);
self.write_component(entity, comp::Health::new(body, health_level));
self.write_component(entity, comp::Energy::new(body, energy_level));
self.write_component(entity, comp::Poise::new(body));
self.write_component(entity, stats);
self.write_component(entity, inventory);
self.write_component(

View File

@ -54,6 +54,7 @@ impl<'a> System<'a> for Sys {
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 500.0,
poise_damage: 60.0,
}),
),
RadiusEffect::TerrainDestruction(4.0),
@ -81,6 +82,7 @@ impl<'a> System<'a> for Sys {
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 50.0,
poise_damage: 10.0,
}),
),
RadiusEffect::TerrainDestruction(4.0),

View File

@ -2,8 +2,8 @@ use super::SysTimer;
use common::{
comp::{
Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity,
Group, Health, Inventory, Item, LightEmitter, Mass, MountState, Mounting, Ori, Player, Pos,
Scale, Shockwave, Stats, Sticky, Vel,
Group, Health, Inventory, Item, LightEmitter, Mass, MountState, Mounting, Ori, Player,
Poise, Pos, Scale, Shockwave, Stats, Sticky, Vel,
},
span,
uid::Uid,
@ -51,6 +51,7 @@ pub struct TrackedComps<'a> {
pub auras: ReadStorage<'a, Auras>,
pub energy: ReadStorage<'a, Energy>,
pub health: ReadStorage<'a, Health>,
pub poise: ReadStorage<'a, Poise>,
pub can_build: ReadStorage<'a, CanBuild>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
pub item: ReadStorage<'a, Item>,
@ -179,6 +180,7 @@ pub struct ReadTrackers<'a> {
pub auras: ReadExpect<'a, UpdateTracker<Auras>>,
pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
pub health: ReadExpect<'a, UpdateTracker<Health>>,
pub poise: ReadExpect<'a, UpdateTracker<Poise>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
pub inventory: ReadExpect<'a, UpdateTracker<Inventory>>,
@ -252,6 +254,7 @@ pub struct WriteTrackers<'a> {
auras: WriteExpect<'a, UpdateTracker<Auras>>,
energy: WriteExpect<'a, UpdateTracker<Energy>>,
health: WriteExpect<'a, UpdateTracker<Health>>,
poise: WriteExpect<'a, UpdateTracker<Poise>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
item: WriteExpect<'a, UpdateTracker<Item>>,
@ -319,6 +322,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
log_counts!(stats, "Stats");
log_counts!(energy, "Energies");
log_vounts!(health, "Healths");
log_vounts!(poise, "Poises");
log_counts!(light_emitter, "Light emitters");
log_counts!(item, "Items");
log_counts!(scale, "Scales");
@ -344,6 +348,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Auras>();
world.register_tracker::<Energy>();
world.register_tracker::<Health>();
world.register_tracker::<Poise>();
world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>();
world.register_tracker::<Item>();

View File

@ -152,6 +152,7 @@ impl<'a> System<'a> for Sys {
LoadoutBuilder::build_loadout(body, main_tool, loadout_config).build();
let health = comp::Health::new(body, entity.level.unwrap_or(0));
let poise = comp::Poise::new(stats.body_type);
let can_speak = match body {
comp::Body::Humanoid(_) => alignment == comp::Alignment::Npc,
@ -176,6 +177,7 @@ impl<'a> System<'a> for Sys {
pos: Pos(entity.pos),
stats,
health,
poise,
loadout,
agent: if entity.has_agency {
Some(comp::Agent::new(