Overhauled dash melee.

This commit is contained in:
Sam 2021-04-18 18:09:57 -04:00
parent cb817c0313
commit ce7581037c
15 changed files with 202 additions and 119 deletions

View File

@ -61,6 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- There is now a brief period after a character leaves the world where they cannot rejoin until their data is saved - There is now a brief period after a character leaves the world where they cannot rejoin until their data is saved
- Certain uses of client-authoritative physics now subject the player to server-authoritative physics. - Certain uses of client-authoritative physics now subject the player to server-authoritative physics.
- Dodge roll iframes and staff explosion are now unlocked by default, with points refunded for existing characters. - Dodge roll iframes and staff explosion are now unlocked by default, with points refunded for existing characters.
- Dash melee now stops after hitting something. Infinite dash also now replaced with dash through.
### Removed ### Removed

View File

@ -14,6 +14,6 @@ DashMelee(
charge_duration: 3.0, charge_duration: 3.0,
swing_duration: 0.35, swing_duration: 0.35,
recover_duration: 1.2, recover_duration: 1.2,
infinite_charge: true, charge_through: true,
is_interruptible: true, is_interruptible: true,
) )

View File

@ -14,6 +14,6 @@ DashMelee(
charge_duration: 1.0, charge_duration: 1.0,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 0.5, recover_duration: 0.5,
infinite_charge: true, charge_through: true,
is_interruptible: true, is_interruptible: true,
) )

View File

@ -6,14 +6,14 @@ DashMelee(
scaled_poise_damage: 0, scaled_poise_damage: 0,
base_knockback: 8.0, base_knockback: 8.0,
scaled_knockback: 7.0, scaled_knockback: 7.0,
range: 5.0, range: 4.0,
angle: 45.0, angle: 60.0,
energy_drain: 600, energy_drain: 300,
forward_speed: 3.0, forward_speed: 3.0,
buildup_duration: 0.25, buildup_duration: 0.25,
charge_duration: 0.6, charge_duration: 1.2,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 0.5, recover_duration: 0.5,
infinite_charge: true, charge_through: true,
is_interruptible: true, is_interruptible: true,
) )

View File

@ -11,9 +11,9 @@ DashMelee(
energy_drain: 0, energy_drain: 0,
forward_speed: 4.0, forward_speed: 4.0,
buildup_duration: 0.25, buildup_duration: 0.25,
charge_duration: 0.6, charge_duration: 1.2,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 0.5, recover_duration: 0.5,
infinite_charge: true, charge_through: true,
is_interruptible: true, is_interruptible: true,
) )

View File

@ -14,6 +14,6 @@ DashMelee(
charge_duration: 1.0, charge_duration: 1.0,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 0.8, recover_duration: 0.8,
infinite_charge: true, charge_through: true,
is_interruptible: false, is_interruptible: false,
) )

View File

@ -11,9 +11,9 @@ DashMelee(
energy_drain: 0, energy_drain: 0,
forward_speed: 2.0, forward_speed: 2.0,
buildup_duration: 0.5, buildup_duration: 0.5,
charge_duration: 0.4, charge_duration: 0.8,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 0.5, recover_duration: 0.5,
infinite_charge: true, charge_through: true,
is_interruptible: false, is_interruptible: false,
) )

View File

@ -14,6 +14,6 @@ DashMelee(
charge_duration: 1.2, charge_duration: 1.2,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 1.1, recover_duration: 1.1,
infinite_charge: true, charge_through: true,
is_interruptible: false, is_interruptible: false,
) )

View File

@ -11,9 +11,9 @@ DashMelee(
energy_drain: 0, energy_drain: 0,
forward_speed: 2.5, forward_speed: 2.5,
buildup_duration: 1.2, buildup_duration: 1.2,
charge_duration: 0.5, charge_duration: 1.0,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 0.5, recover_duration: 0.5,
infinite_charge: true, charge_through: true,
is_interruptible: false, is_interruptible: false,
) )

