Addressed feedback from testing

This commit is contained in:
Sam 2022-10-23 21:12:59 -04:00
parent b5682c4682
commit 0fe073fcdc
67 changed files with 440 additions and 235 deletions

View File

@ -4,9 +4,9 @@ ChargedRanged(
initial_regen: 2,
scaled_regen: 14,
initial_damage: 2,
scaled_damage: 14,
scaled_damage: 12,
initial_knockback: 0,
scaled_knockback: 15,
scaled_knockback: 12,
buildup_duration: 0.25,
charge_duration: 1.0,
recover_duration: 0.4,

View File

@ -1,5 +1,5 @@
RepeaterRanged(
energy_cost: 5.0,
energy_cost: 6.0,
buildup_duration: 0.2,
shoot_duration: 0.3,
recover_duration: 0.5,

View File

@ -48,7 +48,7 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 6,
energy_cost_per_strike: 5,
meta: (
kind: Some(Sword(Cleaving)),
),

View File

@ -3,10 +3,10 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 20,
damage: 16,
poise: 0,
knockback: 0,
energy_regen: 10,
energy_regen: 5,
),
range: 6.0,
angle: 360.0,

View File

@ -14,7 +14,7 @@ ComboMelee2(
kind: Bleeding,
dur_secs: 10.0,
strength: DamageFraction(0.2),
chance: 0.25,
chance: 0.1,
))),
),
buildup_duration: 0.2,
@ -26,7 +26,7 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 22,
damage: 14,
poise: 0,
knockback: 0,
energy_regen: 13,
@ -37,18 +37,18 @@ ComboMelee2(
kind: Bleeding,
dur_secs: 10.0,
strength: DamageFraction(0.2),
chance: 0.5,
chance: 0.25,
))),
),
buildup_duration: 0.3,
swing_duration: 0.2,
buildup_duration: 0.2,
swing_duration: 0.1,
hit_timing: 0.5,
recover_duration: 0.4,
recover_duration: 0.3,
ori_modifier: 0.6,
),
],
is_stance: true,
energy_cost_per_strike: 5,
energy_cost_per_strike: 4,
meta: (
kind: Some(Sword(Crippling)),
),

View File

