Merge branch 'sam/sceptre-overhaul' into 'master'

Overhaul sceptre

Closes #768

See merge request veloren/veloren!1845
This commit is contained in:
Samuel Keiffer 2021-03-11 16:47:50 +00:00
commit 9b198b5cda
66 changed files with 1377 additions and 726 deletions

View File

@ -71,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved static light rendering and illumination
- Improved the tree spawning model to allow for overlapping forests
- Changed sunlight (and, in general, static light) propagation through blocks to allow for more material properties
- Overhauled the sceptre
### Removed

View File

@ -1,15 +1,11 @@
BasicBeam(
HealingBeam(
buildup_duration: 0.25,
recover_duration: 0.25,
beam_duration: 1.0,
base_hps: 60,
base_dps: 60,
heal: 40,
tick_rate: 2.0,
range: 25.0,
max_angle: 1.0,
lifesteal_eff: 0.15,
energy_regen: 25,
energy_cost: 50,
energy_drain: 0,
orientation_behavior: Normal,
energy_cost: 75,
specifier: HealingBeam,
)

View File

@ -1,19 +0,0 @@
BasicRanged(
energy_cost: 450,
buildup_duration: 0.8,
recover_duration: 0.05,
projectile: Heal(
heal: 80.0,
damage: 60.0,
poise_damage: 0,
radius: 6.0,
),
projectile_body: Object(BoltNature),
/*projectile_light: Some(LightEmitter {
col: (0.0, 1.0, 0.0).into(),
..Default::default()
}),*/
projectile_gravity: Some(Gravity(0.5)),
projectile_speed: 40.0,
can_continue: false,
)

View File

@ -0,0 +1,14 @@
BasicBeam(
buildup_duration: 0.25,
recover_duration: 0.25,
beam_duration: 1.0,
damage: 40,
tick_rate: 2.0,
range: 25.0,
max_angle: 1.0,
lifesteal_eff: 0.15,
energy_regen: 50,
energy_drain: 0,
orientation_behavior: Normal,
specifier: LifestealBeam
)

View File

@ -0,0 +1,15 @@
BasicAura(
buildup_duration: 0.25,
cast_duration: 0.5,
recover_duration: 0.25,
targets: InGroup,
aura: (
kind: ProtectingWard,
strength: 0.33,
duration: Some(10.0),
category: Magical,
),
aura_duration: 1.0,
range: 25.0,
energy_cost: 400,
)

View File

@ -2,14 +2,13 @@ BasicBeam(
buildup_duration: 0.25,
recover_duration: 0.25,
beam_duration: 1.0,
base_hps: 0,
base_dps: 150,
damage: 50,
tick_rate: 3.0,
range: 20.0,
max_angle: 10.0,
lifesteal_eff: 0.0,
energy_regen: 0,
energy_cost: 1,
energy_drain: 350,
orientation_behavior: Normal,
)
specifier: Flamethrower,
)

View File

@ -2,14 +2,13 @@ BasicBeam(
buildup_duration: 0.5,
recover_duration: 0.5,
beam_duration: 1.0,
base_hps: 0,
base_dps: 150,
damage: 50,
tick_rate: 3.0,
range: 20.0,
max_angle: 0.1,
lifesteal_eff: 0.0,
energy_regen: 0,
energy_cost: 1,
energy_drain: 350,
orientation_behavior: Normal,
specifier: Flamethrower,
)

View File

@ -2,14 +2,13 @@ BasicBeam(
buildup_duration: 0.25,
recover_duration: 0.25,
beam_duration: 1.0,
base_hps: 60,
base_dps: 60,
damage: 30,
tick_rate: 2.0,
range: 25.0,
max_angle: 1.0,
lifesteal_eff: 0.15,
energy_regen: 25,
energy_cost: 50,
energy_drain: 0,
orientation_behavior: Normal,
specifier: HealingBeam,
)

View File

@ -2,14 +2,13 @@ BasicBeam(
buildup_duration: 0.4,
recover_duration: 0.25,
beam_duration: 0.5,
base_hps: 0,
base_dps: 150,
damage: 50,
tick_rate: 3.0,
range: 15.0,
max_angle: 22.5,
lifesteal_eff: 0.0,
energy_regen: 0,
energy_cost: 0,
energy_drain: 0,
orientation_behavior: Normal,
specifier: Flamethrower,
)

View File

@ -2,14 +2,13 @@ BasicBeam(
buildup_duration: 0.25,
recover_duration: 0.25,
beam_duration: 0.5,
base_hps: 0,
base_dps: 9001,
damage: 3000,
tick_rate: 3.0,
range: 30.0,
max_angle: 1.0,
lifesteal_eff: 0.0,
energy_regen: 0,
energy_cost: 0,
energy_drain: 0,
orientation_behavior: Turret,
specifier: Flamethrower,
)

View File

@ -70,9 +70,11 @@
],
),
Sceptre: (
primary: "common.abilities.sceptre.healingbeam",
secondary: "common.abilities.sceptre.healingbomb",
abilities: [],
primary: "common.abilities.sceptre.lifestealbeam",
secondary: "common.abilities.sceptre.healingbeam",
abilities: [
(Some(Sceptre(UnlockAura)), "common.abilities.sceptre.wardingaura"),
],
),
Dagger: (
primary: "common.abilities.dagger.tempbasic",

View File

@ -58,17 +58,17 @@
Staff(SKnockback): Some(2),
Staff(SRange): Some(2),
Staff(SCost): Some(2),
Sceptre(BHeal): Some(3),
Sceptre(BDamage): Some(2),
Sceptre(BRange): Some(2),
Sceptre(BLifesteal): Some(2),
Sceptre(BRegen): Some(2),
Sceptre(BCost): Some(2),
Sceptre(PHeal): Some(3),
Sceptre(PDamage): Some(2),
Sceptre(PRadius): Some(2),
Sceptre(PCost): Some(2),
Sceptre(PProjSpeed): Some(2),
Sceptre(LDamage): Some(3),
Sceptre(LRange): Some(2),
Sceptre(LLifesteal): Some(3),
Sceptre(LRegen): Some(2),
Sceptre(HHeal): Some(3),
Sceptre(HCost): Some(2),
Sceptre(HRange): Some(2),
Sceptre(AStrength): Some(2),
Sceptre(ADuration): Some(2),
Sceptre(ARange): Some(2),
Sceptre(ACost): Some(2),
Roll(Cost): Some(2),
Roll(Strength): Some(2),
Roll(Duration): Some(2),

View File

@ -1,68 +1,27 @@
({
Roll(Cost): {Roll(ImmuneMelee): None},
Roll(Strength): {Roll(ImmuneMelee): None},
Roll(Duration): {Roll(ImmuneMelee): None},
Sword(TsDamage): {Sword(TsCombo): None},
Sword(TsRegen): {Sword(TsCombo): None},
Sword(TsSpeed): {Sword(TsCombo): None},
Sword(DDrain): {Sword(DDamage): Some(1)},
Sword(DCost): {Sword(DDamage): Some(1)},
Sword(DSpeed): {Sword(DDamage): Some(1)},
Sword(DInfinite): {Sword(DDamage): Some(1)},
Sword(DScaling): {Sword(DDamage): Some(1)},
Sword(SDamage): {Sword(UnlockSpin): None},
Sword(SSpeed): {Sword(UnlockSpin): None},
Sword(SCost): {Sword(UnlockSpin): None},
Sword(SSpins): {Sword(UnlockSpin): None},
Axe(DsDamage): {Axe(DsCombo): None},
Axe(DsSpeed): {Axe(DsCombo): None},
Axe(DsRegen): {Axe(DsCombo): None},
Axe(SHelicopter): {Axe(SInfinite): None},
Axe(SDamage): {Axe(SInfinite): None},
Axe(SSpeed): {Axe(SInfinite): None},
Axe(SCost): {Axe(SInfinite): None},
Axe(LDamage): {Axe(UnlockLeap): None},
Axe(LKnockback): {Axe(UnlockLeap): None},
Axe(LCost): {Axe(UnlockLeap): None},
Axe(LDistance): {Axe(UnlockLeap): None},
Hammer(SsDamage): {Hammer(SsKnockback): Some(1)},
Hammer(SsRegen): {Hammer(SsKnockback): Some(1)},
Hammer(SsSpeed): {Hammer(SsKnockback): Some(1)},
Hammer(CDamage): {Hammer(CKnockback): Some(1)},
Hammer(CDrain): {Hammer(CKnockback): Some(1)},
Hammer(CSpeed): {Hammer(CKnockback): Some(1)},
Hammer(LDamage): {Hammer(UnlockLeap): None},
Hammer(LCost): {Hammer(UnlockLeap): None},
Hammer(LDistance): {Hammer(UnlockLeap): None},
Hammer(LKnockback): {Hammer(UnlockLeap): None},
Hammer(LRange): {Hammer(UnlockLeap): None},
Bow(BRegen): {Bow(BDamage): Some(1)},
Bow(CKnockback): {Bow(CDamage): Some(1)},
Bow(CProjSpeed): {Bow(CDamage): Some(1)},
Bow(CDrain): {Bow(CDamage): Some(1)},
Bow(CSpeed): {Bow(CDamage): Some(1)},
Bow(CMove): {Bow(CDamage): Some(1)},
Bow(RDamage): {Bow(UnlockRepeater): None},
Bow(RGlide): {Bow(UnlockRepeater): None},
Bow(RArrows): {Bow(UnlockRepeater): None},
Bow(RCost): {Bow(UnlockRepeater): None},
Staff(BDamage): {Staff(BExplosion): None},
Staff(BRegen): {Staff(BExplosion): None},
Staff(BRadius): {Staff(BExplosion): None},
Staff(FRange): {Staff(FDamage): Some(1)},
Staff(FDrain): {Staff(FDamage): Some(1)},
Staff(FVelocity): {Staff(FDamage): Some(1)},
Staff(SDamage): {Staff(UnlockShockwave): None},
Staff(SKnockback): {Staff(UnlockShockwave): None},
Staff(SRange): {Staff(UnlockShockwave): None},
Staff(SCost): {Staff(UnlockShockwave): None},
Sceptre(BDamage): {Sceptre(BHeal): Some(1)},
Sceptre(BRange): {Sceptre(BHeal): Some(1)},
Sceptre(BLifesteal): {Sceptre(BHeal): Some(1)},
Sceptre(BRegen): {Sceptre(BHeal): Some(1)},
Sceptre(BCost): {Sceptre(BHeal): Some(1)},
Sceptre(PDamage): {Sceptre(PHeal): Some(1)},
Sceptre(PRadius): {Sceptre(PHeal): Some(1)},
Sceptre(PCost): {Sceptre(PHeal): Some(1)},
Sceptre(PProjSpeed): {Sceptre(PHeal): Some(1)},
Sceptre(AStrength): {Sceptre(UnlockAura): None},
Sceptre(ADuration): {Sceptre(UnlockAura): None},
Sceptre(ARange): {Sceptre(UnlockAura): None},
Sceptre(ACost): {Sceptre(UnlockAura): None},
})

View File

@ -95,16 +95,17 @@
Staff(SCost),
],
Weapon(Sceptre): [
Sceptre(BHeal),
Sceptre(BDamage),
Sceptre(BRange),
Sceptre(BLifesteal),
Sceptre(BRegen),
Sceptre(BCost),
Sceptre(PHeal),
Sceptre(PDamage),
Sceptre(PRadius),
Sceptre(PCost),
Sceptre(PProjSpeed),
Sceptre(LDamage),
Sceptre(LRange),
Sceptre(LLifesteal),
Sceptre(LRegen),
Sceptre(HHeal),
Sceptre(HCost),
Sceptre(HRange),
Sceptre(UnlockAura),
Sceptre(AStrength),
Sceptre(ADuration),
Sceptre(ARange),
Sceptre(ACost),
],
})

Binary file not shown.

BIN
assets/voxygen/element/icons/lifesteal.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/sceptre_protection.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -17,6 +17,8 @@
"buff.desc.campfire_heal": "Resting at a campfire heals 1% per second.",
"buff.title.invulnerability": "Invulnerability",
"buff.desc.invulnerability": "You cannot be damaged by any attack.",
"buff.title.protectingward": "Protecting Ward",
"buff.desc.protectingward": "You are protected, somewhat, from attacks.",
// Debuffs
"buff.title.bleed": "Bleeding",
"buff.desc.bleed": "Inflicts regular damage.",

View File