View File

@ -14,6 +14,6 @@ DashMelee(
charge_duration: 1.2, charge_duration: 1.2,
swing_duration: 0.1, swing_duration: 0.1,
recover_duration: 1.1, recover_duration: 1.1,
infinite_charge: true, charge_through: true,
is_interruptible: false, is_interruptible: false,
) )

View File

@ -189,8 +189,8 @@
"hud.skill.sw_dash_cost": "Decreases the initial cost of the dash by 25%{SP}", "hud.skill.sw_dash_cost": "Decreases the initial cost of the dash by 25%{SP}",
"hud.skill.sw_dash_speed_title": "Dash Speed", "hud.skill.sw_dash_speed_title": "Dash Speed",
"hud.skill.sw_dash_speed": "Increases how fast you go while dashing by 30%{SP}", "hud.skill.sw_dash_speed": "Increases how fast you go while dashing by 30%{SP}",
"hud.skill.sw_dash_inf_title": "Dash Infinite", "hud.skill.sw_dash_charge_through_title": "Charge Through",
"hud.skill.sw_dash_inf": "Allows you to dash for as long as you have energy{SP}", "hud.skill.sw_dash_charge_through": "Allows you to charge through the first enemies you hit{SP}",
"hud.skill.sw_dash_scale_title": "Dash Scaling Damage", "hud.skill.sw_dash_scale_title": "Dash Scaling Damage",
"hud.skill.sw_dash_scale": "Increases the damage scaling from the dash by 20%{SP}", "hud.skill.sw_dash_scale": "Increases the damage scaling from the dash by 20%{SP}",
"hud.skill.sw_spin_title": "Spin Unlock", "hud.skill.sw_spin_title": "Spin Unlock",

View File

@ -113,7 +113,7 @@ pub enum CharacterAbility {
charge_duration: f32, charge_duration: f32,
swing_duration: f32, swing_duration: f32,
recover_duration: f32, recover_duration: f32,
infinite_charge: bool, charge_through: bool,
is_interruptible: bool, is_interruptible: bool,
}, },
BasicBlock, BasicBlock,
@ -650,7 +650,7 @@ impl CharacterAbility {
ref mut base_damage, ref mut base_damage,
ref mut scaled_damage, ref mut scaled_damage,
ref mut forward_speed, ref mut forward_speed,
ref mut infinite_charge, ref mut charge_through,
.. ..
} => { } => {
*is_interruptible = skillset.has_skill(Sword(InterruptingAttacks)); *is_interruptible = skillset.has_skill(Sword(InterruptingAttacks));
@ -669,7 +669,7 @@ impl CharacterAbility {
if skillset.has_skill(Sword(DSpeed)) { if skillset.has_skill(Sword(DSpeed)) {
*forward_speed *= 1.15; *forward_speed *= 1.15;
} }
*infinite_charge = skillset.has_skill(Sword(DInfinite)); *charge_through = skillset.has_skill(Sword(DInfinite));
}, },
SpinMelee { SpinMelee {
ref mut is_interruptible, ref mut is_interruptible,
@ -1212,7 +1212,7 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
charge_duration, charge_duration,
swing_duration, swing_duration,
recover_duration, recover_duration,
infinite_charge, charge_through,
is_interruptible, is_interruptible,
} => CharacterState::DashMelee(dash_melee::Data { } => CharacterState::DashMelee(dash_melee::Data {
static_data: dash_melee::StaticData { static_data: dash_melee::StaticData {
@ -1226,7 +1226,7 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
angle: *angle, angle: *angle,
energy_drain: *energy_drain, energy_drain: *energy_drain,
forward_speed: *forward_speed, forward_speed: *forward_speed,
infinite_charge: *infinite_charge, charge_through: *charge_through,
buildup_duration: Duration::from_secs_f32(*buildup_duration), buildup_duration: Duration::from_secs_f32(*buildup_duration),
charge_duration: Duration::from_secs_f32(*charge_duration), charge_duration: Duration::from_secs_f32(*charge_duration),
swing_duration: Duration::from_secs_f32(*swing_duration), swing_duration: Duration::from_secs_f32(*swing_duration),
@ -1236,7 +1236,7 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
}, },
auto_charge: false, auto_charge: false,
timer: Duration::default(), timer: Duration::default(),
refresh_distance: 0.0, charge_end_timer: Duration::from_secs_f32(*charge_duration),
stage_section: StageSection::Buildup, stage_section: StageSection::Buildup,
exhausted: false, exhausted: false,
}), }),

