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
- 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.
- Dash melee now stops after hitting something. Infinite dash also now replaced with dash through.
### Removed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,6 @@ DashMelee(
charge_duration: 1.2,
swing_duration: 0.1,
recover_duration: 1.1,
infinite_charge: true,
charge_through: true,
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_speed_title": "Dash Speed",
"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_inf": "Allows you to dash for as long as you have energy{SP}",
"hud.skill.sw_dash_charge_through_title": "Charge Through",
"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": "Increases the damage scaling from the dash by 20%{SP}",
"hud.skill.sw_spin_title": "Spin Unlock",

View File

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

View File

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

View File

@ -9,7 +9,6 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use vek::Vec3;
/// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -34,8 +33,8 @@ pub struct StaticData {
pub energy_drain: f32,
/// How quickly dasher moves forward
pub forward_speed: f32,
/// Whether state keeps charging after reaching max charge duration
pub infinite_charge: bool,
/// Whether the state can charge through enemies and do a second hit
pub charge_through: bool,
/// How long until state should deal damage
pub buildup_duration: Duration,
/// How long the state charges for until it reaches max damage
@ -60,12 +59,12 @@ pub struct Data {
pub auto_charge: bool,
/// Timer for each stage
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
pub stage_section: StageSection,
/// Whether the state should attempt attacking again
pub exhausted: bool,
/// Time that charge should end (used for charge through)
pub charge_end_timer: Duration,
}
impl CharacterBehavior for Data {
@ -97,8 +96,7 @@ impl CharacterBehavior for Data {
}
},
StageSection::Charge => {
if (self.static_data.infinite_charge
|| self.timer < self.static_data.charge_duration)
if self.timer < self.charge_end_timer
&& (input_is_pressed(data, self.static_data.ability_info.input)
|| (self.auto_charge && self.timer < self.static_data.charge_duration))
&& 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
// character state spamming attacks while checking if it has hit something
if !self.exhausted {
// Hit attempt (also checks if player is moving)
if update.vel.0.distance_squared(Vec3::zero()) > 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();
// Hit attempt
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)),
});
}
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
@ -185,56 +181,68 @@ impl CharacterBehavior for Data {
..*self
})
} 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 attack has not applied, just tick duration
update.character = CharacterState::DashMelee(Data {
timer,
refresh_distance: self.refresh_distance
+ data.dt.0 * data.vel.0.magnitude(),
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
})
// If melee attack has applied, but hit nothing, remove
// exhausted so it can attack again
});
} 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 {
timer,
refresh_distance: 0.0,
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
exhausted: false,
..*self
})
// Else, melee attack applied and hit something, enter
// cooldown
} else if self.refresh_distance < self.static_data.range {
});
} else if self.static_data.charge_through {
// If can charge through, set charge_end_timer to stop after a little
// 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 {
timer,
refresh_distance: self.refresh_distance
+ data.dt.0 * data.vel.0.magnitude(),
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
charge_end_timer,
..*self
})
// Else cooldown has finished, remove exhausted
});
} else {
// Stop charging now and go to swing stage section
update.character = CharacterState::DashMelee(Data {
timer,
refresh_distance: 0.0,
timer: Duration::default(),
stage_section: StageSection::Swing,
exhausted: false,
..*self
})
});
}
} else {
// If melee attack has not applied, just tick duration
update.character = CharacterState::DashMelee(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
refresh_distance: 0.0,
exhausted: false,
..*self
})
});
}
// 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 {
timer: Duration::default(),
stage_section: StageSection::Swing,
exhausted: false,
..*self
});
}
},
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
update.character = CharacterState::DashMelee(Data {
timer: self

View File

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