@ -34,32 +34,34 @@
"hud.skill.roll_dur_title": "Rolling Duration",
"hud.skill.roll_dur": "Roll for 20% more time{SP}",
// Sceptre
"hud.skill.sc_healbomb_title" : "Heal Bomb",
"hud.skill.sc_healbomb" : "Killing with kindness",
"hud.skill.sc_projectile_speed_title": "Projectile Speed",
"hud.skill.sc_projectile_speed": "Projectile threw 25% faster{SP}",
"hud.skill.sc_energy_cost_title" : "Energy Cost",
"hud.skill.sc_energy_cost" : "Decreases energy cost of bomb by 15%{SP}",
"hud.skill.sc_radius_title" : "Radius",
"hud.skill.sc_radius" : "Increases radius by 30%{SP}",
"hud.skill.sc_damage_title" : "Damage",
"hud.skill.sc_damage" : "Increases damage of bomb by 20%{SP}",
"hud.skill.sc_heal_title" : "Heal",
"hud.skill.sc_heal" : "Increases healing of bomb by 15%{SP}",
"hud.skill.sc_heal_cost_title" : "Heal Cost",
"hud.skill.sc_heal_cost" : "Use 10% less energy when healing{SP}",
"hud.skill.sc_lifesteal_efficiency_title" : "Lifesteal Efficiency",
"hud.skill.sc_lifesteal_efficiency" : "Thieve 25% more health{SP}",
"hud.skill.sc_range_title" : "Range",
"hud.skill.sc_range" : "Longer beam, by 20%{SP}",
"hud.skill.sc_energy_regen_title" : "Energy Regen",
"hud.skill.sc_energy_regen" : "Increases energy regen from dealing damage by 10%{SP}",
"hud.skill.sc_beam_title" : "Heal Beam",
"hud.skill.sc_beam" : "Heal your party, but unheal the enemies",
"hud.skill.sc_beam_damage_title" : "Beam Damage",
"hud.skill.sc_beam_damage" : "Increases damage from the beam by 25%{SP}",
"hud.skill.sc_beam_heal_title" : "Beam Heal",
"hud.skill.sc_beam_heal" : "Increased healing from the beam by 15%{SP}",
"hud.skill.sc_lifesteal_title": "Lifesteal Beam",
"hud.skill.sc_lifesteal": "Drain the life from your enemies",
"hud.skill.sc_lifesteal_damage_title": "Damage",
"hud.skill.sc_lifesteal_damage": "Deal 20% more damage{SP}",
"hud.skill.sc_lifesteal_range_title": "Range",
"hud.skill.sc_lifesteal_range": "Your beam reaches 25% further{SP}",
"hud.skill.sc_lifesteal_lifesteal_title": "Lifesteal",
"hud.skill.sc_lifesteal_lifesteal": "Convert an additional 30% of damage into health{SP}",
"hud.skill.sc_lifesteal_regen_title": "Stamina Regen",
"hud.skill.sc_lifesteal_regen": "Replenish your stamina by an additional 25%{SP}",
"hud.skill.sc_heal_title": "Healing Beam",
"hud.skill.sc_heal": "Heal your allies using the blood of your enemies",
"hud.skill.sc_heal_heal_title": "Heal",
"hud.skill.sc_heal_heal": "Increases the amount you heal others by 20%{SP}",
"hud.skill.sc_heal_cost_title": "Stamina Cost",
"hud.skill.sc_heal_cost": "Healing others requires 20% less stamina{SP}",
"hud.skill.sc_heal_range_title": "Range",
"hud.skill.sc_heal_range": "Your beam reachs 25% further{SP}",
"hud.skill.sc_wardaura_unlock_title": "Warding Aura Unlock",
"hud.skill.sc_wardaura_unlock": "Allows you to ward your allies against enemy attacks{SP}",
"hud.skill.sc_wardaura_strength_title": "Strength",
"hud.skill.sc_wardaura_strength": "The strength of your protection increases by 20%{SP}",
"hud.skill.sc_wardaura_duration_title": "Duration",
"hud.skill.sc_wardaura_duration": "The effects of your ward last 30% longer{SP}",
"hud.skill.sc_wardaura_range_title": "Radius",
"hud.skill.sc_wardaura_range": "Your ward reaches 25% further{SP}",
"hud.skill.sc_wardaura_cost_title": "Stamina Cost",
"hud.skill.sc_wardaura_cost": "Creating the ward requries 20% less energy{SP}",
// Staff
"hud.skill.st_shockwave_range_title" : "Shockwave Range",
"hud.skill.st_shockwave_range" : "throw things that used to be out of reach, range increased 20%{SP}",

View File