View File

@ -128,7 +128,7 @@ pub enum SwordSkill {
DDamage, DDamage,
DScaling, DScaling,
DSpeed, DSpeed,
DInfinite, DInfinite, // Represents charge through, not migrated because laziness
// Spin upgrades // Spin upgrades
UnlockSpin, UnlockSpin,
SDamage, SDamage,

View File

@ -9,7 +9,6 @@ use crate::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
use vek::Vec3;
/// Separated out to condense update portions of character state /// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -34,8 +33,8 @@ pub struct StaticData {
pub energy_drain: f32, pub energy_drain: f32,
/// How quickly dasher moves forward /// How quickly dasher moves forward
pub forward_speed: f32, pub forward_speed: f32,
/// Whether state keeps charging after reaching max charge duration /// Whether the state can charge through enemies and do a second hit
pub infinite_charge: bool, pub charge_through: bool,
/// How long until state should deal damage /// How long until state should deal damage
pub buildup_duration: Duration, pub buildup_duration: Duration,
/// How long the state charges for until it reaches max damage /// How long the state charges for until it reaches max damage
@ -60,12 +59,12 @@ pub struct Data {
pub auto_charge: bool, pub auto_charge: bool,
/// Timer for each stage /// Timer for each stage
pub timer: Duration, pub timer: Duration,
/// Distance used to limit how often another attack will be applied
pub refresh_distance: f32,
/// What section the character stage is in /// What section the character stage is in
pub stage_section: StageSection, pub stage_section: StageSection,
/// Whether the state should attempt attacking again /// Whether the state should attempt attacking again
pub exhausted: bool, pub exhausted: bool,
/// Time that charge should end (used for charge through)
pub charge_end_timer: Duration,
} }
impl CharacterBehavior for Data { impl CharacterBehavior for Data {
@ -97,8 +96,7 @@ impl CharacterBehavior for Data {
} }
}, },
StageSection::Charge => { StageSection::Charge => {
if (self.static_data.infinite_charge if self.timer < self.charge_end_timer
|| self.timer < self.static_data.charge_duration)
&& (input_is_pressed(data, self.static_data.ability_info.input) && (input_is_pressed(data, self.static_data.ability_info.input)
|| (self.auto_charge && self.timer < self.static_data.charge_duration)) || (self.auto_charge && self.timer < self.static_data.charge_duration))
&& update.energy.current() > 0 && update.energy.current() > 0
@ -120,62 +118,60 @@ impl CharacterBehavior for Data {
// This logic basically just decides if a charge should end, and prevents the // This logic basically just decides if a charge should end, and prevents the
// character state spamming attacks while checking if it has hit something // character state spamming attacks while checking if it has hit something
if !self.exhausted { if !self.exhausted {
// Hit attempt (also checks if player is moving) // Hit attempt
if update.vel.0.distance_squared(Vec3::zero()) > 1.0 { let poise = AttackEffect::new(
let poise = AttackEffect::new( Some(GroupTarget::OutOfGroup),
Some(GroupTarget::OutOfGroup), CombatEffect::Poise(
CombatEffect::Poise( self.static_data.base_poise_damage as f32
self.static_data.base_poise_damage as f32 + charge_frac * self.static_data.scaled_poise_damage as f32,
+ charge_frac * self.static_data.scaled_poise_damage as f32, ),
), )
) .with_requirement(CombatRequirement::AnyDamage);
.with_requirement(CombatRequirement::AnyDamage); let knockback = AttackEffect::new(
let knockback = AttackEffect::new( Some(GroupTarget::OutOfGroup),
Some(GroupTarget::OutOfGroup), CombatEffect::Knockback(Knockback {
CombatEffect::Knockback(Knockback { strength: self.static_data.base_knockback
strength: self.static_data.base_knockback + charge_frac * self.static_data.scaled_knockback,
+ charge_frac * self.static_data.scaled_knockback, direction: KnockbackDir::Away,
direction: KnockbackDir::Away, }),
}), )
) .with_requirement(CombatRequirement::AnyDamage);
.with_requirement(CombatRequirement::AnyDamage); let buff = CombatEffect::Buff(CombatBuff::default_physical());
let buff = CombatEffect::Buff(CombatBuff::default_physical()); let damage = AttackDamage::new(
let damage = AttackDamage::new( Damage {
Damage { source: DamageSource::Melee,
source: DamageSource::Melee, value: self.static_data.base_damage as f32
value: self.static_data.base_damage as f32 + charge_frac * self.static_data.scaled_damage as f32,
+ charge_frac * self.static_data.scaled_damage as f32, },
}, Some(GroupTarget::OutOfGroup),
Some(GroupTarget::OutOfGroup), )
) .with_effect(buff);
.with_effect(buff); let (crit_chance, crit_mult) =
let (crit_chance, crit_mult) = get_crit_data(data, self.static_data.ability_info);
get_crit_data(data, self.static_data.ability_info); let attack = Attack::default()
let attack = Attack::default() .with_damage(damage)
.with_damage(damage) .with_crit(crit_chance, crit_mult)
.with_crit(crit_chance, crit_mult) .with_effect(poise)
.with_effect(poise) .with_effect(knockback)
.with_effect(knockback) .with_combo_increment();
.with_combo_increment();
data.updater.insert(data.entity, Melee { data.updater.insert(data.entity, Melee {
attack, attack,
range: self.static_data.range, range: self.static_data.range,
max_angle: self.static_data.angle.to_radians(), max_angle: self.static_data.angle.to_radians(),
applied: false, applied: false,
hit_count: 0, hit_count: 0,
break_block: data break_block: data
.inputs .inputs
.select_pos .select_pos
.map(|p| { .map(|p| {
( (
p.map(|e| e.floor() as i32), p.map(|e| e.floor() as i32),
self.static_data.ability_info.tool, self.static_data.ability_info.tool,
) )
}) })
.filter(|(_, tool)| tool == &Some(ToolKind::Pick)), .filter(|(_, tool)| tool == &Some(ToolKind::Pick)),
}); });
}
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer: self timer: self
.timer .timer
@ -185,56 +181,68 @@ impl CharacterBehavior for Data {
..*self ..*self
}) })
} else if let Some(melee) = data.melee_attack { } else if let Some(melee) = data.melee_attack {
// Creates timer ahead of time so xmac can look at line count
let timer = self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default();
// If melee attack has not applied yet, tick both duration and dsitance
if !melee.applied { if !melee.applied {
// If melee attack has not applied, just tick duration
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer, timer: self
refresh_distance: self.refresh_distance .timer
+ data.dt.0 * data.vel.0.magnitude(), .checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self ..*self
}) });
// If melee attack has applied, but hit nothing, remove
// exhausted so it can attack again
} else if melee.hit_count == 0 { } else if melee.hit_count == 0 {
// If melee attack has applied, but not hit anything, remove exhausted
// so it can attack again
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer, timer: self
refresh_distance: 0.0, .timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
exhausted: false, exhausted: false,
..*self ..*self
}) });
// Else, melee attack applied and hit something, enter } else if self.static_data.charge_through {
// cooldown // If can charge through, set charge_end_timer to stop after a little
} else if self.refresh_distance < self.static_data.range { // more time
let charge_end_timer =
if self.charge_end_timer != self.static_data.charge_duration {
self.charge_end_timer
} else {
self.timer
.checked_add(Duration::from_secs_f32(
0.2 * self.static_data.range
/ self.static_data.forward_speed,
))
.unwrap_or(self.static_data.charge_duration)
.min(self.static_data.charge_duration)
};
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer, timer: self
refresh_distance: self.refresh_distance .timer
+ data.dt.0 * data.vel.0.magnitude(), .checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
charge_end_timer,
..*self ..*self
}) });
// Else cooldown has finished, remove exhausted
} else { } else {
// Stop charging now and go to swing stage section
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer, timer: Duration::default(),
refresh_distance: 0.0, stage_section: StageSection::Swing,
exhausted: false, exhausted: false,
..*self ..*self
}) });
} }
} else { } else {
// If melee attack has not applied, just tick duration
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer: self timer: self
.timer .timer
.checked_add(Duration::from_secs_f32(data.dt.0)) .checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(), .unwrap_or_default(),
refresh_distance: 0.0,
exhausted: false, exhausted: false,
..*self ..*self
}) });
} }
// Consumes energy if there's enough left and charge has not stopped // Consumes energy if there's enough left and charge has not stopped
@ -247,12 +255,82 @@ impl CharacterBehavior for Data {
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer: Duration::default(), timer: Duration::default(),
stage_section: StageSection::Swing, stage_section: StageSection::Swing,
exhausted: false,
..*self ..*self
}); });
} }
}, },
StageSection::Swing => { StageSection::Swing => {
if self.timer < self.static_data.swing_duration { if self.static_data.charge_through && !self.exhausted {
// If can charge through and not exhausted, do one more melee attack
// Assumes charge got to charge_end_timer for damage calculations
let charge_frac = (self.charge_end_timer.as_secs_f32()
/ self.static_data.charge_duration.as_secs_f32())
.min(1.0);
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(
self.static_data.base_poise_damage as f32
+ charge_frac * self.static_data.scaled_poise_damage as f32,
),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.base_knockback
+ charge_frac * self.static_data.scaled_knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let (crit_chance, crit_mult) =
get_crit_data(data, self.static_data.ability_info);
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(poise)
.with_effect(knockback)
.with_combo_increment();
data.updater.insert(data.entity, Melee {
attack,
range: self.static_data.range,
max_angle: self.static_data.angle.to_radians(),
applied: false,
hit_count: 0,
break_block: data
.inputs
.select_pos
.map(|p| {
(
p.map(|e| e.floor() as i32),
self.static_data.ability_info.tool,
)
})
.filter(|(_, tool)| tool == &Some(ToolKind::Pick)),
});
update.character = CharacterState::DashMelee(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
exhausted: true,
..*self
})
} else if self.timer < self.static_data.swing_duration {
// Swings // Swings
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {
timer: self timer: self

View File

@ -1387,7 +1387,7 @@ impl<'a> Widget for Diary<'a> {
}; };
let skill = Skill::Sword(DInfinite); let skill = Skill::Sword(DInfinite);
if create_skill_button( if create_skill_button(
self.imgs.physical_infinite_skill, self.imgs.physical_distance_skill,
state.skills_top_r[5], state.skills_top_r[5],
&self.skill_set, &self.skill_set,
skill, skill,
@ -1396,9 +1396,13 @@ impl<'a> Widget for Diary<'a> {
) )
.with_tooltip( .with_tooltip(
self.tooltip_manager, self.tooltip_manager,
&self.localized_strings.get("hud.skill.sw_dash_inf_title"), &self
.localized_strings
.get("hud.skill.sw_dash_charge_through_title"),
&add_sp_cost_tooltip( &add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sw_dash_inf"), &self
.localized_strings
.get("hud.skill.sw_dash_charge_through"),
skill, skill,
&self.skill_set, &self.skill_set,
&self.localized_strings, &self.localized_strings,