@ -15,13 +15,13 @@ FinisherMelee(
damage_effect: Some(Buff((
kind: Bleeding,
dur_secs: 10.0,
strength: DamageFraction(0.5),
strength: DamageFraction(0.25),
chance: 1.0,
))),
),
scaling: Some((
target: Buff,
kind: Linear,
kind: Sqrt,
)),
minimum_combo: 10,
meta: (

View File

@ -3,7 +3,7 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 20,
damage: 14,
poise: 0,
knockback: 0,
energy_regen: 5,

View File

@ -3,15 +3,15 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 10,
damage: 8,
poise: 0,
knockback: 0,
energy_regen: 6,
energy_regen: 8,
),
range: 3.0,
angle: 45.0,
),
buildup_duration: 0.4,
buildup_duration: 0.3,
swing_duration: 0.1,
hit_timing: 0.5,
recover_duration: 0.5,
@ -36,7 +36,7 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 3,
energy_cost_per_strike: 2,
meta: (
kind: Some(Sword(Defensive)),
capabilities: (

View File

@ -18,7 +18,7 @@ ComboMelee2(
movement: (
buildup: None,
swing: None,
recover: Some(Reverse(1.0)),
recover: Some(Reverse(1.5)),
),
ori_modifier: 0.6,
),

View File

@ -4,8 +4,8 @@ ComboMelee2(
melee_constructor: (
kind: Slash(
damage: 18,
poise: 0,
knockback: 0,
poise: 5,
knockback: 5,
energy_regen: 10,
),
range: 3.0,
@ -21,8 +21,8 @@ ComboMelee2(
melee_constructor: (
kind: Slash(
damage: 30,
poise: 0,
knockback: 0,
poise: 5,
knockback: 5,
energy_regen: 15,
),
range: 3.0,
@ -36,12 +36,12 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 5,
energy_cost_per_strike: 4,
meta: (
kind: Some(Sword(Heavy)),
capabilities: (
// Poise resistant during attack
bits: 0b00001000,
// Poise and knockback resistant during attack
bits: 0b00011000,
),
),
)

View File

@ -5,8 +5,8 @@ FinisherMelee(
recover_duration: 0.4,
melee_constructor: (
kind: Bash(
damage: 30,
poise: 30,
damage: 40,
poise: 40,
knockback: 0,
energy_regen: 10,
),

View File

@ -4,7 +4,7 @@ ComboMelee2(
melee_constructor: (
kind: Bash(
damage: 20,
poise: 20,
poise: 25,
knockback: 0,
energy_regen: 5,
),

View File

@ -70,7 +70,7 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 3,
energy_cost_per_strike: 2,
meta: (
kind: Some(Sword(Mobility)),
capabilities: (

View File

@ -58,7 +58,7 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 4,
energy_cost_per_strike: 3,
meta: (
kind: Some(Sword(Offensive)),
),

View File

@ -21,7 +21,7 @@ FinisherMelee(
),
scaling: Some((
target: Attack,
kind: Linear,
kind: Sqrt,
)),
minimum_combo: 10,
meta: (

View File

@ -3,7 +3,7 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 7,
damage: 6,
poise: 0,
knockback: 0,
energy_regen: 9,
@ -20,7 +20,7 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 13,
damage: 11,
poise: 0,
knockback: 0,
energy_regen: 13,
@ -36,7 +36,7 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 6,
energy_cost_per_strike: 5,
meta: (
kind: Some(Sword(Parrying)),
capabilities: (

View File

@ -3,7 +3,7 @@ ComboMelee2(
(
melee_constructor: (
kind: Slash(
damage: 25,
damage: 30,
poise: 0,
knockback: 0,
energy_regen: 5,

View File

@ -7,7 +7,7 @@ BasicBlock(
buildup: true,
recover: true,
),
energy_cost: 10,
energy_cost: 15,
can_hold: false,
meta: (
kind: Some(Sword(Parrying)),

View File

@ -1,8 +1,8 @@
RiposteMelee(
energy_cost: 20,
buildup_duration: 0.2,
buildup_duration: 0.3,
swing_duration: 0.2,
recover_duration: 0.4,
recover_duration: 0.3,
melee_constructor: (
kind: Slash(
damage: 25,

View File

@ -36,7 +36,7 @@ ComboMelee2(
),
],
is_stance: true,
energy_cost_per_strike: 5,
energy_cost_per_strike: 4,
meta: (
kind: Some(Sword(Reaching)),
),

View File

@ -7,12 +7,12 @@ RapidMelee(
damage: 10,
poise: 0,
knockback: 0,
energy_regen: 5,
energy_regen: 4,
),
range: 4.5,
angle: 10.0,
),
energy_cost: 10,
energy_cost: 8,
max_strikes: 6,
meta: (
kind: Some(Sword(Reaching)),

View File

@ -6,12 +6,12 @@ ItemDef(
toolkind: Sword,
stats: (
equip_time_secs: 1.1,
power: 1.3,
effect_power: 1.2,
speed: 0.7,
power: 1.1,
effect_power: 1.1,
speed: 0.9,
crit_chance: 0.9,
range: 1.1,
energy_efficiency: 0.8,
energy_efficiency: 0.9,
buff_strength: 1.1,
),
hand_restriction: Some(Two),

View File

@ -6,12 +6,12 @@ ItemDef(
toolkind: Sword,
stats: (
equip_time_secs: 0.9,
power: 0.7,
effect_power: 0.8,
speed: 1.3,
power: 0.9,
effect_power: 0.9,
speed: 1.1,
crit_chance: 1.1,
range: 0.9,
energy_efficiency: 1.2,
energy_efficiency: 1.1,
buff_strength: 0.9,
),
hand_restriction: Some(One),

BIN
assets/voxygen/element/de_buffs/buff_fortitude_0.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/de_buffs/debuff_parried_0.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,7 @@ common-abilities-sceptre-wardingaura = Warding Aura
common-abilities-sword-balanced_combo = Balanced Stance
.desc = This stance has few downsides, but is not particularly special.
common-abilities-sword-balanced_thrust = Sword Thrust
.desc = Extend yourself to hit those a little further away.
.desc = Charge a thrust to hit enemies with a longer distance.
common-abilities-sword-balanced_finisher = Finisher
.desc = A powerful strike you can use after fighting long enough.
common-abilities-sword-offensive_combo = Offensive Stance

View File

@ -2,6 +2,7 @@ use crate::comp::buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource};
#[cfg(not(target_arch = "wasm32"))]
use crate::{
comp::{
ability::Capability,
inventory::{
item::{armor::Protection, tool::ToolKind, ItemDesc, ItemKind, MaterialStatManifest},
slot::EquipSlot,
@ -334,7 +335,8 @@ impl Attack {
for effect in damage.effects.iter() {
match effect {
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir) * strength_modifier;
let impulse =
kb.calculate_impulse(dir, target.char_state) * strength_modifier;
if !impulse.is_approx_zero() {
emit(ServerEvent::Knockback {
entity: target.entity,
@ -432,7 +434,10 @@ impl Attack {
},
CombatEffect::BuildupsVulnerable => {
if target.char_state.map_or(false, |cs| {
matches!(cs.stage_section(), Some(StageSection::Buildup))
matches!(
cs.stage_section(),
Some(StageSection::Buildup | StageSection::Charge)
)
}) {
emit(ServerEvent::HealthChange {
entity: target.entity,
@ -496,7 +501,8 @@ impl Attack {
is_applied = true;
match effect.effect {
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir) * strength_modifier;
let impulse =
kb.calculate_impulse(dir, target.char_state) * strength_modifier;
if !impulse.is_approx_zero() {
emit(ServerEvent::Knockback {
entity: target.entity,
@ -951,18 +957,26 @@ pub enum KnockbackDir {
#[cfg(not(target_arch = "wasm32"))]
impl Knockback {
pub fn calculate_impulse(self, dir: Dir) -> Vec3<f32> {
// TEMP until source knockback values have been updated
50.0 * match self.direction {
KnockbackDir::Away => self.strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
KnockbackDir::Towards => {
self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5)
},
KnockbackDir::Up => self.strength * Vec3::unit_z(),
KnockbackDir::TowardsUp => {
self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85)
},
}
pub fn calculate_impulse(self, dir: Dir, char_state: Option<&CharacterState>) -> Vec3<f32> {
let from_char = {
let resistant = char_state
.and_then(|cs| cs.ability_info())
.and_then(|a| a.ability_meta)
.map_or(false, |a| {
a.capabilities.contains(Capability::KNOCKBACK_RESISTANT)
});
if resistant { 0.5 } else { 1.0 }
};
// TEMP: 50.0 multiplication kept until source knockback values have been
// updated
50.0 * self.strength
* from_char
* match self.direction {
KnockbackDir::Away => *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
KnockbackDir::Towards => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5),
KnockbackDir::Up => Vec3::unit_z(),
KnockbackDir::TowardsUp => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85),
}
}
#[must_use]

View File

@ -3,6 +3,7 @@ use crate::{
combat::{self, CombatEffect, DamageKind, Knockback},
comp::{
self, aura, beam, buff,
character_state::AttackImmunities,
inventory::{
item::{
tool::{AbilityContext, AbilityItem, AuxiliaryAbilityKind, Stats, ToolKind},
@ -534,7 +535,7 @@ pub enum CharacterAbility {
movement_duration: f32,
recover_duration: f32,
roll_strength: f32,
immune_melee: bool,
attack_immunities: AttackImmunities,
#[serde(default)]
meta: AbilityMeta,
},
@ -871,8 +872,15 @@ impl CharacterAbility {
buildup_duration: 0.05,
movement_duration: 0.33,
recover_duration: 0.125,
roll_strength: 2.0,
immune_melee: true,
roll_strength: 2.5,
attack_immunities: AttackImmunities {
melee: true,
projectiles: false,
beams: true,
ground_shockwaves: false,
air_shockwaves: true,
explosions: true,
},
meta: Default::default(),
}
}
@ -1002,7 +1010,7 @@ impl CharacterAbility {
ref mut movement_duration,
ref mut recover_duration,
roll_strength: _,
immune_melee: _,
attack_immunities: _,
meta: _,
} => {
*buildup_duration /= stats.speed;
@ -1346,7 +1354,7 @@ impl CharacterAbility {
self
}
pub fn get_energy_cost(&self) -> f32 {
pub fn energy_cost(&self) -> f32 {
use CharacterAbility::*;
match self {
BasicMelee { energy_cost, .. }
@ -1386,6 +1394,46 @@ impl CharacterAbility {
}
}
#[allow(clippy::bool_to_int_with_if)]
pub fn combo_cost(&self) -> u32 {
use CharacterAbility::*;
match self {
BasicAura {
scales_with_combo, ..
} => {
if *scales_with_combo {
1
} else {
0
}
},
FinisherMelee { minimum_combo, .. } => *minimum_combo,
BasicMelee { .. }
| BasicRanged { .. }
| RepeaterRanged { .. }
| DashMelee { .. }
| Roll { .. }
| LeapMelee { .. }
| SpinMelee { .. }
| ChargedMelee { .. }
| ChargedRanged { .. }
| Shockwave { .. }
| BasicBlock { .. }
| SelfBuff { .. }
| ComboMelee2 { .. }
| DiveMelee { .. }
| RiposteMelee { .. }
| RapidMelee { .. }
| BasicBeam { .. }
| Boost { .. }
| ComboMelee { .. }
| Blink { .. }
| Music { .. }
| BasicSummon { .. }
| SpriteSummon { .. } => 0,
}
}
// TODO: Maybe consider making CharacterAbility a struct at some point?
pub fn ability_meta(&self) -> AbilityMeta {
use CharacterAbility::*;
@ -2060,7 +2108,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
movement_duration,
recover_duration,
roll_strength,
immune_melee,
attack_immunities,
meta: _,
} => CharacterState::Roll(roll::Data {
static_data: roll::StaticData {
@ -2068,7 +2116,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
movement_duration: Duration::from_secs_f32(*movement_duration),
recover_duration: Duration::from_secs_f32(*recover_duration),
roll_strength: *roll_strength,
immune_melee: *immune_melee,
attack_immunities: *attack_immunities,
ability_info,
},
timer: Duration::default(),
@ -2120,10 +2168,15 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
exhausted: false,
start_next_strike: false,
timer: Duration::default(),
// If ability is a stance, enter the stance without beginning a strike, otherwise
// immediately begin the strike
// If ability is a stance, if starting from wielding, get ready to enter stance,
// otherwise enter stance immediately, otherwise if not a stance immediately begin
// the strike
stage_section: if *is_stance {
Some(StageSection::Ready)
if matches!(data.character, CharacterState::Wielding(_)) {
Some(StageSection::Ready)
} else {
None
}
} else {
Some(StageSection::Buildup)
},
@ -2608,12 +2661,14 @@ bitflags::bitflags! {
#[derive(Default, Serialize, Deserialize)]
pub struct Capability: u8 {
// Allows rolls to interrupt the ability at any point, not just during buildup
const ROLL_INTERRUPT = 0b00000001;
const ROLL_INTERRUPT = 0b00000001;
// Allows blocking to interrupt the ability at any point
const BLOCK_INTERRUPT = 0b00000010;
const BLOCK_INTERRUPT = 0b00000010;
// When the ability is in the buildup section, it counts as a parry
const BUILDUP_PARRIES = 0b00000100;
const BUILDUP_PARRIES = 0b00000100;
// When in the ability, an entity only receives half as much poise damage
const POISE_RESISTANT = 0b00001000;
const POISE_RESISTANT = 0b00001000;
// WHen in the ability, an entity only receives half as much knockback
const KNOCKBACK_RESISTANT = 0b00010000;
}
}

View File

@ -828,7 +828,12 @@ impl Body {
pub fn immune_to(&self, buff: BuffKind) -> bool {
match buff {
BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_) | Body::Ship(_)),
BuffKind::Bleeding => match self {
Body::Object(_) | Body::Golem(_) | Body::Ship(_) => true,
Body::BipedSmall(b) => matches!(b.species, biped_small::Species::Husk),
Body::BipedLarge(b) => matches!(b.species, biped_large::Species::Huskbrute),
_ => false,
},
BuffKind::Burning => match self {
Body::Golem(g) => matches!(g.species, golem::Species::ClayGolem),
Body::BipedSmall(b) => matches!(b.species, biped_small::Species::Haniwa),

View File

@ -323,8 +323,12 @@ impl CharacterState {
pub fn is_music(&self) -> bool { matches!(self, CharacterState::Music(_)) }
pub fn is_melee_dodge(&self) -> bool {
matches!(self, CharacterState::Roll(d) if d.static_data.immune_melee)
pub fn attack_immunities(&self) -> Option<AttackImmunities> {
if let CharacterState::Roll(c) = self {
Some(c.static_data.attack_immunities)
} else {
None
}
}
pub fn is_stunned(&self) -> bool { matches!(self, CharacterState::Stunned(_)) }
@ -854,6 +858,16 @@ pub struct DurationsInfo {
pub ready: Option<Duration>,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq)]
pub struct AttackImmunities {
pub melee: bool,
pub projectiles: bool,
pub beams: bool,
pub ground_shockwaves: bool,
pub air_shockwaves: bool,
pub explosions: bool,
}
impl Default for CharacterState {
fn default() -> Self {
Self::Idle(idle::Data {

View File

@ -145,7 +145,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -219,7 +219,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -111,7 +111,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -132,7 +132,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -137,7 +137,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -166,7 +166,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -182,7 +182,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -355,7 +355,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -106,7 +106,7 @@ pub struct Data {
}
pub const STANCE_ENTER_TIME: Duration = Duration::from_millis(250);
pub const STANCE_LEAVE_TIME: Duration = Duration::from_secs(20);
pub const STANCE_LEAVE_TIME: Duration = Duration::from_secs(30);
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
@ -127,7 +127,7 @@ impl CharacterBehavior for Data {
1.0
};
handle_move(data, &mut update, move_eff);
let interrupted = handle_interrupts(data, &mut update, Some(ability_input));
let interrupted = handle_interrupts(data, &mut update);
let strike_data = self.strike_data();

View File

@ -257,7 +257,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -50,6 +50,7 @@ impl CharacterBehavior for Data {
match self.stage_section {
StageSection::Movement => {
handle_move(data, &mut update, 1.0);
if data.physics.on_ground.is_some() {
// Transitions to swing portion of state upon hitting ground
if let CharacterState::DiveMelee(c) = &mut update.character {

View File

@ -51,7 +51,7 @@ impl CharacterBehavior for Data {
handle_orientation(data, &mut update, 1.0, None);
handle_move(data, &mut update, 0.7);
handle_jump(data, output_events, &mut update, 1.0);
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
match self.stage_section {
StageSection::Buildup => {
@ -88,6 +88,9 @@ impl CharacterBehavior for Data {
self.static_data.combo_on_use as f32
/ self.static_data.minimum_combo as f32
},
ScalingKind::Sqrt => {
(self.static_data.combo_on_use as f32 / self.static_data.minimum_combo as f32).sqrt()
},
};
match scaling.target {
ScalingTarget::Attack => {
@ -155,6 +158,8 @@ pub enum ScalingKind {
// Reaches a scaling of 1 when at minimum combo, and a scaling of 2 when at double minimum
// combo
Linear,
// Reaches a scaling of 1 when at minimum combo, and a scaling of 2 when at 4x minimum combo
Sqrt,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -149,7 +149,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -48,7 +48,7 @@ impl CharacterBehavior for Data {
handle_orientation(data, &mut update, 1.0, None);
handle_move(data, &mut update, 0.7);
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
match self.stage_section {
StageSection::Buildup => {

View File

@ -158,7 +158,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -43,7 +43,7 @@ impl CharacterBehavior for Data {
handle_orientation(data, &mut update, 1.0, None);
handle_move(data, &mut update, 0.7);
handle_jump(data, output_events, &mut update, 1.0);
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
match self.stage_section {
StageSection::Buildup => {

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
buff::{BuffChange, BuffKind},
character_state::OutputEvents,
character_state::{OutputEvents, AttackImmunities},
CharacterState, InputKind, StateUpdate,
},
event::ServerEvent,
@ -24,8 +24,8 @@ pub struct StaticData {
pub recover_duration: Duration,
/// Affects the speed and distance of the roll
pub roll_strength: f32,
/// Affects whether you are immune to melee attacks while rolling
pub immune_melee: bool,
/// Affects whether you are immune to various attacks while rolling
pub attack_immunities: AttackImmunities,
/// Information about the ability
pub ability_info: AbilityInfo,
}

View File

@ -114,7 +114,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -167,7 +167,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -161,7 +161,7 @@ impl CharacterBehavior for Data {
}
// At end of state logic so an interrupt isn't overwritten
handle_interrupts(data, &mut update, None);
handle_interrupts(data, &mut update);
update
}

View File

@ -1096,40 +1096,28 @@ pub fn handle_dodge_input(data: &JoinData<'_>, update: &mut StateUpdate) -> bool
pub fn handle_interrupts(
data: &JoinData,
update: &mut StateUpdate,
// Used when an input other than the one that activated the ability being pressed should block
// an interrupt
input_override: Option<InputKind>,
) -> bool {
// Check that the input used to enter current character state (if there was one)
// is not pressed
if input_override
.or_else(|| data.character.ability_info().and_then(|a| a.input))
.map_or(true, |input| !input_is_pressed(data, input))
{
let can_dodge = {
let in_buildup = data
.character
.stage_section()
.map_or(true, |stage_section| {
matches!(stage_section, StageSection::Buildup)
});
let interruptible = data.character.ability_info().and_then(|info| info.ability_meta).map_or(false, |meta| {
meta.capabilities
.contains(Capability::ROLL_INTERRUPT)
let can_dodge = {
let in_buildup = data
.character
.stage_section()
.map_or(true, |stage_section| {
matches!(stage_section, StageSection::Buildup)
});
in_buildup || interruptible
};
let can_block = data.character.ability_info().and_then(|info| info.ability_meta).map_or(false, |meta| {
let interruptible = data.character.ability_info().and_then(|info| info.ability_meta).map_or(false, |meta| {
meta.capabilities
.contains(Capability::BLOCK_INTERRUPT)
.contains(Capability::ROLL_INTERRUPT)
});
if can_dodge {
handle_dodge_input(data, update)
} else if can_block {
handle_block_input(data, update)
} else {
false
}
in_buildup || interruptible
};
let can_block = data.character.ability_info().and_then(|info| info.ability_meta).map_or(false, |meta| {
meta.capabilities
.contains(Capability::BLOCK_INTERRUPT)
});
if can_dodge {
handle_dodge_input(data, update)
} else if can_block {
handle_block_input(data, update)
} else {
false
}
@ -1358,6 +1346,9 @@ impl AbilityInfo {
None
};
// If this ability should not be returned to, check if this ability was going to return to another ability, and return to that one instead
let return_ability = return_ability.or_else(|| char_state.ability_info().and_then(|info| info.return_ability));
Self {
tool: None,
hand: None,

View File

@ -229,6 +229,11 @@ impl<'a> System<'a> for Sys {
energy: read_data.energies.get(target),
};
let target_dodging = read_data
.character_states
.get(target)
.and_then(|cs| cs.attack_immunities())
.map_or(false, |i| i.beams);
// PvP check
let may_harm = combat::may_harm(
&read_data.alignments,
@ -238,8 +243,7 @@ impl<'a> System<'a> for Sys {
target,
);
let attack_options = AttackOptions {
// No luck with dodging beams
target_dodging: false,
target_dodging,
may_harm,
target_group,
};

View File

@ -136,7 +136,8 @@ impl<'a> System<'a> for Sys {
let target_dodging = read_data
.char_states
.get(target)
.map_or(false, |c_s| c_s.is_melee_dodge());
.and_then(|cs| cs.attack_immunities())
.map_or(false, |i| i.melee);
// Check if it is a hit
if attacker != target

View File

@ -307,10 +307,13 @@ fn dispatch_hit(
target,
);
let target_dodging = read_data
.character_states
.get(target)
.and_then(|cs| cs.attack_immunities())
.map_or(false, |i| i.projectiles);
let attack_options = AttackOptions {
// They say witchers can dodge arrows,
// but we don't have witchers
target_dodging: false,
target_dodging,
may_harm,
target_group: projectile_target_info.target_group,
};

View File

@ -211,6 +211,17 @@ impl<'a> System<'a> for Sys {
energy: read_data.energies.get(target),
};
let target_dodging = read_data
.character_states
.get(target)
.and_then(|cs| cs.attack_immunities())
.map_or(false, |i| {
if shockwave.requires_ground {
i.ground_shockwaves
} else {
i.air_shockwaves
}
});
// PvP check
let may_harm = combat::may_harm(
&read_data.alignments,
@ -220,8 +231,7 @@ impl<'a> System<'a> for Sys {
target,
);
let attack_options = AttackOptions {
// Trying roll during earthquake isn't the best idea
target_dodging: false,
target_dodging,
may_harm,
target_group,
};

View File

@ -9,6 +9,7 @@ use common::{
},
event::{EventBus, ServerEvent},
resources::{DeltaTime, EntitiesDiedLastTick, Time},
states,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
@ -154,7 +155,11 @@ impl<'a> System<'a> for Sys {
| CharacterState::GlideWield(_)
| CharacterState::Wielding(_)
| CharacterState::Equipping(_)
| CharacterState::Boost(_) => {
| CharacterState::Boost(_)
| CharacterState::ComboMelee2(states::combo_melee2::Data {
stage_section: None,
..
}) => {
let res = { energy.current() < energy.maximum() };
if res {

View File

@ -1410,7 +1410,7 @@ impl<'a> AgentData<'a> {
// Use shotgun if target close and have sufficient energy
controller.push_basic_input(InputKind::Ability(0));
} else if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
&& self.energy.current() > CharacterAbility::default_roll().get_energy_cost()
&& self.energy.current() > CharacterAbility::default_roll().energy_cost()
&& !matches!(self.char_state, CharacterState::BasicRanged(c) if !matches!(c.stage_section, StageSection::Recover))
{
// Else roll away if can roll and have enough energy, and not using shotgun
@ -1536,10 +1536,10 @@ impl<'a> AgentData<'a> {
CharacterAbility::BasicBeam { range, .. } => range,
_ => 20.0_f32,
};
let shockwave_cost = shockwave.get_energy_cost();
let shockwave_cost = shockwave.energy_cost();
if self.body.map_or(false, |b| b.is_humanoid())
&& attack_data.in_min_range()
&& self.energy.current() > CharacterAbility::default_roll().get_energy_cost()
&& self.energy.current() > CharacterAbility::default_roll().energy_cost()
&& !matches!(self.char_state, CharacterState::Shockwave(_))
{
// if a humanoid, have enough stamina, not in shockwave, and in melee range,
@ -1576,7 +1576,7 @@ impl<'a> AgentData<'a> {
[ActionStateConditions::ConditionStaffCanShockwave as usize] = true;
}
} else if self.energy.current()
> shockwave_cost + CharacterAbility::default_roll().get_energy_cost()
> shockwave_cost + CharacterAbility::default_roll().energy_cost()
&& attack_data.dist_sqrd < flamethrower_range.powi(2)
{
controller.push_basic_input(InputKind::Secondary);
@ -1713,7 +1713,7 @@ impl<'a> AgentData<'a> {
}
} else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
if self.body.map_or(false, |b| b.is_humanoid())
&& self.energy.current() > CharacterAbility::default_roll().get_energy_cost()
&& self.energy.current() > CharacterAbility::default_roll().energy_cost()
&& !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
{
// Else roll away if can roll and have enough energy, and not using aura or in
@ -3610,7 +3610,7 @@ impl<'a> AgentData<'a> {
}
} else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
if self.body.map_or(false, |b| b.is_humanoid())
&& self.energy.current() > CharacterAbility::default_roll().get_energy_cost()
&& self.energy.current() > CharacterAbility::default_roll().energy_cost()
&& !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
{
// Else use steam beam

View File

@ -543,16 +543,23 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
let ecs = server.state.ecs();
if vel.z <= -30.0 {
let char_states = ecs.read_storage::<CharacterState>();
let reduced_vel_z = if let Some(CharacterState::DiveMelee(c)) = char_states.get(entity) {
(vel.z + c.static_data.vertical_speed).min(0.0)
} else {
vel.z
};
let mass = ecs
.read_storage::<comp::Mass>()
.get(entity)
.copied()
.unwrap_or_default();
let impact_energy = mass.0 * vel.z.powi(2) / 2.0;
let impact_energy = mass.0 * reduced_vel_z.powi(2) / 2.0;
let falldmg = impact_energy / 1000.0;
let inventories = ecs.read_storage::<Inventory>();
let char_states = ecs.read_storage::<CharacterState>();
let stats = ecs.read_storage::<Stats>();
let time = ecs.read_resource::<Time>();
let msm = ecs.read_resource::<MaterialStatManifest>();
@ -583,7 +590,7 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
server_eventbus.emit_now(ServerEvent::HealthChange { entity, change });
// Emit poise change
let poise_damage = -(mass.0 * vel.magnitude_squared() / 1500.0);
let poise_damage = -(mass.0 * reduced_vel_z.powi(2) / 1500.0);
let poise_change = Poise::apply_poise_reduction(
poise_damage,
inventories.get(entity),
@ -929,6 +936,9 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
energy: energies.get(entity_b),
};
let target_dodging = char_state_b_maybe
.and_then(|cs| cs.attack_immunities())
.map_or(false, |i| i.explosions);
// PvP check
let may_harm = combat::may_harm(
alignments,
@ -938,9 +948,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
entity_b,
);
let attack_options = combat::AttackOptions {
// cool guyz maybe don't look at explosions
// but they still got hurt, it's not Hollywood
target_dodging: false,
target_dodging,
may_harm,
target_group,
};
@ -1237,20 +1245,53 @@ pub fn handle_parry_hook(server: &Server, defender: EcsEntity, attacker: Option<
.and_then(|info| info.return_ability)
.is_some();
match &mut *char_state {
let return_to_wield = match &mut *char_state {
CharacterState::RiposteMelee(c) => {
c.stage_section = StageSection::Action;
c.timer = Duration::default();
false
},
CharacterState::BasicBlock(c) if should_return => {
c.timer = c.static_data.recover_duration;
c.stage_section = StageSection::Recover;
// Refund half the energy of entering the block for a successful parry
server_eventbus.emit_now(ServerEvent::EnergyChange {
entity: defender,
change: c.static_data.energy_cost / 2.0,
});
false
},
char_state @ CharacterState::BasicBlock(_) => {
*char_state =
CharacterState::Wielding(common::states::wielding::Data { is_sneaking: false });
CharacterState::BasicBlock(c) => {
// Refund half the energy of entering the block for a successful parry
server_eventbus.emit_now(ServerEvent::EnergyChange {
entity: defender,
change: c.static_data.energy_cost / 2.0,
});
true
},
_char_state => {},
char_state => {
// If the character state is not one of the ones above, if it parried because it
// had the capability to parry in its buildup, subtract 5 energy from the
// defender
if char_state
.ability_info()
.and_then(|info| info.ability_meta)
.map_or(false, |meta| {
meta.capabilities
.contains(comp::ability::Capability::BUILDUP_PARRIES)
})
{
server_eventbus.emit_now(ServerEvent::EnergyChange {
entity: defender,
change: -5.0,
});
}
false
},
};
if return_to_wield {
*char_state =
CharacterState::Wielding(common::states::wielding::Data { is_sneaking: false });
}
};

View File

@ -2,8 +2,8 @@ use super::*;
use crate::audio::sfx::SfxEvent;
use common::{
comp::{
bird_large, humanoid, quadruped_medium, quadruped_small, Body, CharacterState, Ori,
PhysicsState,
bird_large, character_state::AttackImmunities, humanoid, quadruped_medium, quadruped_small,
Body, CharacterState, Ori, PhysicsState,
},
states,
terrain::{Block, BlockKind},
@ -184,7 +184,14 @@ fn maps_roll() {
movement_duration: Duration::default(),
recover_duration: Duration::default(),
roll_strength: 0.0,
immune_melee: false,
attack_immunities: AttackImmunities {
melee: false,
projectiles: false,
beams: false,
ground_shockwaves: false,
air_shockwaves: false,
explosions: false,
},
ability_info: empty_ability_info(),
},
timer: Duration::default(),

View File

@ -682,6 +682,7 @@ image_ids! {
buff_dmg_red_0: "voxygen.element.de_buffs.buff_damage_reduce_0",
buff_frenzy_0: "voxygen.element.de_buffs.buff_frenzy_0",
buff_haste_0: "voxygen.element.de_buffs.buff_haste_0",
buff_fortitude_0: "voxygen.element.de_buffs.buff_fortitude_0",
// Debuffs
debuff_skull_0: "voxygen.element.de_buffs.debuff_skull_0",
@ -692,6 +693,7 @@ image_ids! {
debuff_wet_0: "voxygen.element.de_buffs.debuff_wet_0",
debuff_ensnared_0: "voxygen.element.de_buffs.debuff_ensnared_0",
debuff_poisoned_0: "voxygen.element.de_buffs.debuff_poisoned_0",
debuff_parried_0: "voxygen.element.de_buffs.debuff_parried_0",
// Animation Frames
// Buff Frame

View File

@ -84,7 +84,7 @@ use common::{
combat,
comp::{
self,
ability::{self, AuxiliaryAbility},
ability::AuxiliaryAbility,
fluid_dynamics,
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
item::{
@ -565,39 +565,12 @@ impl<'a> BuffIcon<'a> {
}
fn from_char_state(char_state: &comp::CharacterState) -> Option<Self> {
use ability::{AbilityKind, SwordStance};
if let Some(ability_kind) = char_state
.ability_info()
.and_then(|info| info.ability_meta)
.and_then(|meta| meta.kind)
{
let id = match ability_kind {
AbilityKind::Sword(SwordStance::Balanced) => {
"common.abilities.sword.balanced_combo"
},
AbilityKind::Sword(SwordStance::Offensive) => {
"common.abilities.sword.offensive_combo"
},
AbilityKind::Sword(SwordStance::Crippling) => {
"common.abilities.sword.crippling_combo"
},
AbilityKind::Sword(SwordStance::Cleaving) => {
"common.abilities.sword.cleaving_combo"
},
AbilityKind::Sword(SwordStance::Defensive) => {
"common.abilities.sword.defensive_combo"
},
AbilityKind::Sword(SwordStance::Parrying) => {
"common.abilities.sword.parrying_combo"
},
AbilityKind::Sword(SwordStance::Heavy) => "common.abilities.sword.heavy_combo",
AbilityKind::Sword(SwordStance::Mobility) => {
"common.abilities.sword.mobility_combo"
},
AbilityKind::Sword(SwordStance::Reaching) => {
"common.abilities.sword.reaching_combo"
},
};
let id = util::representative_ability_id(ability_kind);
Some(BuffIcon {
kind: BuffIconKind::Ability { ability_id: id },
is_buff: true,
@ -2890,6 +2863,7 @@ impl Hud {
let active_abilities = ecs.read_storage::<comp::ActiveAbilities>();
let bodies = ecs.read_storage::<comp::Body>();
let poises = ecs.read_storage::<comp::Poise>();
let combos = ecs.read_storage::<comp::Combo>();
// Combo floater stuffs
self.floaters.combo_floater = self.floaters.combo_floater.map(|mut f| {
f.timer -= dt.as_secs_f64();
@ -2939,6 +2913,8 @@ impl Hud {
&msm,
self.floaters.combo_floater,
context,
combos.get(entity),
char_states.get(entity),
)
.set(self.ids.skillbar, ui_widgets);
}
@ -3642,6 +3618,12 @@ impl Hud {
} else if let (Crafting(c), Inventory(_)) = (a, b) {
// Remove item from crafting input
self.show.crafting_fields.recipe_inputs.remove(&c.index);
} else if let (Ability(AbilitySlot::Ability(ability)), Hotbar(slot)) = (a, b) {
if let Some(Some(HotbarSlotContents::Ability(index))) =
self.hotbar.slots.get(slot as usize)
{
events.push(Event::ChangeAbility(*index, ability));
}
}
},
slot::Event::Dropped(from) => {
@ -4774,8 +4756,7 @@ pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id {
BuffKind::ProtectingWard => imgs.buff_dmg_red_0,
BuffKind::Frenzied => imgs.buff_frenzy_0,
BuffKind::Hastened => imgs.buff_haste_0,
// TODO: Get unique icon
BuffKind::Fortitude => imgs.buff_dmg_red_0,
BuffKind::Fortitude => imgs.buff_fortitude_0,
// Debuffs
BuffKind::Bleeding => imgs.debuff_bleed_0,
BuffKind::Cursed => imgs.debuff_skull_0,
@ -4785,8 +4766,7 @@ pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id {
BuffKind::Wet => imgs.debuff_wet_0,
BuffKind::Ensnared => imgs.debuff_ensnared_0,
BuffKind::Poisoned => imgs.debuff_poisoned_0,
// TODO: Get unique icon
BuffKind::Parried => imgs.debuff_crippled_0,
BuffKind::Parried => imgs.debuff_parried_0,
}
}

View File

@ -25,7 +25,8 @@ use common::comp::{
self,
ability::AbilityInput,
item::{tool::AbilityContext, ItemDesc, MaterialStatManifest},
Ability, ActiveAbilities, Body, Energy, Health, Inventory, Poise, PoiseState, SkillSet,
Ability, ActiveAbilities, Body, CharacterState, Combo, Energy, Health, Inventory, Poise,
PoiseState, SkillSet,
};
use conrod_core::{
color,
@ -276,8 +277,10 @@ pub struct Skillbar<'a> {
#[conrod(common_builder)]
common: widget::CommonBuilder,
msm: &'a MaterialStatManifest,
combo: Option<ComboFloater>,
combo_floater: Option<ComboFloater>,
context: Option<AbilityContext>,
combo: Option<&'a Combo>,
char_state: Option<&'a CharacterState>,
}
impl<'a> Skillbar<'a> {
@ -306,8 +309,10 @@ impl<'a> Skillbar<'a> {
slot_manager: &'a mut slots::SlotManager,
localized_strings: &'a Localization,
msm: &'a MaterialStatManifest,
combo: Option<ComboFloater>,
combo_floater: Option<ComboFloater>,
context: Option<AbilityContext>,
combo: Option<&'a Combo>,
char_state: Option<&'a CharacterState>,
) -> Self {
Self {
client,
@ -334,8 +339,10 @@ impl<'a> Skillbar<'a> {
slot_manager,
localized_strings,
msm,
combo,
combo_floater,
context,
combo,
char_state,
}
}
@ -605,6 +612,7 @@ impl<'a> Skillbar<'a> {
self.active_abilities,
self.body,
self.context,
self.combo,
);
let image_source = (self.item_imgs, self.imgs);
@ -686,7 +694,7 @@ impl<'a> Skillbar<'a> {
// Helper
let tooltip_text = |slot| {
let (hotbar, inventory, _, skill_set, active_abilities, _, context) = content_source;
let (hotbar, inventory, _, skill_set, active_abilities, _, context, _) = content_source;
hotbar.get(slot).and_then(|content| match content {
hotbar::SlotContents::Inventory(i, _) => inventory
.get_by_hash(i)
@ -768,11 +776,33 @@ impl<'a> Skillbar<'a> {
.active_abilities
.and_then(|a| Ability::from(a.primary).ability_id(Some(self.inventory), self.context));
let primary_ability_id = if let Some(override_id) = self
.char_state
.and_then(|cs| cs.ability_info())
.and_then(|info| info.ability_meta)
.and_then(|meta| meta.kind)
.map(util::representative_ability_id)
{
Some(override_id)
} else {
primary_ability_id
};
let (primary_ability_title, primary_ability_desc) =
util::ability_description(primary_ability_id.unwrap_or(""), self.localized_strings);
Button::image(
primary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
)
.w_h(36.0, 36.0)
.middle_of(state.ids.m1_slot_bg)
.with_tooltip(
self.tooltip_manager,
&primary_ability_title,
&primary_ability_desc,
&tooltip,
TEXT_COLOR,
)
.set(state.ids.m1_content, ui);
// Slot M2
Image::new(self.imgs.skillbar_slot)
@ -784,31 +814,43 @@ impl<'a> Skillbar<'a> {
Ability::from(a.secondary).ability_id(Some(self.inventory), self.context)
});
let (secondary_ability_title, secondary_ability_desc) =
util::ability_description(secondary_ability_id.unwrap_or(""), self.localized_strings);
Button::image(
secondary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
)
.w_h(36.0, 36.0)
.middle_of(state.ids.m2_slot_bg)
.image_color(
if self.energy.current()
>= self
.active_abilities
.and_then(|a| {
a.activate_ability(
AbilityInput::Secondary,
Some(self.inventory),
self.skillset,
Some(self.body),
self.context,
)
})
.map_or(0.0, |(a, _)| a.get_energy_cost())
if self
.active_abilities
.and_then(|a| {
a.activate_ability(
AbilityInput::Secondary,
Some(self.inventory),
self.skillset,
Some(self.body),
self.context,
)
})
.map_or(false, |(a, _)| {
self.energy.current() >= a.energy_cost()
&& self.combo.map_or(false, |c| c.counter() >= a.combo_cost())
})
{
Color::Rgba(1.0, 1.0, 1.0, 1.0)
} else {
Color::Rgba(0.3, 0.3, 0.3, 0.8)
},
)
.with_tooltip(
self.tooltip_manager,
&secondary_ability_title,
&secondary_ability_desc,
&tooltip,
TEXT_COLOR,
)
.set(state.ids.m2_content, ui);
// M1 and M2 icons
@ -822,11 +864,11 @@ impl<'a> Skillbar<'a> {
.set(state.ids.m2_ico, ui);
}
fn show_combo_counter(&self, combo: ComboFloater, state: &State, ui: &mut UiCell) {
if combo.combo > 0 {
let combo_txt = format!("{} Combo", combo.combo);
let combo_cnt = combo.combo as f32;
let time_since_last_update = comp::combo::COMBO_DECAY_START - combo.timer;
fn show_combo_counter(&self, combo_floater: ComboFloater, state: &State, ui: &mut UiCell) {
if combo_floater.combo > 0 {
let combo_txt = format!("{} Combo", combo_floater.combo);
let combo_cnt = combo_floater.combo as f32;
let time_since_last_update = comp::combo::COMBO_DECAY_START - combo_floater.timer;
let alpha = (1.0 - time_since_last_update * 0.2).min(1.0) as f32;
let fnt_col = Color::Rgba(
// White -> Yellow -> Red text color gradient depending on count
@ -837,7 +879,7 @@ impl<'a> Skillbar<'a> {
);
// Increase size for higher counts,
// "flash" on update by increasing the font size by 2.
let fnt_size = ((14.0 + combo.timer as f32 * 0.8).min(30.0)) as u32
let fnt_size = ((14.0 + combo_floater.timer as f32 * 0.8).min(30.0)) as u32
+ if (time_since_last_update) < 0.1 { 2 } else { 0 };
Rectangle::fill_with([10.0, 10.0], color::TRANSPARENT)
@ -914,8 +956,8 @@ impl<'a> Widget for Skillbar<'a> {
self.show_slotbar(state, ui, slot_offset);
// Combo Counter
if let Some(combo) = self.combo {
self.show_combo_counter(combo, state, ui);
if let Some(combo_floater) = self.combo_floater {
self.show_combo_counter(combo_floater, state, ui);
}
}
}

View File

@ -10,7 +10,7 @@ use common::{
ability::{Ability, AbilityInput, AuxiliaryAbility},
item::tool::{AbilityContext, ToolKind},
slot::InvSlotId,
ActiveAbilities, Body, Energy, Inventory, Item, ItemKey, SkillSet,
ActiveAbilities, Body, Combo, Energy, Inventory, Item, ItemKey, SkillSet,
},
recipe::ComponentRecipeBook,
};
@ -129,6 +129,7 @@ type HotbarSource<'a> = (
Option<&'a ActiveAbilities>,
&'a Body,
Option<AbilityContext>,
Option<&'a Combo>,
);
type HotbarImageSource<'a> = (&'a ItemImgs, &'a img_ids::Imgs);
@ -137,7 +138,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
fn image_key(
&self,
(hotbar, inventory, energy, skillset, active_abilities, body, context): &HotbarSource<'a>,
(hotbar, inventory, energy, skillset, active_abilities, body, context, combo): &HotbarSource<'a>,
) -> Option<(Self::ImageKey, Option<Color>)> {
const GREYED_OUT: Color = Color::Rgba(0.3, 0.3, 0.3, 0.8);
hotbar.get(*self).and_then(|contents| match contents {
@ -171,7 +172,10 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
.map(|(ability, _)| {
(
image,
if energy.current() > ability.get_energy_cost() {
if energy.current() >= ability.energy_cost()
&& combo
.map_or(false, |c| c.counter() >= ability.combo_cost())
{
Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))
} else {
Some(GREYED_OUT)
@ -183,7 +187,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
})
}
fn amount(&self, (hotbar, inventory, _, _, _, _, _): &HotbarSource<'a>) -> Option<u32> {
fn amount(&self, (hotbar, inventory, ..): &HotbarSource<'a>) -> Option<u32> {
hotbar
.get(*self)
.and_then(|content| match content {

View File

@ -1,6 +1,7 @@
use super::img_ids;
use common::{
comp::{
ability::{AbilityKind, SwordStance},
inventory::trade_pricing::TradePricing,
item::{
armor::{Armor, ArmorKind, Protection},
@ -418,3 +419,17 @@ pub fn ability_description<'a>(
(loc.get_msg(&ability), loc.get_attr(&ability, "desc"))
}
pub fn representative_ability_id(ability_kind: AbilityKind) -> &'static str {
match ability_kind {
AbilityKind::Sword(SwordStance::Balanced) => "common.abilities.sword.balanced_combo",
AbilityKind::Sword(SwordStance::Offensive) => "common.abilities.sword.offensive_combo",
AbilityKind::Sword(SwordStance::Crippling) => "common.abilities.sword.crippling_combo",
AbilityKind::Sword(SwordStance::Cleaving) => "common.abilities.sword.cleaving_combo",
AbilityKind::Sword(SwordStance::Defensive) => "common.abilities.sword.defensive_combo",
AbilityKind::Sword(SwordStance::Parrying) => "common.abilities.sword.parrying_combo",
AbilityKind::Sword(SwordStance::Heavy) => "common.abilities.sword.heavy_combo",
AbilityKind::Sword(SwordStance::Mobility) => "common.abilities.sword.mobility_combo",
AbilityKind::Sword(SwordStance::Reaching) => "common.abilities.sword.reaching_combo",
}
}