@ -60,6 +60,7 @@ const int FIRE_BOWL = 18;
const int SNOW = 19;
const int EXPLOSION = 20;
const int ICE = 21;
const int LIFESTEAL_BEAM = 22;
// meters per second squared (acceleration)
const float earth_gravity = 9.807;
@ -340,13 +341,23 @@ void main() {
vec4(vec3(0.4, 1.6 + 0.3 * sin(tick.x * 10 - lifetime * 3 + 4), 1.0 + 0.15 * sin(tick.x * 5 - lifetime * 5)), 1 /*0.3*/),
spin_in_axis(inst_dir, tick.z)
);
} else if (inst_mode == LIFESTEAL_BEAM) {
f_reflect = 0.0;
float green_col = 0.2 + 1.4 * sin(tick.x * 5 + lifetime * 5);
float purple_col = 1.2 + 0.1 * sin(tick.x * 3 - lifetime * 3) - max(green_col, 1) + 1;
attr = Attr(
spiral_motion(inst_dir, 0.3 * (floor(2 * rand0 + 0.5) - 0.5) * min(linear_scale(10), 1), lifetime / inst_lifespan, 10.0, inst_time),
vec3((1.7 - 0.7 * abs(floor(2 * rand0 - 0.5) + 0.5)) * (1.5 + 0.5 * sin(tick.x * 10 - lifetime * 4))),
vec4(vec3(purple_col, green_col, 0.75 * purple_col), 1),
spin_in_axis(inst_dir, tick.z)
);
} else if (inst_mode == ENERGY_NATURE) {
f_reflect = 0.0;
float spiral_radius = start_end(1 - pow(abs(rand5), 5), 1) * length(inst_dir);
attr = Attr(
inst_dir * slow_end(0.03) + spiral_motion(vec3(rand1, rand2, rand3),
0.2 * (rand4 + 1.3) * slow_end(0.02), percent() * 3 * (rand4 + 4.0) + rand0, 1.0, 0.0),
vec3(1.0),
vec4(vec3(0, 2.5, 1.5 + rand7 * 0.7), 1),
spiral_motion(vec3(0, 0, rand3 + 1), spiral_radius, lifetime, abs(rand0), rand1 * 2 * PI) + vec3(0, 0, rand2),
vec3(6 * abs(rand4) * (1 - slow_start(2)) * pow(spiral_radius / length(inst_dir), 0.5)),
vec4(vec3(0, 1.7, 1.3), 1),
spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3)
);
} else if (inst_mode == FLAMETHROWER) {

View File

@ -13,8 +13,8 @@ use crate::{
},
poise::PoiseChange,
skills::{SkillGroupKind, SkillSet},
Body, Energy, EnergyChange, EnergySource, Health, HealthChange, HealthSource, Inventory,
Stats,
Body, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange, HealthSource,
Inventory, Stats,
},
event::ServerEvent,
uid::Uid,
@ -45,6 +45,7 @@ pub struct AttackerInfo<'a> {
pub entity: EcsEntity,
pub uid: Uid,
pub energy: Option<&'a Energy>,
pub combo: Option<&'a Combo>,
}
#[cfg(not(target_arch = "wasm32"))]
@ -227,9 +228,9 @@ impl Attack {
.filter(|e| e.target.map_or(true, |t| t == target_group))
.filter(|e| !(matches!(e.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
{
if match &effect.requirement {
Some(CombatRequirement::AnyDamage) => accumulated_damage > 0.0,
Some(CombatRequirement::SufficientEnergy(r)) => {
if effect.requirements.iter().all(|req| match req {
CombatRequirement::AnyDamage => accumulated_damage > 0.0,
CombatRequirement::Energy(r) => {
if let Some(AttackerInfo {
entity,
energy: Some(e),
@ -252,8 +253,27 @@ impl Attack {
false
}
},
None => true,
} {
CombatRequirement::Combo(r) => {
if let Some(AttackerInfo {
entity,
combo: Some(c),
..
}) = attacker
{
let sufficient_combo = c.counter() >= *r;
if sufficient_combo {
emit(ServerEvent::ComboChange {
entity,
change: -(*r as i32),
});
}
sufficient_combo
} else {
false
}
},
}) {
match effect.effect {
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir);
@ -368,7 +388,7 @@ impl AttackDamage {
pub struct AttackEffect {
target: Option<GroupTarget>,
effect: CombatEffect,
requirement: Option<CombatRequirement>,
requirements: Vec<CombatRequirement>,
}
#[cfg(not(target_arch = "wasm32"))]
@ -377,12 +397,12 @@ impl AttackEffect {
Self {
target,
effect,
requirement: None,
requirements: Vec::new(),
}
}
pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
self.requirement = Some(requirement);
self.requirements.push(requirement);
self
}
@ -405,7 +425,8 @@ pub enum CombatEffect {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CombatRequirement {
AnyDamage,
SufficientEnergy(f32),
Energy(f32),
Combo(u32),
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -1,8 +1,9 @@
use crate::{
assets::{self, Asset},
combat,
comp::{
inventory::item::tool::ToolKind, projectile::ProjectileConstructor, skills, Body,
CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate,
aura, beam, inventory::item::tool::ToolKind, projectile::ProjectileConstructor, skills,
Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate,
},
states::{
behavior::JoinData,
@ -13,7 +14,6 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use vek::Vec3;
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)]
pub enum CharacterAbilityType {
@ -216,16 +216,36 @@ pub enum CharacterAbility {
buildup_duration: f32,
recover_duration: f32,
beam_duration: f32,
base_hps: f32,
base_dps: f32,
damage: f32,
tick_rate: f32,
range: f32,
max_angle: f32,
lifesteal_eff: f32,
energy_regen: f32,
energy_cost: f32,
energy_drain: f32,
orientation_behavior: basic_beam::MovementBehavior,
specifier: beam::FrontendSpecifier,
},
BasicAura {
buildup_duration: f32,
cast_duration: f32,
recover_duration: f32,
targets: combat::GroupTarget,
aura: aura::AuraBuffConstructor,
aura_duration: f32,
range: f32,
energy_cost: f32,
},
HealingBeam {
buildup_duration: f32,
recover_duration: f32,
beam_duration: f32,
heal: f32,
tick_rate: f32,
range: f32,
max_angle: f32,
energy_cost: f32,
specifier: beam::FrontendSpecifier,
},
}
@ -264,34 +284,14 @@ impl CharacterAbility {
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok()
},
CharacterAbility::DashMelee { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::BasicMelee { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::BasicRanged { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::LeapMelee { energy_cost, .. } => {
update.vel.0.z >= 0.0
&& update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok()
},
CharacterAbility::SpinMelee { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::ChargedRanged { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::ChargedMelee { energy_cost, .. } => update
CharacterAbility::DashMelee { energy_cost, .. }
| CharacterAbility::BasicMelee { energy_cost, .. }
| CharacterAbility::BasicRanged { energy_cost, .. }
| CharacterAbility::SpinMelee { energy_cost, .. }
| CharacterAbility::ChargedRanged { energy_cost, .. }
| CharacterAbility::ChargedMelee { energy_cost, .. }
| CharacterAbility::Shockwave { energy_cost, .. }
| CharacterAbility::BasicAura { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
@ -304,10 +304,14 @@ impl CharacterAbility {
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok()
},
CharacterAbility::Shockwave { energy_cost, .. } => update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok(),
CharacterAbility::LeapMelee { energy_cost, .. } => {
update.vel.0.z >= 0.0
&& update
.energy
.try_change_by(-(*energy_cost as i32), EnergySource::Ability)
.is_ok()
},
CharacterAbility::HealingBeam { .. } => data.combo.counter() > 0,
_ => true,
}
}
@ -348,7 +352,7 @@ impl CharacterAbility {
} => {
*buildup_duration /= speed;
*recover_duration /= speed;
*projectile = projectile.modified_projectile(power, 1_f32, 1_f32, power);
*projectile = projectile.modified_projectile(power, 1_f32, 1_f32);
},
RepeaterRanged {
ref mut movement_duration,
@ -362,7 +366,7 @@ impl CharacterAbility {
*buildup_duration /= speed;
*shoot_duration /= speed;
*recover_duration /= speed;
*projectile = projectile.modified_projectile(power, 1_f32, 1_f32, power);
*projectile = projectile.modified_projectile(power, 1_f32, 1_f32);
},
Boost {
ref mut movement_duration,
@ -488,16 +492,37 @@ impl CharacterAbility {
BasicBeam {
ref mut buildup_duration,
ref mut recover_duration,
ref mut base_hps,
ref mut base_dps,
ref mut damage,
ref mut tick_rate,
..
} => {
*buildup_duration /= speed;
*recover_duration /= speed;
// hps and dps adjusted by speed as they are normalized by tick rate already
*base_hps *= power * speed;
*base_dps *= power * speed;
*damage *= power;
*tick_rate *= speed;
},
BasicAura {
ref mut buildup_duration,
ref mut cast_duration,
ref mut recover_duration,
ref mut aura,
..
} => {
*buildup_duration /= speed;
*cast_duration /= speed;
*recover_duration /= speed;
aura.strength *= power;
},
HealingBeam {
ref mut buildup_duration,
ref mut recover_duration,
ref mut heal,
ref mut tick_rate,
..
} => {
*buildup_duration /= speed;
*recover_duration /= speed;
*heal *= power;
*tick_rate *= speed;
},
}
@ -517,7 +542,15 @@ impl CharacterAbility {
| ChargedMelee { energy_cost, .. }
| ChargedRanged { energy_cost, .. }
| Shockwave { energy_cost, .. }
| BasicBeam { energy_cost, .. } => *energy_cost as u32,
| HealingBeam { energy_cost, .. }
| BasicAura { energy_cost, .. } => *energy_cost as u32,
BasicBeam { energy_drain, .. } => {
if *energy_drain > f32::EPSILON {
1
} else {
0
}
},
BasicBlock | Boost { .. } | ComboMelee { .. } => 0,
}
}
@ -827,7 +860,7 @@ impl CharacterAbility {
.unwrap_or(0);
let power = 1.20_f32.powi(damage_level.into());
let regen = 1.4_f32.powi(regen_level.into());
*projectile = projectile.modified_projectile(power, regen, 1_f32, 1_f32);
*projectile = projectile.modified_projectile(power, regen, 1_f32);
},
ChargedRanged {
ref mut scaled_damage,
@ -874,8 +907,7 @@ impl CharacterAbility {
}
if let Ok(Some(level)) = skillset.skill_level(Bow(RDamage)) {
let power = 1.4_f32.powi(level.into());
*projectile =
projectile.modified_projectile(power, 1_f32, 1_f32, 1_f32);
*projectile = projectile.modified_projectile(power, 1_f32, 1_f32);
}
if !skillset.has_skill(Bow(RGlide)) {
*buildup_duration = 0.001;
@ -914,17 +946,17 @@ impl CharacterAbility {
let power = 1.2_f32.powi(damage_level.into());
let regen = 1.2_f32.powi(regen_level.into());
let range = 1.1_f32.powi(range_level.into());
*projectile = projectile.modified_projectile(power, regen, range, 1_f32);
*projectile = projectile.modified_projectile(power, regen, range);
},
BasicBeam {
ref mut base_dps,
ref mut damage,
ref mut range,
ref mut energy_drain,
ref mut beam_duration,
..
} => {
if let Ok(Some(level)) = skillset.skill_level(Staff(FDamage)) {
*base_dps *= 1.3_f32.powi(level.into());
*damage *= 1.3_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Staff(FRange)) {
let range_mod = 1.25_f32.powi(level.into());
@ -968,66 +1000,66 @@ impl CharacterAbility {
use skills::SceptreSkill::*;
match self {
BasicBeam {
ref mut base_hps,
ref mut base_dps,
ref mut lifesteal_eff,
ref mut damage,
ref mut range,
ref mut energy_regen,
ref mut energy_cost,
ref mut beam_duration,
ref mut lifesteal_eff,
ref mut energy_regen,
..
} => {
if let Ok(Some(level)) = skillset.skill_level(Sceptre(BHeal)) {
*base_hps *= 1.15_f32.powi(level.into());
if let Ok(Some(level)) = skillset.skill_level(Sceptre(LDamage)) {
*damage *= 1.2_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(BDamage)) {
*base_dps *= 1.25_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(BRange)) {
let range_mod = 1.20_f32.powi(level.into());
if let Ok(Some(level)) = skillset.skill_level(Sceptre(LRange)) {
let range_mod = 1.25_f32.powi(level.into());
*range *= range_mod;
// Duration modified to keep velocity constant
*beam_duration *= range_mod;
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(BLifesteal)) {
*lifesteal_eff *= 1.25_f32.powi(level.into());
if let Ok(Some(level)) = skillset.skill_level(Sceptre(LRegen)) {
*energy_regen *= 1.25_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(BRegen)) {
*energy_regen *= 1.1_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(BCost)) {
*energy_cost *= 0.9_f32.powi(level.into());
if let Ok(Some(level)) = skillset.skill_level(Sceptre(LLifesteal)) {
*lifesteal_eff *= 1.3_f32.powi(level.into());
}
},
BasicRanged {
HealingBeam {
ref mut heal,
ref mut energy_cost,
ref mut projectile,
ref mut projectile_speed,
ref mut range,
ref mut beam_duration,
..
} => {
{
let heal_level = skillset
.skill_level(Sceptre(PHeal))
.unwrap_or(None)
.unwrap_or(0);
let damage_level = skillset
.skill_level(Sceptre(PDamage))
.unwrap_or(None)
.unwrap_or(0);
let range_level = skillset
.skill_level(Sceptre(PRadius))
.unwrap_or(None)
.unwrap_or(0);
let heal = 1.15_f32.powi(heal_level.into());
let power = 1.2_f32.powi(damage_level.into());
let range = 1.3_f32.powi(range_level.into());
*projectile = projectile.modified_projectile(power, 1_f32, range, heal);
if let Ok(Some(level)) = skillset.skill_level(Sceptre(HHeal)) {
*heal *= 1.2_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(PCost)) {
*energy_cost *= 0.85_f32.powi(level.into());
if let Ok(Some(level)) = skillset.skill_level(Sceptre(HRange)) {
let range_mod = 1.25_f32.powi(level.into());
*range *= range_mod;
// Duration modified to keep velocity constant
*beam_duration *= range_mod;
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(PProjSpeed)) {
*projectile_speed *= 1.25_f32.powi(level.into());
if let Ok(Some(level)) = skillset.skill_level(Sceptre(HCost)) {
*energy_cost *= 0.8_f32.powi(level.into());
}
},
BasicAura {
ref mut aura,
ref mut range,
ref mut energy_cost,
..
} => {
if let Ok(Some(level)) = skillset.skill_level(Sceptre(AStrength)) {
aura.strength *= 1.2_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(ADuration)) {
aura.duration.map(|dur| dur * 1.3_f32.powi(level.into()));
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(ARange)) {
*range *= 1.25_f32.powi(level.into());
}
if let Ok(Some(level)) = skillset.skill_level(Sceptre(ACost)) {
*energy_cost *= 0.8_f32.powi(level.into());
}
},
_ => {},
@ -1436,36 +1468,82 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
buildup_duration,
recover_duration,
beam_duration,
base_hps,
base_dps,
damage,
tick_rate,
range,
max_angle,
lifesteal_eff,
energy_regen,
energy_cost,
energy_drain,
orientation_behavior,
specifier,
} => CharacterState::BasicBeam(basic_beam::Data {
static_data: basic_beam::StaticData {
buildup_duration: Duration::from_secs_f32(*buildup_duration),
recover_duration: Duration::from_secs_f32(*recover_duration),
beam_duration: Duration::from_secs_f32(*beam_duration),
base_hps: *base_hps,
base_dps: *base_dps,
damage: *damage,
tick_rate: *tick_rate,
range: *range,
max_angle: *max_angle,
lifesteal_eff: *lifesteal_eff,
energy_regen: *energy_regen,
energy_cost: *energy_cost,
energy_drain: *energy_drain,
ability_info,
orientation_behavior: *orientation_behavior,
specifier: *specifier,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
}),
CharacterAbility::BasicAura {
buildup_duration,
cast_duration,
recover_duration,
targets,
aura,
aura_duration,
range,
energy_cost: _,
} => CharacterState::BasicAura(basic_aura::Data {
static_data: basic_aura::StaticData {
buildup_duration: Duration::from_secs_f32(*buildup_duration),
cast_duration: Duration::from_secs_f32(*cast_duration),
recover_duration: Duration::from_secs_f32(*recover_duration),
targets: *targets,
aura: *aura,
aura_duration: Duration::from_secs_f32(*aura_duration),
range: *range,
ability_info,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
}),
CharacterAbility::HealingBeam {
buildup_duration,
recover_duration,
beam_duration,
heal,
tick_rate,
range,
max_angle,
energy_cost,
specifier,
} => CharacterState::HealingBeam(healing_beam::Data {
static_data: healing_beam::StaticData {
buildup_duration: Duration::from_secs_f32(*buildup_duration),
recover_duration: Duration::from_secs_f32(*recover_duration),
beam_duration: Duration::from_secs_f32(*beam_duration),
heal: *heal,
tick_rate: *tick_rate,
range: *range,
max_angle: *max_angle,
energy_cost: *energy_cost,
ability_info,
specifier: *specifier,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
offset: Vec3::zero(),
}),
}
}

View File

@ -1,4 +1,5 @@
use crate::{
combat::GroupTarget,
comp::buff::{BuffCategory, BuffData, BuffKind, BuffSource},
uid::Uid,
};
@ -62,11 +63,24 @@ pub enum AuraTarget {
/// Targets the group of the entity specified by the `Uid`. This is useful
/// for auras which should only affect a player's party.
GroupOf(Uid),
/// Targets everyone not in the group of the entity specified by the `Uid`.
/// This is useful for auras which should only affect a player's
/// enemies.
NotGroupOf(Uid),
/// Targets all entities. This is for auras which are global or neutral.
All,
}
impl From<(Option<GroupTarget>, Option<&Uid>)> for AuraTarget {
fn from((target, uid): (Option<GroupTarget>, Option<&Uid>)) -> Self {
match (target, uid) {
(Some(GroupTarget::InGroup), Some(uid)) => Self::GroupOf(*uid),
(Some(GroupTarget::OutOfGroup), Some(uid)) => Self::NotGroupOf(*uid),
_ => Self::All,
}
}
}
impl Aura {
/// Creates a new Aura to be assigned to an entity
pub fn new(
@ -104,6 +118,35 @@ impl Auras {
pub fn remove(&mut self, key: AuraKey) { self.auras.remove(key); }
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuraBuffConstructor {
pub kind: BuffKind,
pub strength: f32,
pub duration: Option<f32>,
pub category: BuffCategory,
}
impl AuraBuffConstructor {
pub fn to_aura(
self,
uid: &Uid,
radius: f32,
duration: Option<Duration>,
target: AuraTarget,
) -> Aura {
let aura_kind = AuraKind::Buff {
kind: self.kind,
data: BuffData {
strength: self.strength,
duration: self.duration.map(Duration::from_secs_f32),
},
category: self.category,
source: BuffSource::Character { by: *uid },
};
Aura::new(aura_kind, radius, duration, target)
}
}
impl Component for Auras {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}

View File

@ -11,6 +11,7 @@ pub struct Properties {
pub speed: f32,
pub duration: Duration,
pub owner: Option<Uid>,
pub specifier: FrontendSpecifier,
}
// TODO: Separate components out for cheaper network syncing
@ -44,3 +45,10 @@ pub struct Beam {
impl Component for Beam {
type Storage = IdvStorage<Self>;
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum FrontendSpecifier {
Flamethrower,
LifestealBeam,
HealingBeam,
}

View File

@ -33,6 +33,8 @@ pub enum BuffKind {
IncreaseMaxHealth,
/// Makes you immune to attacks
Invulnerability,
/// Reduces incoming damage
ProtectingWard,
}
#[cfg(not(target_arch = "wasm32"))]
@ -49,6 +51,7 @@ impl BuffKind {
BuffKind::IncreaseMaxEnergy => true,
BuffKind::IncreaseMaxHealth => true,
BuffKind::Invulnerability => true,
BuffKind::ProtectingWard => true,
}
}
@ -101,16 +104,11 @@ pub enum BuffEffect {
kind: ModifierKind,
},
/// Changes maximum health by a certain amount
MaxHealthModifier {
value: f32,
kind: ModifierKind,
},
MaxHealthModifier { value: f32, kind: ModifierKind },
/// Changes maximum stamina by a certain amount
MaxEnergyModifier {
value: f32,
kind: ModifierKind,
},
ImmuneToAttacks,
MaxEnergyModifier { value: f32, kind: ModifierKind },
/// Reduces damage after armor is accounted for by this fraction
DamageReduction(f32),
}
/// Actual de/buff.
@ -213,7 +211,16 @@ impl Buff {
}],
data.duration,
),
BuffKind::Invulnerability => (vec![BuffEffect::ImmuneToAttacks], data.duration),
BuffKind::Invulnerability => (vec![BuffEffect::DamageReduction(1.0)], data.duration),
BuffKind::ProtectingWard => (
vec![BuffEffect::DamageReduction(
// Causes non-linearity in effect strength, but necessary to allow for tool
// power and other things to affect the strength. 0.5 also still provides 50%
// damage reduction.
data.strength / (0.5 + data.strength),
)],
data.duration,
),
};
Buff {
kind,

View File

@ -81,6 +81,13 @@ pub enum CharacterState {
/// A continuous attack that affects all creatures in a cone originating
/// from the source
BasicBeam(basic_beam::Data),
/// Creates an aura that persists as long as you are actively casting
BasicAura(basic_aura::Data),
/// A directed beam that heals targets in range. This is separate from basic
/// beam as a large amount of functionality needed to be special cased
/// specifically for the healing beam. There was also functionality present
/// on basic beam which was unnecessary for the healing beam.
HealingBeam(healing_beam::Data),
}
impl CharacterState {
@ -100,6 +107,8 @@ impl CharacterState {
| CharacterState::RepeaterRanged(_)
| CharacterState::Shockwave(_)
| CharacterState::BasicBeam(_)
| CharacterState::BasicAura(_)
| CharacterState::HealingBeam(_)
)
}
@ -121,6 +130,8 @@ impl CharacterState {
| CharacterState::RepeaterRanged(_)
| CharacterState::Shockwave(_)
| CharacterState::BasicBeam(_)
| CharacterState::BasicAura(_)
| CharacterState::HealingBeam(_)
)
}
@ -141,6 +152,7 @@ impl CharacterState {
| CharacterState::Stunned(_)
| CharacterState::Wielding
| CharacterState::Talk
| CharacterState::HealingBeam(_)
)
}

View File

@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
pub const COMBO_DECAY_START: f64 = 5.0; // seconds
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Combo {
counter: u32,
@ -24,14 +26,14 @@ impl Combo {
pub fn reset(&mut self) { self.counter = 0; }
pub fn increase_by(&mut self, amount: u32, time: f64) {
self.counter = self.counter.saturating_add(amount);
pub fn change_by(&mut self, amount: i32, time: f64) {
if amount > 0 {
self.counter = self.counter.saturating_add(amount as u32);
} else {
self.counter = self.counter.saturating_sub(amount.abs() as u32);
}
self.last_increase = time;
}
pub fn decrease_by(&mut self, amount: u32) {
self.counter = self.counter.saturating_sub(amount);
}
}
impl Component for Combo {

View File

@ -58,11 +58,6 @@ pub enum ProjectileConstructor {
damage: f32,
energy_regen: f32,
},
Heal {
heal: f32,
damage: f32,
radius: f32,
},
Possess,
}
@ -103,7 +98,8 @@ impl ProjectileConstructor {
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(energy)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
Projectile {
hit_solid: vec![Effect::Stick],
@ -130,7 +126,8 @@ impl ProjectileConstructor {
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(energy);
.with_effect(energy)
.with_combo_increment();
let explosion = Explosion {
effects: vec![
RadiusEffect::Attack(attack),
@ -157,7 +154,8 @@ impl ProjectileConstructor {
);
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult);
.with_crit(crit_chance, crit_mult)
.with_combo_increment();
let explosion = Explosion {
effects: vec![RadiusEffect::Attack(attack)],
radius,
@ -187,7 +185,8 @@ impl ProjectileConstructor {
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(energy);
.with_effect(energy)
.with_combo_increment();
Projectile {
hit_solid: vec![Effect::Vanish],
@ -197,36 +196,6 @@ impl ProjectileConstructor {
ignore_group: true,
}
},
Heal {
heal,
damage,
radius,
} => {
let damage = AttackDamage::new(
Damage {
source: DamageSource::Explosion,
value: damage,
},
Some(GroupTarget::OutOfGroup),
);
let heal = AttackEffect::new(Some(GroupTarget::InGroup), CombatEffect::Heal(heal));
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(heal);
let explosion = Explosion {
effects: vec![RadiusEffect::Attack(attack)],
radius,
reagent: Some(Reagent::Green),
};
Projectile {
hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish],
hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
time_left: Duration::from_secs(10),
owner,
ignore_group: false,
}
},
Possess => Projectile {
hit_solid: vec![Effect::Stick],
hit_entity: vec![Effect::Stick, Effect::Possess],
@ -237,13 +206,7 @@ impl ProjectileConstructor {
}
}
pub fn modified_projectile(
mut self,
power: f32,
regen: f32,
range: f32,
heal_power: f32,
) -> Self {
pub fn modified_projectile(mut self, power: f32, regen: f32, range: f32) -> Self {
use ProjectileConstructor::*;
match self {
Arrow {
@ -280,16 +243,6 @@ impl ProjectileConstructor {
*damage *= power;
*energy_regen *= regen;
},
Heal {
ref mut damage,
ref mut heal,
ref mut radius,
..
} => {
*damage *= power;
*heal *= heal_power;
*radius *= range;
},
Possess => {},
}
self

View File

@ -219,19 +219,21 @@ pub enum StaffSkill {
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum SceptreSkill {
// Beam upgrades
BHeal,
BDamage,
BRange,
BLifesteal,
BRegen,
BCost,
// Projectile upgrades
PHeal,
PDamage,
PRadius,
PCost,
PProjSpeed,
// Lifesteal beam upgrades
LDamage,
LRange,
LLifesteal,
LRegen,
// Healing beam upgrades
HHeal,
HCost,
HRange,
// Warding aura upgrades
UnlockAura,
AStrength,
ADuration,
ARange,
ACost,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -1,5 +1,5 @@
use crate::{comp, uid::Uid};
use comp::item::Reagent;
use comp::{beam, item::Reagent};
use serde::{Deserialize, Serialize};
use vek::*;
@ -24,7 +24,7 @@ pub enum Outcome {
},
Beam {
pos: Vec3<f32>,
heal: bool,
specifier: beam::FrontendSpecifier,
},
ExpChange {
uid: Uid,
@ -37,16 +37,20 @@ pub enum Outcome {
// TODO: Access ECS to get position from Uid to conserve bandwidth
pos: Vec3<f32>,
},
ComboChange {
uid: Uid,
combo: u32,
},
}
impl Outcome {
pub fn get_pos(&self) -> Option<Vec3<f32>> {
match self {
Outcome::Explosion { pos, .. } => Some(*pos),
Outcome::ProjectileShot { pos, .. } => Some(*pos),
Outcome::Beam { pos, .. } => Some(*pos),
Outcome::ExpChange { .. } => None,
Outcome::SkillPointGain { pos, .. } => Some(*pos),
Outcome::Explosion { pos, .. }
| Outcome::ProjectileShot { pos, .. }
| Outcome::Beam { pos, .. }
| Outcome::SkillPointGain { pos, .. } => Some(*pos),
Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => None,
}
}
}

View File

@ -0,0 +1,137 @@
use crate::{
combat::GroupTarget,
comp::{
aura::{AuraBuffConstructor, AuraChange, AuraTarget},
CharacterState, StateUpdate,
},
event::ServerEvent,
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
};
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 should create the aura
pub buildup_duration: Duration,
/// How long the state is creating an aura
pub cast_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// Determines how the aura selects its targets
pub targets: GroupTarget,
/// Has information used to construct the aura
pub aura: AuraBuffConstructor,
/// How long aura lasts
pub aura_duration: Duration,
/// Radius of aura
pub range: f32,
/// What key is used to press ability
pub ability_info: AbilityInfo,
}
#[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,
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
let mut update = StateUpdate::from(data);
handle_move(data, &mut update, 0.8);
handle_jump(data, &mut update);
if !ability_key_is_pressed(data, self.static_data.ability_info.key) {
handle_interrupt(data, &mut update, false);
match update.character {
CharacterState::BasicAura(_) => {},
_ => {
return update;
},
}
}
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.buildup_duration {
// Build up
update.character = CharacterState::BasicAura(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Creates aura
let targets =
AuraTarget::from((Some(self.static_data.targets), Some(data.uid)));
let aura = self.static_data.aura.to_aura(
data.uid,
self.static_data.range,
Some(self.static_data.aura_duration),
targets,
);
update.server_events.push_front(ServerEvent::Aura {
entity: data.entity,
aura_change: AuraChange::Add(aura),
});
// Build up
update.character = CharacterState::BasicAura(Data {
timer: Duration::default(),
stage_section: StageSection::Cast,
..*self
});
}
},
StageSection::Cast => {
if self.timer < self.static_data.cast_duration {
// Cast
update.character = CharacterState::BasicAura(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
update.character = CharacterState::BasicAura(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
update.character = CharacterState::BasicAura(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
}
},
_ => {
// If it somehow ends up in an incorrect stage section
update.character = CharacterState::Wielding;
},
}
update
}
}

View File

@ -3,7 +3,7 @@ use crate::{
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageSource,
GroupTarget,
},
comp::{beam, Body, CharacterState, EnergyChange, EnergySource, Ori, Pos, StateUpdate},
comp::{beam, CharacterState, EnergyChange, EnergySource, Ori, Pos, StateUpdate},
event::ServerEvent,
states::{
behavior::{CharacterBehavior, JoinData},
@ -24,11 +24,9 @@ pub struct StaticData {
pub recover_duration: Duration,
/// How long each beam segment persists for
pub beam_duration: Duration,
/// Base healing per second
pub base_hps: f32,
/// Base damage per second
pub base_dps: f32,
/// Ticks of damage/healing per second
/// Base damage per tick
pub damage: f32,
/// Ticks per second
pub tick_rate: f32,
/// Max range
pub range: f32,
@ -37,16 +35,16 @@ pub struct StaticData {
/// Lifesteal efficiency (0 gives 0% conversion of damage to health, 1 gives
/// 100% conversion of damage to health)
pub lifesteal_eff: f32,
/// Energy regened per second for damage ticks
/// Energy regenerated per tick
pub energy_regen: f32,
/// Energy consumed per second for heal ticks
pub energy_cost: f32,
/// Energy drained per
/// Energy drained per second
pub energy_drain: f32,
/// Used to dictate how orientation functions in this state
pub orientation_behavior: MovementBehavior,
/// What key is used to press ability
pub ability_info: AbilityInfo,
/// Used to specify the beam to the frontend
pub specifier: beam::FrontendSpecifier,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -58,8 +56,6 @@ pub struct Data {
pub timer: Duration,
/// What section the character stage is in
pub stage_section: StageSection,
/// Used to offset beam and particles
pub offset: Vec3<f32>,
}
impl CharacterBehavior for Data {
@ -103,17 +99,10 @@ impl CharacterBehavior for Data {
tick_dur: Duration::from_secs_f32(1.0 / self.static_data.tick_rate),
timer: Duration::default(),
});
// Gets offsets
let body_offsets = Vec3::new(
(data.body.radius() + 1.0) * data.inputs.look_dir.x,
(data.body.radius() + 1.0) * data.inputs.look_dir.y,
data.body.eye_height(),
) * 0.55;
// Build up
update.character = CharacterState::BasicBeam(Data {
timer: Duration::default(),
stage_section: StageSection::Cast,
offset: body_offsets,
..*self
});
}
@ -135,27 +124,18 @@ impl CharacterBehavior for Data {
let damage = AttackDamage::new(
Damage {
source: DamageSource::Energy,
value: self.static_data.base_dps as f32 / self.static_data.tick_rate,
value: self.static_data.damage,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(lifesteal);
let heal = AttackEffect::new(
Some(GroupTarget::InGroup),
CombatEffect::Heal(
self.static_data.base_hps as f32 / self.static_data.tick_rate,
),
)
.with_requirement(CombatRequirement::SufficientEnergy(
self.static_data.energy_cost,
));
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(energy)
.with_effect(heal);
.with_combo_increment();
let properties = beam::Properties {
attack,
@ -163,20 +143,14 @@ impl CharacterBehavior for Data {
speed,
duration: self.static_data.beam_duration,
owner: Some(*data.uid),
specifier: self.static_data.specifier,
};
// Gets offsets
let body_offsets = match data.body {
Body::Humanoid(_) => Vec3::new(
(data.body.radius() + 2.0) * data.inputs.look_dir.x,
(data.body.radius() + 2.0) * data.inputs.look_dir.y,
data.body.eye_height() * 0.55,
),
_ => Vec3::new(
(data.body.radius() + 3.0) * data.inputs.look_dir.x,
(data.body.radius() + 3.0) * data.inputs.look_dir.y,
data.body.eye_height() * 0.55,
),
};
let body_offsets = Vec3::new(
(data.body.radius() + 1.0) * data.inputs.look_dir.x,
(data.body.radius() + 1.0) * data.inputs.look_dir.y,
data.body.eye_height() * 0.6,
);
let pos = Pos(data.pos.0 + body_offsets);
// Create beam segment
update.server_events.push_front(ServerEvent::BeamSegment {
@ -189,7 +163,6 @@ impl CharacterBehavior for Data {
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
offset: body_offsets,
..*self
});

View File

@ -121,7 +121,8 @@ impl CharacterBehavior for Data {
.with_crit(crit_chance, crit_mult)
.with_effect(energy)
.with_effect(poise)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
// Hit attempt
data.updater.insert(data.entity, Melee {

View File

@ -186,7 +186,8 @@ impl CharacterBehavior for Data {
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(poise)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
// Hit attempt
data.updater.insert(data.entity, Melee {

View File

@ -129,7 +129,8 @@ impl CharacterBehavior for Data {
let attack = Attack::default()
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
// Fire
let projectile = Projectile {

View File

@ -166,7 +166,8 @@ impl CharacterBehavior for Data {
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(poise)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
data.updater.insert(data.entity, Melee {
attack,

View File

@ -0,0 +1,167 @@
use crate::{
combat::{Attack, AttackEffect, CombatEffect, CombatRequirement, GroupTarget},
comp::{beam, CharacterState, Ori, Pos, StateUpdate},
event::ServerEvent,
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
uid::Uid,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use vek::*;
/// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticData {
/// How long until state should deal damage or heal
pub buildup_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// How long each beam segment persists for
pub beam_duration: Duration,
/// Base healing per tick
pub heal: f32,
/// Ticks of healing per second
pub tick_rate: f32,
/// Max range
pub range: f32,
/// Max angle (45.0 will give you a 90.0 angle window)
pub max_angle: f32,
/// Energy consumed per second for heal ticks
pub energy_cost: f32,
/// What key is used to press ability
pub ability_info: AbilityInfo,
/// Used to specify the beam to the frontend
pub specifier: beam::FrontendSpecifier,
}
#[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,
}
impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData) -> StateUpdate {
let mut update = StateUpdate::from(data);
handle_move(data, &mut update, 0.4);
handle_jump(data, &mut update);
if !ability_key_is_pressed(data, self.static_data.ability_info.key) {
handle_interrupt(data, &mut update, false);
match update.character {
CharacterState::HealingBeam(_) => {},
_ => {
return update;
},
}
}
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.buildup_duration {
// Build up
update.character = CharacterState::HealingBeam(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Creates beam
data.updater.insert(data.entity, beam::Beam {
hit_entities: Vec::<Uid>::new(),
tick_dur: Duration::from_secs_f32(1.0 / self.static_data.tick_rate),
timer: Duration::default(),
});
// Build up
update.character = CharacterState::HealingBeam(Data {
timer: Duration::default(),
stage_section: StageSection::Cast,
..*self
});
}
},
StageSection::Cast => {
if ability_key_is_pressed(data, self.static_data.ability_info.key) {
let speed =
self.static_data.range / self.static_data.beam_duration.as_secs_f32();
let heal = AttackEffect::new(
Some(GroupTarget::InGroup),
CombatEffect::Heal(self.static_data.heal),
)
.with_requirement(CombatRequirement::Energy(self.static_data.energy_cost))
.with_requirement(CombatRequirement::Combo(1));
let attack = Attack::default().with_effect(heal);
let properties = beam::Properties {
attack,
angle: self.static_data.max_angle.to_radians(),
speed,
duration: self.static_data.beam_duration,
owner: Some(*data.uid),
specifier: self.static_data.specifier,
};
// Gets offsets
let body_offsets = Vec3::new(
(data.body.radius() + 1.0) * data.inputs.look_dir.x,
(data.body.radius() + 1.0) * data.inputs.look_dir.y,
data.body.eye_height() * 0.6,
);
let pos = Pos(data.pos.0 + body_offsets);
// Create beam segment
update.server_events.push_front(ServerEvent::BeamSegment {
properties,
pos,
ori: Ori::from(data.inputs.look_dir),
});
update.character = CharacterState::HealingBeam(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
update.character = CharacterState::HealingBeam(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
update.character = CharacterState::HealingBeam(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<beam::Beam>(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::<beam::Beam>(data.entity);
},
}
update
}
}

View File

@ -176,7 +176,8 @@ impl CharacterBehavior for Data {
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(poise)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
// Hit attempt, when animation plays
data.updater.insert(data.entity, Melee {

View File

@ -1,3 +1,4 @@
pub mod basic_aura;
pub mod basic_beam;
pub mod basic_block;
pub mod basic_melee;
@ -13,6 +14,7 @@ pub mod dash_melee;
pub mod equipping;
pub mod glide;
pub mod glide_wield;
pub mod healing_beam;
pub mod idle;
pub mod leap_melee;
pub mod repeater_ranged;

View File

@ -106,7 +106,8 @@ impl CharacterBehavior for Data {
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(poise)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
let properties = shockwave::Properties {
angle: self.static_data.shockwave_angle,
vertical_angle: self.static_data.shockwave_vertical_angle,

View File

@ -141,7 +141,8 @@ impl CharacterBehavior for Data {
.with_damage(damage)
.with_crit(crit_chance, crit_mult)
.with_effect(poise)
.with_effect(knockback);
.with_effect(knockback)
.with_combo_increment();
// Hit attempt
data.updater.insert(data.entity, Melee {

View File

@ -7,7 +7,7 @@ use common::{
},
event::{EventBus, ServerEvent},
resources::DeltaTime,
uid::UidAllocator,
uid::{Uid, UidAllocator},
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
@ -26,6 +26,7 @@ pub struct ReadData<'a> {
char_states: ReadStorage<'a, CharacterState>,
healths: ReadStorage<'a, Health>,
groups: ReadStorage<'a, Group>,
uids: ReadStorage<'a, Uid>,
}
#[derive(Default)]
@ -79,11 +80,12 @@ impl<'a> System<'a> for Sys {
expired_auras.push(key);
}
}
for (target, target_pos, mut target_buffs, health) in (
for (target, target_pos, mut target_buffs, health, target_uid) in (
&read_data.entities,
&read_data.positions,
&mut buffs,
&read_data.healths,
&read_data.uids,
)
.join()
{
@ -96,7 +98,8 @@ impl<'a> System<'a> for Sys {
.and_then(|e| read_data.groups.get(e))
.map_or(false, |owner_group| {
Some(owner_group) == read_data.groups.get(target)
});
})
|| *target_uid == uid;
if !same_group {
continue;
@ -125,13 +128,16 @@ impl<'a> System<'a> for Sys {
if apply_buff {
// Checks that target is not already receiving a buff from an
// aura, where the buff is of the same kind, and is of at least
// the same strength
// the same strength and of at least the same duration
// If no such buff is present, adds the buff
let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| {
buff.cat_ids.iter().any(|cat_id| {
matches!(cat_id, BuffCategory::FromAura(_))
}) && buff.kind == kind
&& buff.data.strength >= data.strength
&& buff.time.map_or(true, |dur| {
data.duration.map_or(false, |dur_2| dur >= dur_2)
})
});
if emit_buff {
use buff::*;

View File

@ -1,8 +1,8 @@
use common::{
combat::{AttackerInfo, TargetInfo},
comp::{
Beam, BeamSegment, Body, Energy, Group, Health, HealthSource, Inventory, Last, Ori, Pos,
Scale, Stats,
Beam, BeamSegment, Body, Combo, Energy, Group, Health, HealthSource, Inventory, Last, Ori,
Pos, Scale, Stats,
},
event::{EventBus, ServerEvent},
resources::{DeltaTime, Time},
@ -35,6 +35,7 @@ pub struct ReadData<'a> {
groups: ReadStorage<'a, Group>,
energies: ReadStorage<'a, Energy>,
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
}
/// This system is responsible for handling beams that heal or do damage
@ -162,6 +163,7 @@ impl<'a> System<'a> for Sys {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
});
let target_info = TargetInfo {

View File

@ -165,8 +165,8 @@ impl<'a> System<'a> for Sys {
energy.set_maximum(new_max);
},
},
BuffEffect::ImmuneToAttacks => {
stat.damage_reduction = 1.0;
BuffEffect::DamageReduction(dr) => {
stat.damage_reduction = dr.min(1.0);
},
};
}

View File

@ -303,6 +303,8 @@ impl<'a> System<'a> for Sys {
CharacterState::RepeaterRanged(data) => data.handle_event(&j, action),
CharacterState::Shockwave(data) => data.handle_event(&j, action),
CharacterState::BasicBeam(data) => data.handle_event(&j, action),
CharacterState::BasicAura(data) => data.handle_event(&j, action),
CharacterState::HealingBeam(data) => data.handle_event(&j, action),
};
local_emitter.append(&mut state_update.local_events);
server_emitter.append(&mut state_update.server_events);
@ -342,6 +344,8 @@ impl<'a> System<'a> for Sys {
CharacterState::RepeaterRanged(data) => data.behavior(&j),
CharacterState::Shockwave(data) => data.behavior(&j),
CharacterState::BasicBeam(data) => data.behavior(&j),
CharacterState::BasicAura(data) => data.behavior(&j),
CharacterState::HealingBeam(data) => data.behavior(&j),
};
local_emitter.append(&mut state_update.local_events);

View File

@ -1,6 +1,9 @@
use common::{
combat::{AttackerInfo, TargetInfo},
comp::{Body, CharacterState, Energy, Group, Health, Inventory, Melee, Ori, Pos, Scale, Stats},
comp::{
Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Pos, Scale,
Stats,
},
event::{EventBus, ServerEvent},
uid::Uid,
util::Dir,
@ -27,6 +30,7 @@ pub struct ReadData<'a> {
char_states: ReadStorage<'a, CharacterState>,
server_bus: Read<'a, EventBus<ServerEvent>>,
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
}
/// This system is responsible for handling accepted inputs like moving or
@ -113,6 +117,7 @@ impl<'a> System<'a> for Sys {
entity: attacker,
uid: *uid,
energy: read_data.energies.get(attacker),
combo: read_data.combos.get(attacker),
});
let target_info = TargetInfo {

View File

@ -1,8 +1,8 @@
use common::{
combat::{AttackerInfo, TargetInfo},
comp::{
projectile, Energy, Group, HealthSource, Inventory, Ori, PhysicsState, Pos, Projectile,
Stats, Vel,
projectile, Combo, Energy, Group, HealthSource, Inventory, Ori, PhysicsState, Pos,
Projectile, Stats, Vel,
},
event::{EventBus, ServerEvent},
resources::DeltaTime,
@ -30,6 +30,7 @@ pub struct ReadData<'a> {
groups: ReadStorage<'a, Group>,
energies: ReadStorage<'a, Energy>,
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
}
/// This system is responsible for handling projectile effect triggers
@ -110,6 +111,7 @@ impl<'a> System<'a> for Sys {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
}
});

View File

@ -1,8 +1,8 @@
use common::{
combat::{AttackerInfo, TargetInfo},
comp::{
Body, Energy, Group, Health, HealthSource, Inventory, Last, Ori, PhysicsState, Pos, Scale,
Shockwave, ShockwaveHitEntities, Stats,
Body, Combo, Energy, Group, Health, HealthSource, Inventory, Last, Ori, PhysicsState, Pos,
Scale, Shockwave, ShockwaveHitEntities, Stats,
},
event::{EventBus, ServerEvent},
resources::{DeltaTime, Time},
@ -36,6 +36,7 @@ pub struct ReadData<'a> {
physics_states: ReadStorage<'a, PhysicsState>,
energies: ReadStorage<'a, Energy>,
stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>,
}
/// This system is responsible for handling accepted inputs like moving or
@ -184,6 +185,7 @@ impl<'a> System<'a> for Sys {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
});
let target_info = TargetInfo {

View File

@ -1,5 +1,6 @@
use common::{
comp::{
self,
skills::{GeneralSkill, Skill},
Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health, Poise,
PoiseChange, PoiseSource, Pos, Stats,
@ -18,7 +19,6 @@ use vek::Vec3;
const ENERGY_REGEN_ACCEL: f32 = 10.0;
const POISE_REGEN_ACCEL: f32 = 2.0;
const COMBO_DECAY_START: f64 = 5.0; // seconds
#[derive(SystemData)]
pub struct ReadData<'a> {
@ -232,7 +232,9 @@ impl<'a> System<'a> for Sys {
| CharacterState::ChargedRanged { .. }
| CharacterState::RepeaterRanged { .. }
| CharacterState::Shockwave { .. }
| CharacterState::BasicBeam { .. } => {
| CharacterState::BasicBeam { .. }
| CharacterState::BasicAura { .. }
| CharacterState::HealingBeam { .. } => {
if energy.get_unchecked().regen_rate != 0.0 {
energy.get_mut_unchecked().regen_rate = 0.0
}
@ -262,7 +264,9 @@ impl<'a> System<'a> for Sys {
// Decay combo
for (_, mut combo) in (&read_data.entities, &mut combos).join() {
if combo.counter() > 0 && read_data.time.0 - combo.last_increase() > COMBO_DECAY_START {
if combo.counter() > 0
&& read_data.time.0 - combo.last_increase() > comp::combo::COMBO_DECAY_START
{
combo.reset();
}
}

View File

@ -1,7 +1,6 @@
use crate::{sys, Server, StateExt};
use common::{
character::CharacterId,
combat,
comp::{
self,
aura::{Aura, AuraKind, AuraTarget},
@ -175,10 +174,7 @@ pub fn handle_beam(server: &mut Server, properties: beam::Properties, pos: Pos,
let ecs = state.ecs();
ecs.write_resource::<Vec<Outcome>>().push(Outcome::Beam {
pos: pos.0,
heal: properties
.attack
.effects()
.any(|e| matches!(e.effect(), combat::CombatEffect::Heal(h) if *h > 0.0)),
specifier: properties.specifier,
});
state.create_beam(properties, pos, ori).build();
}

View File

@ -676,6 +676,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
},
RadiusEffect::Attack(attack) => {
let energies = &ecs.read_storage::<comp::Energy>();
let combos = &ecs.read_storage::<comp::Combo>();
for (entity_b, pos_b, _health_b, inventory_b_maybe, stats_b_maybe) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
@ -715,6 +716,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
entity,
uid,
energy: energies.get(entity),
combo: combos.get(entity),
});
let target_info = combat::TargetInfo {
@ -893,11 +895,14 @@ fn handle_exp_gain(
pub fn handle_combo_change(server: &Server, entity: EcsEntity, change: i32) {
let ecs = &server.state.ecs();
if let Some(mut combo) = ecs.write_storage::<comp::Combo>().get_mut(entity) {
if change > 0 {
let time = ecs.read_resource::<Time>();
combo.increase_by(change as u32, time.0);
} else {
combo.decrease_by(change.abs() as u32);
let time = ecs.read_resource::<Time>();
let mut outcomes = ecs.write_resource::<Vec<Outcome>>();
combo.change_by(change, time.0);
if let Some(uid) = ecs.read_storage::<Uid>().get(entity) {
outcomes.push(Outcome::ComboChange {
uid: *uid,
combo: combo.counter(),
});
}
}
}

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1,16 @@
-- Resets sceptre skill tree by deleting scetpre skills and setting available skill points to earned skill points
-- Deletes all sceptre skills, does not delete unlock sceptre skill
DELETE FROM skill WHERE skill = 'Sceptre BHeal';
DELETE FROM skill WHERE skill = 'Sceptre BDamage';
DELETE FROM skill WHERE skill = 'Sceptre BRange';
DELETE FROM skill WHERE skill = 'Sceptre BLifesteal';
DELETE FROM skill WHERE skill = 'Sceptre BRegen';
DELETE FROM skill WHERE skill = 'Sceptre BCost';
DELETE FROM skill WHERE skill = 'Sceptre PHeal';
DELETE FROM skill WHERE skill = 'Sceptre PDamage';
DELETE FROM skill WHERE skill = 'Sceptre PRadius';
DELETE FROM skill WHERE skill = 'Sceptre PCost';
DELETE FROM skill WHERE skill = 'Sceptre PProjSpeed';
-- Resets available skill points to earned skill points for sceptre skill tree
UPDATE skill_group
SET available_sp = earned_sp WHERE skill_group_kind = 'Weapon Sceptre';

View File

@ -118,17 +118,18 @@ pub fn skill_to_db_string(skill: comp::skills::Skill) -> String {
Staff(StaffSkill::SKnockback) => "Staff SKnockback",
Staff(StaffSkill::SRange) => "Staff SRange",
Staff(StaffSkill::SCost) => "Staff SCost",
Sceptre(SceptreSkill::BHeal) => "Sceptre BHeal",
Sceptre(SceptreSkill::BDamage) => "Sceptre BDamage",
Sceptre(SceptreSkill::BRange) => "Sceptre BRange",
Sceptre(SceptreSkill::BLifesteal) => "Sceptre BLifesteal",
Sceptre(SceptreSkill::BRegen) => "Sceptre BRegen",
Sceptre(SceptreSkill::BCost) => "Sceptre BCost",
Sceptre(SceptreSkill::PHeal) => "Sceptre PHeal",
Sceptre(SceptreSkill::PDamage) => "Sceptre PDamage",
Sceptre(SceptreSkill::PRadius) => "Sceptre PRadius",
Sceptre(SceptreSkill::PCost) => "Sceptre PCost",
Sceptre(SceptreSkill::PProjSpeed) => "Sceptre PProjSpeed",
Sceptre(SceptreSkill::LDamage) => "Sceptre LDamage",
Sceptre(SceptreSkill::LRange) => "Sceptre LRange",
Sceptre(SceptreSkill::LLifesteal) => "Sceptre LLifesteal",
Sceptre(SceptreSkill::LRegen) => "Sceptre LRegen",
Sceptre(SceptreSkill::HHeal) => "Sceptre HHeal",
Sceptre(SceptreSkill::HCost) => "Sceptre HCost",
Sceptre(SceptreSkill::HRange) => "Sceptre HRange",
Sceptre(SceptreSkill::UnlockAura) => "Sceptre UnlockAura",
Sceptre(SceptreSkill::AStrength) => "Sceptre AStrength",
Sceptre(SceptreSkill::ADuration) => "Sceptre ADuration",
Sceptre(SceptreSkill::ARange) => "Sceptre ARange",
Sceptre(SceptreSkill::ACost) => "Sceptre ACost",
Roll(RollSkill::ImmuneMelee) => "Roll ImmuneMelee",
Roll(RollSkill::Cost) => "Roll Cost",
Roll(RollSkill::Strength) => "Roll Strength",
@ -240,17 +241,18 @@ pub fn db_string_to_skill(skill_string: &str) -> comp::skills::Skill {
"Staff SKnockback" => Staff(StaffSkill::SKnockback),
"Staff SRange" => Staff(StaffSkill::SRange),
"Staff SCost" => Staff(StaffSkill::SCost),
"Sceptre BHeal" => Sceptre(SceptreSkill::BHeal),
"Sceptre BDamage" => Sceptre(SceptreSkill::BDamage),
"Sceptre BRange" => Sceptre(SceptreSkill::BRange),
"Sceptre BLifesteal" => Sceptre(SceptreSkill::BLifesteal),
"Sceptre BRegen" => Sceptre(SceptreSkill::BRegen),
"Sceptre BCost" => Sceptre(SceptreSkill::BCost),
"Sceptre PHeal" => Sceptre(SceptreSkill::PHeal),
"Sceptre PDamage" => Sceptre(SceptreSkill::PDamage),
"Sceptre PRadius" => Sceptre(SceptreSkill::PRadius),
"Sceptre PCost" => Sceptre(SceptreSkill::PCost),
"Sceptre PProjSpeed" => Sceptre(SceptreSkill::PProjSpeed),
"Sceptre LDamage" => Sceptre(SceptreSkill::LDamage),
"Sceptre LRange" => Sceptre(SceptreSkill::LRange),
"Sceptre LLifesteal" => Sceptre(SceptreSkill::LLifesteal),
"Sceptre LRegen" => Sceptre(SceptreSkill::LRegen),
"Sceptre HHeal" => Sceptre(SceptreSkill::HHeal),
"Sceptre HCost" => Sceptre(SceptreSkill::HCost),
"Sceptre HRange" => Sceptre(SceptreSkill::HRange),
"Sceptre UnlockAura" => Sceptre(SceptreSkill::UnlockAura),
"Sceptre AStrength" => Sceptre(SceptreSkill::AStrength),
"Sceptre ADuration" => Sceptre(SceptreSkill::ADuration),
"Sceptre ARange" => Sceptre(SceptreSkill::ARange),
"Sceptre ACost" => Sceptre(SceptreSkill::ACost),
"Roll ImmuneMelee" => Roll(RollSkill::ImmuneMelee),
"Roll Cost" => Roll(RollSkill::Cost),
"Roll Strength" => Roll(RollSkill::Strength),

View File

@ -184,6 +184,7 @@ impl StateExt for State {
.with(inventory)
.with(comp::Buffs::default())
.with(comp::Combo::default())
.with(comp::Auras::default())
}
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder {
@ -275,6 +276,7 @@ impl StateExt for State {
comp::Alignment::Owned(self.read_component_copied(entity).unwrap()),
);
self.write_component(entity, comp::Buffs::default());
self.write_component(entity, comp::Auras::default());
self.write_component(entity, comp::Combo::default());
// Make sure physics components are updated

View File

@ -36,7 +36,7 @@ impl Animation for ShockwaveAnimation {
let (move1, move2, move3) = match stage_section {
Some(StageSection::Buildup) => (anim_time, 0.0, 0.0),
Some(StageSection::Swing) => (1.0, anim_time, 0.0),
Some(StageSection::Swing) | Some(StageSection::Cast) => (1.0, anim_time, 0.0),
Some(StageSection::Recover) => (1.0, 1.0, anim_time),
_ => (0.0, 0.0, 0.0),
};

View File

@ -91,6 +91,7 @@ use client::Client;
use common::{
assets::{self, AssetExt, AssetHandle},
comp::{
beam,
item::{ItemKind, Reagent, ToolKind},
object, Body, CharacterAbilityType, InventoryUpdateEvent,
},
@ -353,16 +354,17 @@ impl SfxMgr {
let file_ref = "voxygen.audio.sfx.character.level_up_sound_-_shorter_wind_up";
audio.play_sfx(file_ref, *pos, None);
},
Outcome::Beam { pos, heal } => {
if *heal {
Outcome::Beam { pos, specifier } => match specifier {
beam::FrontendSpecifier::LifestealBeam | beam::FrontendSpecifier::HealingBeam => {
let file_ref = "voxygen.audio.sfx.abilities.staff_channeling";
audio.play_sfx(file_ref, *pos, None);
} else {
},
beam::FrontendSpecifier::Flamethrower => {
let file_ref = "voxygen.audio.sfx.abilities.flame_thrower";
audio.play_sfx(file_ref, *pos, None);
}
},
},
Outcome::ExpChange { .. } => {},
Outcome::ExpChange { .. } | Outcome::ComboChange { .. } => {},
}
}

View File

@ -135,19 +135,20 @@ widget_ids! {
skill_staff_shockwave_3,
skill_staff_shockwave_4,
sceptre_render,
skill_sceptre_beam_0,
skill_sceptre_beam_1,
skill_sceptre_beam_2,
skill_sceptre_beam_3,
skill_sceptre_beam_4,
skill_sceptre_beam_5,
skill_sceptre_beam_6,
skill_sceptre_bomb_0,
skill_sceptre_bomb_1,
skill_sceptre_bomb_2,
skill_sceptre_bomb_3,
skill_sceptre_bomb_4,
skill_sceptre_bomb_5,
skill_sceptre_lifesteal_0,
skill_sceptre_lifesteal_1,
skill_sceptre_lifesteal_2,
skill_sceptre_lifesteal_3,
skill_sceptre_lifesteal_4,
skill_sceptre_heal_0,
skill_sceptre_heal_1,
skill_sceptre_heal_2,
skill_sceptre_heal_3,
skill_sceptre_aura_0,
skill_sceptre_aura_1,
skill_sceptre_aura_2,
skill_sceptre_aura_3,
skill_sceptre_aura_4,
general_combat_render_0,
general_combat_render_1,
skill_general_stat_0,
@ -547,7 +548,7 @@ impl<'a> Widget for Diary<'a> {
SelectedSkillTree::Weapon(ToolKind::Hammer) => 5,
SelectedSkillTree::Weapon(ToolKind::Bow) => 3,
SelectedSkillTree::Weapon(ToolKind::Staff) => 5,
SelectedSkillTree::Weapon(ToolKind::Sceptre) => 7,
SelectedSkillTree::Weapon(ToolKind::Sceptre) => 5,
_ => 0,
};
let skills_top_r = match sel_tab {
@ -557,7 +558,7 @@ impl<'a> Widget for Diary<'a> {
SelectedSkillTree::Weapon(ToolKind::Hammer) => 5,
SelectedSkillTree::Weapon(ToolKind::Bow) => 7,
SelectedSkillTree::Weapon(ToolKind::Staff) => 5,
SelectedSkillTree::Weapon(ToolKind::Sceptre) => 6,
SelectedSkillTree::Weapon(ToolKind::Sceptre) => 4,
_ => 0,
};
let skills_bot_l = match sel_tab {
@ -567,6 +568,7 @@ impl<'a> Widget for Diary<'a> {
SelectedSkillTree::Weapon(ToolKind::Hammer) => 6,
SelectedSkillTree::Weapon(ToolKind::Bow) => 5,
SelectedSkillTree::Weapon(ToolKind::Staff) => 5,
SelectedSkillTree::Weapon(ToolKind::Sceptre) => 5,
_ => 0,
};
let skills_bot_r = match sel_tab {
@ -3261,20 +3263,20 @@ impl<'a> Widget for Diary<'a> {
// 5 1 6
// 3 0 4
// 8 2 7
Button::image(self.imgs.heal_0)
Button::image(self.imgs.skill_sceptre_lifesteal)
.w_h(74.0, 74.0)
.mid_top_with_margin_on(state.skills_top_l[0], 3.0)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_beam_title"),
&self.localized_strings.get("hud.skill.sc_beam"),
&self.localized_strings.get("hud.skill.sc_lifesteal_title"),
&self.localized_strings.get("hud.skill.sc_lifesteal"),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_0, ui);
let skill = Skill::Sceptre(BHeal);
.set(state.skill_sceptre_lifesteal_0, ui);
let skill = Skill::Sceptre(LDamage);
if create_skill_button(
self.imgs.heal_heal_skill,
self.imgs.magic_damage_skill,
state.skills_top_l[1],
&self.stats.skill_set,
skill,
@ -3283,9 +3285,11 @@ impl<'a> Widget for Diary<'a> {
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_beam_heal_title"),
&self
.localized_strings
.get("hud.skill.sc_lifesteal_damage_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_beam_heal"),
&self.localized_strings.get("hud.skill.sc_lifesteal_damage"),
skill,
&self.stats.skill_set,
&self.localized_strings,
@ -3293,14 +3297,14 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_1, ui)
.set(state.skill_sceptre_lifesteal_1, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(BDamage);
let skill = Skill::Sceptre(LRange);
if create_skill_button(
self.imgs.heal_damage_skill,
self.imgs.magic_distance_skill,
state.skills_top_l[2],
&self.stats.skill_set,
skill,
@ -3309,9 +3313,11 @@ impl<'a> Widget for Diary<'a> {
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_beam_damage_title"),
&self
.localized_strings
.get("hud.skill.sc_lifesteal_range_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_beam_damage"),
&self.localized_strings.get("hud.skill.sc_lifesteal_range"),
skill,
&self.stats.skill_set,
&self.localized_strings,
@ -3319,14 +3325,14 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_2, ui)
.set(state.skill_sceptre_lifesteal_2, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(BRegen);
let skill = Skill::Sceptre(LLifesteal);
if create_skill_button(
self.imgs.heal_energy_regen_skill,
self.imgs.magic_lifesteal_skill,
state.skills_top_l[3],
&self.stats.skill_set,
skill,
@ -3337,9 +3343,11 @@ impl<'a> Widget for Diary<'a> {
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_energy_regen_title"),
.get("hud.skill.sc_lifesteal_lifesteal_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_energy_regen"),
&self
.localized_strings
.get("hud.skill.sc_lifesteal_lifesteal"),
skill,
&self.stats.skill_set,
&self.localized_strings,
@ -3347,55 +3355,27 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_3, ui)
.set(state.skill_sceptre_lifesteal_3, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(BRange);
let skill = Skill::Sceptre(LRegen);
if create_skill_button(
self.imgs.heal_radius_skill,
self.imgs.magic_energy_regen_skill,
state.skills_top_l[4],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_range_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_range"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_4, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(BLifesteal);
if create_skill_button(
self.imgs.heal_lifesteal_skill,
state.skills_top_l[5],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_lifesteal_efficiency_title"),
.get("hud.skill.sc_lifesteal_regen_title"),
&add_sp_cost_tooltip(
&self
.localized_strings
.get("hud.skill.sc_lifesteal_efficiency"),
&self.localized_strings.get("hud.skill.sc_lifesteal_regen"),
skill,
&self.stats.skill_set,
&self.localized_strings,
@ -3403,15 +3383,53 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_5, ui)
.set(state.skill_sceptre_lifesteal_4, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(BCost);
// Top right skills
Button::image(self.imgs.skill_sceptre_heal)
.w_h(74.0, 74.0)
.mid_top_with_margin_on(state.skills_top_r[0], 3.0)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_heal_title"),
&self.localized_strings.get("hud.skill.sc_heal"),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_heal_0, ui);
let skill = Skill::Sceptre(HHeal);
if create_skill_button(
self.imgs.heal_heal_skill,
state.skills_top_r[1],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_heal_heal_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_heal_heal"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_heal_1, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(HCost);
if create_skill_button(
self.imgs.heal_cost_skill,
state.skills_top_l[6],
state.skills_top_r[2],
&self.stats.skill_set,
skill,
self.fonts,
@ -3429,78 +3447,14 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_beam_6, ui)
.set(state.skill_sceptre_heal_2, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
Button::image(self.imgs.heal_bomb)
.w_h(74.0, 74.0)
.mid_top_with_margin_on(state.skills_top_r[0], 3.0)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_healbomb_title"),
&self.localized_strings.get("hud.skill.sc_healbomb"),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_bomb_0, ui);
// Top right skills
let skill = Skill::Sceptre(PHeal);
let skill = Skill::Sceptre(HRange);
if create_skill_button(
self.imgs.heal_heal_skill,
state.skills_top_r[1],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_heal_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_heal"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_bomb_1, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(PDamage);
if create_skill_button(
self.imgs.heal_damage_skill,
state.skills_top_r[2],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_damage_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_damage"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_bomb_2, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(PRadius);
if create_skill_button(
self.imgs.heal_radius_skill,
self.imgs.heal_distance_skill,
state.skills_top_r[3],
&self.stats.skill_set,
skill,
@ -3509,9 +3463,9 @@ impl<'a> Widget for Diary<'a> {
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_radius_title"),
&self.localized_strings.get("hud.skill.sc_heal_range_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_radius"),
&self.localized_strings.get("hud.skill.sc_heal_range"),
skill,
&self.stats.skill_set,
&self.localized_strings,
@ -3519,41 +3473,16 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_bomb_3, ui)
.set(state.skill_sceptre_heal_3, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(PCost);
// Bottom left skills
let skill = Skill::Sceptre(UnlockAura);
if create_skill_button(
self.imgs.heal_cost_skill,
state.skills_top_r[4],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get("hud.skill.sc_energy_cost_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_energy_cost"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_bomb_4, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(PProjSpeed);
if create_skill_button(
self.imgs.heal_projectile_speed_skill,
state.skills_top_r[5],
self.imgs.skill_sceptre_aura,
state.skills_bot_l[0],
&self.stats.skill_set,
skill,
self.fonts,
@ -3563,9 +3492,9 @@ impl<'a> Widget for Diary<'a> {
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_projectile_speed_title"),
.get("hud.skill.sc_wardaura_unlock_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_projectile_speed"),
&self.localized_strings.get("hud.skill.sc_wardaura_unlock"),
skill,
&self.stats.skill_set,
&self.localized_strings,
@ -3573,7 +3502,119 @@ impl<'a> Widget for Diary<'a> {
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_bomb_5, ui)
.set(state.skill_sceptre_aura_0, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(AStrength);
if create_skill_button(
self.imgs.buff_damage_skill,
state.skills_bot_l[1],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_wardaura_strength_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_wardaura_strength"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_aura_1, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(ADuration);
if create_skill_button(
self.imgs.buff_speed_skill,
state.skills_bot_l[2],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_wardaura_duration_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_wardaura_duration"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_aura_2, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(ARange);
if create_skill_button(
self.imgs.buff_radius_skill,
state.skills_bot_l[3],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_wardaura_range_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_wardaura_range"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_aura_3, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));
};
let skill = Skill::Sceptre(ACost);
if create_skill_button(
self.imgs.buff_cost_skill,
state.skills_bot_l[4],
&self.stats.skill_set,
skill,
self.fonts,
&get_skill_label(skill, &self.stats.skill_set),
)
.with_tooltip(
self.tooltip_manager,
&self
.localized_strings
.get("hud.skill.sc_wardaura_cost_title"),
&add_sp_cost_tooltip(
&self.localized_strings.get("hud.skill.sc_wardaura_cost"),
skill,
&self.stats.skill_set,
&self.localized_strings,
),
&diary_tooltip,
TEXT_COLOR,
)
.set(state.skill_sceptre_aura_4, ui)
.was_clicked()
{
events.push(Event::UnlockSkill(skill));

View File

@ -446,12 +446,13 @@ image_ids! {
// Icons
snake_arrow_0: "voxygen.element.icons.snake",
heal_0: "voxygen.element.icons.heal_0",
skill_sceptre_lifesteal: "voxygen.element.icons.lifesteal",
sword_whirlwind: "voxygen.element.icons.sword_whirlwind",
heal_bomb: "voxygen.element.icons.heal_bomb",
skill_sceptre_heal: "voxygen.element.icons.heal_0",
hammerleap: "voxygen.element.icons.skill_hammerleap",
skill_axe_leap_slash: "voxygen.element.icons.skill_axe_leap_slash",
skill_bow_jump_burst: "voxygen.element.icons.skill_bow_jump_burst",
skill_sceptre_aura: "voxygen.element.icons.sceptre_protection",
missing_icon: "voxygen.element.icons.missing_icon_grey",
// Buttons
@ -541,6 +542,7 @@ image_ids! {
buff_energyplus_0: "voxygen.element.icons.de_buffs.buff_energyplus_0",
buff_healthplus_0: "voxygen.element.icons.de_buffs.buff_healthplus_0",
buff_invincibility_0: "voxygen.element.icons.de_buffs.buff_invincibility_0",
buff_dmg_red_0: "voxygen.element.icons.de_buffs.buff_damage_reduce_0",
// Debuffs
debuff_skull_0: "voxygen.element.icons.de_buffs.debuff_skull_0",

View File

@ -61,7 +61,6 @@ use crate::{
GlobalState,
};
use client::Client;
use common::{
combat,
comp::{
@ -309,6 +308,13 @@ pub struct SkillPointGain {
pub timer: f32,
}
#[derive(Debug, Clone, Copy)]
pub struct ComboFloater {
pub owner: Uid,
pub combo: u32,
pub timer: f64,
}
pub struct DebugInfo {
pub tps: f64,
pub frame_time: Duration,
@ -732,6 +738,7 @@ pub struct Hud {
crosshair_opacity: f32,
exp_floaters: Vec<ExpFloater>,
skill_point_displays: Vec<SkillPointGain>,
combo_floaters: VecDeque<ComboFloater>,
}
impl Hud {
@ -840,6 +847,7 @@ impl Hud {
crosshair_opacity: 0.0,
exp_floaters: Vec::new(),
skill_point_displays: Vec::new(),
combo_floaters: VecDeque::new(),
}
}
@ -2158,6 +2166,19 @@ impl Hud {
let controllers = ecs.read_storage::<comp::Controller>();
let ability_map = ecs.fetch::<comp::item::tool::AbilityMap>();
let bodies = ecs.read_storage::<comp::Body>();
// Combo floater stuffs
for combo_floater in self.combo_floaters.iter_mut() {
combo_floater.timer -= dt.as_secs_f64();
}
self.combo_floaters.retain(|f| f.timer > 0_f64);
let combo = if let Some(uid) = ecs.read_storage::<Uid>().get(entity) {
self.combo_floaters
.iter()
.find(|c| c.owner == *uid)
.copied()
} else {
None
};
if let (
Some(health),
@ -2190,6 +2211,7 @@ impl Hud {
i18n,
&ability_map,
&msm,
combo,
)
.set(self.ids.skillbar, ui_widgets);
}
@ -3203,6 +3225,11 @@ impl Hud {
total_points: *total_points,
timer: 5.0,
}),
Outcome::ComboChange { uid, combo } => self.combo_floaters.push_front(ComboFloater {
owner: *uid,
combo: *combo,
timer: comp::combo::COMBO_DECAY_START,
}),
_ => {},
}
}
@ -3278,6 +3305,7 @@ pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id {
BuffKind::IncreaseMaxEnergy { .. } => imgs.buff_energyplus_0,
BuffKind::IncreaseMaxHealth { .. } => imgs.buff_healthplus_0,
BuffKind::Invulnerability => imgs.buff_invincibility_0,
BuffKind::ProtectingWard => imgs.buff_dmg_red_0,
// Debuffs
BuffKind::Bleeding { .. } => imgs.debuff_bleed_0,
BuffKind::Cursed { .. } => imgs.debuff_skull_0,
@ -3294,6 +3322,7 @@ pub fn get_buff_title(buff: BuffKind, localized_strings: &Localization) -> &str
BuffKind::IncreaseMaxHealth { .. } => localized_strings.get("buff.title.IncreaseMaxHealth"),
BuffKind::IncreaseMaxEnergy { .. } => localized_strings.get("buff.title.staminaup"),
BuffKind::Invulnerability => localized_strings.get("buff.title.invulnerability"),
BuffKind::ProtectingWard => localized_strings.get("buff.title.protectingward"),
// Debuffs
BuffKind::Bleeding { .. } => localized_strings.get("buff.title.bleed"),
BuffKind::Cursed { .. } => localized_strings.get("buff.title.cursed"),
@ -3310,6 +3339,7 @@ pub fn get_buff_desc(buff: BuffKind, localized_strings: &Localization) -> &str {
BuffKind::IncreaseMaxHealth { .. } => localized_strings.get("buff.desc.IncreaseMaxHealth"),
BuffKind::IncreaseMaxEnergy { .. } => localized_strings.get("buff.desc.IncreaseMaxEnergy"),
BuffKind::Invulnerability => localized_strings.get("buff.desc.invulnerability"),
BuffKind::ProtectingWard => localized_strings.get("buff.desc.protectingward"),
// Debuffs
BuffKind::Bleeding { .. } => localized_strings.get("buff.desc.bleed"),
BuffKind::Cursed { .. } => localized_strings.get("buff.desc.cursed"),

View File

@ -6,6 +6,7 @@ use super::{
STAMINA_COLOR, TEXT_COLOR, UI_HIGHLIGHT_0,
};
use crate::{
hud::ComboFloater,
i18n::Localization,
ui::{
fonts::Fonts,
@ -16,6 +17,7 @@ use crate::{
GlobalState,
};
use common::comp::{
self,
inventory::slot::EquipSlot,
item::{
tool::{AbilityMap, Tool, ToolKind},
@ -74,6 +76,10 @@ widget_ids! {
stamina_txt_alignment,
stamina_txt_bg,
stamina_txt,
// Combo Counter
combo_align,
combo_bg,
combo,
// Slots
m1_slot,
m1_slot_bg,
@ -141,6 +147,7 @@ pub struct Skillbar<'a> {
common: widget::CommonBuilder,
ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
combo: Option<ComboFloater>,
}
impl<'a> Skillbar<'a> {
@ -163,6 +170,7 @@ impl<'a> Skillbar<'a> {
localized_strings: &'a Localization,
ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
combo: Option<ComboFloater>,
) -> Self {
Self {
global_state,
@ -183,6 +191,7 @@ impl<'a> Skillbar<'a> {
localized_strings,
ability_map,
msm,
combo,
}
}
}
@ -575,7 +584,7 @@ impl<'a> Widget for Skillbar<'a> {
ToolKind::Hammer => self.imgs.twohhammer_m1,
ToolKind::Axe => self.imgs.twohaxe_m1,
ToolKind::Bow => self.imgs.bow_m1,
ToolKind::Sceptre => self.imgs.heal_0,
ToolKind::Sceptre => self.imgs.skill_sceptre_lifesteal,
ToolKind::Staff => self.imgs.fireball,
ToolKind::Debug => self.imgs.flyingrod_m1,
_ => self.imgs.nothing,
@ -622,7 +631,7 @@ impl<'a> Widget for Skillbar<'a> {
Some(ToolKind::Hammer) => self.imgs.hammergolf,
Some(ToolKind::Axe) => self.imgs.axespin,
Some(ToolKind::Bow) => self.imgs.bow_m2,
Some(ToolKind::Sceptre) => self.imgs.heal_bomb,
Some(ToolKind::Sceptre) => self.imgs.skill_sceptre_heal,
Some(ToolKind::Staff) => self.imgs.flamethrower,
Some(ToolKind::Debug) => self.imgs.flyingrod_m2,
_ => self.imgs.nothing,
@ -909,6 +918,44 @@ impl<'a> Widget for Skillbar<'a> {
.w_h(16.0, 18.0)
.mid_bottom_with_margin_on(state.ids.m2_content, -11.0)
.set(state.ids.m2_ico, ui);
// Combo Counter
if let Some(combo) = self.combo {
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;
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
(1.0 - combo_cnt / (combo_cnt + 20.0)).max(0.79),
(1.0 - combo_cnt / (combo_cnt + 80.0)).max(0.19),
(1.0 - combo_cnt / (combo_cnt + 5.0)).max(0.17),
alpha,
);
let fnt_size = ((14.0 + combo.timer as f32 * 0.8).min(30.0)) as u32
+ if (time_since_last_update) < 0.1 { 2 } else { 0 }; // Increase size for higher counts, "flash" on update by increasing the font size by 2
Rectangle::fill_with([10.0, 10.0], color::TRANSPARENT)
.middle_of(ui.window)
.set(state.ids.combo_align, ui);
Text::new(combo_txt.as_str())
.mid_bottom_with_margin_on(
state.ids.combo_align,
-350.0 + time_since_last_update * -8.0,
)
.font_size(self.fonts.cyri.scale(fnt_size))
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, alpha))
.set(state.ids.combo_bg, ui);
Text::new(combo_txt.as_str())
.bottom_right_with_margins_on(state.ids.combo_bg, 1.0, 1.0)
.font_size(self.fonts.cyri.scale(fnt_size))
.font_id(self.fonts.cyri.conrod_id)
.color(fnt_col)
.set(state.ids.combo, ui);
}
}
}
}
@ -936,6 +983,10 @@ fn ability_description(tool: &ToolKind) -> Option<(&str, &str)> {
"Possessing Arrow",
"\nShoots a poisonous arrow.\nLets you control your target.",
)),
ToolKind::Sceptre => Some((
"Thorn Bulwark",
"\nProtects you and your group with thorns\nfor a short amount of time.",
)),
_ => None,
}
}

View File

@ -116,6 +116,7 @@ pub enum HotbarImage {
HammerLeap,
AxeLeapSlash,
BowJumpBurst,
SceptreAura,
}
type HotbarSource<'a> = (
@ -239,6 +240,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
HotbarImage::HammerLeap => vec![imgs.hammerleap],
HotbarImage::AxeLeapSlash => vec![imgs.skill_axe_leap_slash],
HotbarImage::BowJumpBurst => vec![imgs.skill_bow_jump_burst],
HotbarImage::SceptreAura => vec![imgs.skill_sceptre_aura],
}
}
}
@ -268,6 +270,7 @@ fn hotbar_image(tool: ToolKind) -> Option<HotbarImage> {
ToolKind::Bow => Some(HotbarImage::BowJumpBurst),
ToolKind::Debug => Some(HotbarImage::SnakeArrow),
ToolKind::Sword => Some(HotbarImage::SwordWhirlwind),
ToolKind::Sceptre => Some(HotbarImage::SceptreAura),
_ => None,
}
}

View File

@ -107,7 +107,10 @@ fn consumable_desc(effects: &[Effect], desc: &str) -> String {
format!("Raises Maximum Health by {}", strength)
},
BuffKind::Invulnerability => "Grants invulnerability".to_string(),
BuffKind::Bleeding | BuffKind::CampfireHeal | BuffKind::Cursed => continue,
BuffKind::Bleeding
| BuffKind::CampfireHeal
| BuffKind::Cursed
| BuffKind::ProtectingWard => continue,
};
write!(&mut description, "\n\n{}", buff_desc).unwrap();
@ -125,7 +128,8 @@ fn consumable_desc(effects: &[Effect], desc: &str) -> String {
BuffKind::Bleeding
| BuffKind::Potion
| BuffKind::CampfireHeal
| BuffKind::Cursed => continue,
| BuffKind::Cursed
| BuffKind::ProtectingWard => continue,
}
} else if let BuffKind::Saturation | BuffKind::Regeneration = buff.kind {
"every second".to_string()

View File

@ -118,6 +118,7 @@ pub enum ParticleMode {
Snow = 19,
Explosion = 20,
Ice = 21,
LifestealBeam = 22,
}
impl ParticleMode {

View File

@ -1099,6 +1099,34 @@ impl FigureMgr {
skeleton_attr,
)
},
CharacterState::BasicAura(s) => {
let stage_time = s.timer.as_secs_f32();
let stage_progress = match s.stage_section {
StageSection::Buildup => {
stage_time / s.static_data.buildup_duration.as_secs_f32()
},
StageSection::Cast => {
stage_time / s.static_data.cast_duration.as_secs_f32()
},
StageSection::Recover => {
stage_time / s.static_data.recover_duration.as_secs_f32()
},
_ => 0.0,
};
anim::character::ShockwaveAnimation::update_skeleton(
&target_base,
(
active_tool_kind,
second_tool_kind,
time,
vel.0.magnitude(),
Some(s.stage_section),
),
stage_progress,
&mut state_animation_rate,
skeleton_attr,
)
},
CharacterState::LeapMelee(s) => {
let stage_progress = match active_tool_kind {
Some(ToolKind::Axe | ToolKind::Hammer) => {
@ -1252,6 +1280,32 @@ impl FigureMgr {
skeleton_attr,
)
},
CharacterState::HealingBeam(s) => {
let stage_time = s.timer.as_secs_f32();
let stage_progress = match s.stage_section {
StageSection::Buildup => {
stage_time / s.static_data.buildup_duration.as_secs_f32()
},
StageSection::Cast => s.timer.as_secs_f32(),
StageSection::Recover => {
stage_time / s.static_data.recover_duration.as_secs_f32()
},
_ => 0.0,
};
anim::character::BeamAnimation::update_skeleton(
&target_base,
(
active_tool_kind,
second_tool_kind,
time,
vel.0.magnitude(),
Some(s.stage_section),
),
stage_progress,
&mut state_animation_rate,
skeleton_attr,
)
},
CharacterState::ComboMelee(s) => {
let stage_index = (s.stage - 1) as usize;
let stage_time = s.timer.as_secs_f32();

View File

@ -8,8 +8,10 @@ use crate::{
};
use common::{
assets::{AssetExt, DotVoxAsset},
combat::CombatEffect,
comp::{item::Reagent, object, BeamSegment, Body, CharacterState, Ori, Pos, Shockwave},
comp::{
self, aura, beam, buff, item::Reagent, object, BeamSegment, Body, CharacterState, Ori, Pos,
Shockwave,
},
figure::Segment,
outcome::Outcome,
resources::DeltaTime,
@ -180,6 +182,7 @@ impl ParticleMgr {
self.maintain_beam_particles(scene_data, lights);
self.maintain_block_particles(scene_data, terrain);
self.maintain_shockwave_particles(scene_data);
self.maintain_aura_particles(scene_data);
} else {
// remove all particle lifespans
self.particles.clear();
@ -401,104 +404,113 @@ impl ParticleMgr {
.join()
.filter(|(_, _, b)| b.creation.map_or(true, |c| (c + dt as f64) >= time))
{
//
// TODO: Handle this less hackily. Done this way as beam segments are created
// every server tick, which is approximately 33 ms. Heartbeat scheduler used to
// account for clients with less than 30 fps because they start the creation
// time when the segments are received and could receive 2 at once
let beam_tick_count = 33.max(self.scheduler.heartbeats(Duration::from_millis(1)));
let range = beam.properties.speed * beam.properties.duration.as_secs_f32();
if beam
.properties
.attack
.effects()
.any(|e| matches!(e.effect(), CombatEffect::Heal(h) if *h > 0.0))
{
// Emit a light when using healing
lights.push(Light::new(pos.0, Rgb::new(0.1, 1.0, 0.15), 1.0));
for i in 0..self.scheduler.heartbeats(Duration::from_millis(1)) {
self.particles.push(Particle::new_directed(
beam.properties.duration,
time + i as f64 / 1000.0,
ParticleMode::HealingBeam,
match beam.properties.specifier {
beam::FrontendSpecifier::Flamethrower => {
let mut rng = thread_rng();
let (from, to) = (Vec3::<f32>::unit_z(), *ori.look_dir());
let m = Mat3::<f32>::rotation_from_to_3d(from, to);
// Emit a light when using flames
lights.push(Light::new(
pos.0,
pos.0 + *ori.look_dir() * range,
Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
2.0,
));
/*
if let CharacterState::BasicBeam(b) = character_state {
if b.stage_section == StageSection::Cast {
if b.static_data.base_hps > 0.0 {//
// Emit a light when using healing
lights.push(Light::new(pos.0 + b.offset, Rgb::new(0.1, 1.0, 0.15), 1.0));
for i in 0..self.scheduler.heartbeats(Duration::from_millis(1)) {
self.particles.push(Particle::new_directed(
b.static_data.beam_duration,
time + i as f64 / 1000.0,
ParticleMode::HealingBeam,
pos.0 + particle_ori * 0.5 + b.offset,
pos.0 + particle_ori * b.static_data.range + b.offset,
));
}
} else {
let mut rng = thread_rng();
let (from, to) = (Vec3::<f32>::unit_z(), particle_ori);
let m = Mat3::<f32>::rotation_from_to_3d(from, to);
// Emit a light when using flames
lights.push(Light::new(
pos.0 + b.offset,
Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
2.0,
));
self.particles.resize_with(
self.particles.len()
+ 2 * usize::from(
self.scheduler.heartbeats(Duration::from_millis(1)),
),
|| {
let phi: f32 =
rng.gen_range(0.0..b.static_data.max_angle.to_radians());
let theta: f32 = rng.gen_range(0.0..2.0 * PI);
let offset_z = Vec3::new(
phi.sin() * theta.cos(),
phi.sin() * theta.sin(),
phi.cos(),
);
let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
Particle::new_directed(
b.static_data.beam_duration,
time,
ParticleMode::FlameThrower,
pos.0 + random_ori * 0.5 + b.offset,
pos.0 + random_ori * b.static_data.range + b.offset,
)
},
);
}
*/
}
} else {
let mut rng = thread_rng();
let (from, to) = (Vec3::<f32>::unit_z(), *ori.look_dir());
let m = Mat3::<f32>::rotation_from_to_3d(from, to);
// Emit a light when using flames
lights.push(Light::new(
pos.0,
Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)),
2.0,
));
self.particles.resize_with(
self.particles.len()
+ 2 * usize::from(self.scheduler.heartbeats(Duration::from_millis(1))),
|| {
let phi: f32 = rng.gen_range(0.0..beam.properties.angle);
let theta: f32 = rng.gen_range(0.0..2.0 * PI);
let offset_z =
Vec3::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos());
let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
Particle::new_directed(
self.particles.resize_with(
self.particles.len() + 2 * usize::from(beam_tick_count),
|| {
let phi: f32 = rng.gen_range(0.0..beam.properties.angle);
let theta: f32 = rng.gen_range(0.0..2.0 * PI);
let offset_z = Vec3::new(
phi.sin() * theta.cos(),
phi.sin() * theta.sin(),
phi.cos(),
);
let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0);
Particle::new_directed(
beam.properties.duration,
time,
ParticleMode::FlameThrower,
pos.0,
pos.0 + random_ori * range,
)
},
);
},
beam::FrontendSpecifier::HealingBeam => {
// Emit a light when using healing
lights.push(Light::new(pos.0, Rgb::new(0.1, 1.0, 0.15), 1.0));
for i in 0..beam_tick_count {
self.particles.push(Particle::new_directed(
beam.properties.duration,
time,
ParticleMode::FlameThrower,
pos.0, /* + random_ori */
pos.0 + random_ori * range,
)
time + i as f64 / 1000.0,
ParticleMode::HealingBeam,
pos.0,
pos.0 + *ori.look_dir() * range,
));
}
},
beam::FrontendSpecifier::LifestealBeam => {
// Emit a light when using lifesteal beam
lights.push(Light::new(pos.0, Rgb::new(0.8, 1.0, 0.5), 1.0));
for i in 0..beam_tick_count {
self.particles.push(Particle::new_directed(
beam.properties.duration,
time + i as f64 / 1000.0,
ParticleMode::LifestealBeam,
pos.0,
pos.0 + *ori.look_dir() * range,
));
}
},
}
}
}
fn maintain_aura_particles(&mut self, scene_data: &SceneData) {
let state = scene_data.state;
let ecs = state.ecs();
let time = state.get_time();
for (pos, auras) in (
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<comp::Auras>(),
)
.join()
{
for (_, aura) in auras.auras.iter() {
#[allow(clippy::single_match)]
match aura.aura_kind {
aura::AuraKind::Buff {
kind: buff::BuffKind::ProtectingWard,
..
} => {
let mut rng = thread_rng();
let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
self.particles.resize_with(
self.particles.len()
+ aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
|| {
let rand_dist = aura.radius * (1.0 - rng.gen::<f32>().powi(100));
let init_pos = Vec3::new(rand_dist, 0_f32, 0_f32);
let max_dur = Duration::from_secs(1);
Particle::new_directed(
aura.duration.map_or(max_dur, |dur| dur.min(max_dur)),
time,
ParticleMode::EnergyNature,
pos.0,
pos.0 + init_pos,
)
},
);
},
);
_ => {},
}
}
}
}