Frost gigas tweaks

This commit is contained in:
maxicarlos08 2023-10-08 11:35:01 +00:00 committed by flo
parent c1e7f9af50
commit 8a5f237e9c
80 changed files with 762 additions and 256 deletions

View File

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Mutliple singleplayer worlds and map generation UI.
- New arena building in desert cities, suitable for PVP, also NPCs like to watch the fights too
- The loading screen now displays status updates for singleplayer server and client initialization progress
- New Frost Gigas attacks & AI
### Changed
@ -54,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated windowing library, wayland may work better.
- Portal model has been updated by @Nectical
- Chat command responses sent by the server can now be localized
- Frost Gigas spawns in cold areas (but isn't forced to stay there)
- The ability limit for non-humanoids has been removed
### Removed
- Medium and large potions from all loot tables

View File

@ -783,6 +783,9 @@
Simple(None, "common.abilities.custom.gigas_frost.ice_volley"),
Simple(None, "common.abilities.custom.gigas_frost.frost_summons"),
Simple(None, "common.abilities.custom.gigas_frost.flashfreeze"),
Simple(None, "common.abilities.custom.gigas_frost.icespike_targeted"),
Simple(None, "common.abilities.custom.gigas_frost.bonk"),
Simple(None, "common.abilities.custom.gigas_frost.whirlwind"),
],
),
Custom("Boreal Bow"): (

View File

@ -11,7 +11,7 @@ LeapShockwave(
shockwave_vertical_angle: 15.0,
shockwave_speed: 20.0,
shockwave_duration: 0.8,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.2,
damage_kind: Crushing,
specifier: Steam,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 20.0,
shockwave_duration: 0.5,
requires_ground: false,
dodgeable: Roll,
move_efficiency: 0.1,
damage_kind: Energy,
specifier: Fire,

View File

@ -17,7 +17,7 @@ ChargedRanged(
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.1),
strength: Value(0.3),
chance: 1.0,
))),
move_speed: 0.6,

View File

@ -16,7 +16,7 @@ RepeaterRanged(
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.1),
strength: Value(0.3),
chance: 1.0,
))),
)

View File

@ -15,7 +15,7 @@ BasicRanged(
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.1),
strength: Value(0.3),
chance: 1.0,
))),
move_efficiency: 0.3,

View File

@ -18,8 +18,8 @@ DashMelee(
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.1),
chance: 1.0,
strength: Value(0.3),
chance: 0.5,
))),
),
energy_drain: 0,

View File

@ -17,7 +17,7 @@ LeapMelee(
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.1),
strength: Value(0.3),
chance: 1.0,
))),
),

View File

@ -17,8 +17,8 @@ ComboMelee(
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.1),
chance: 1.0,
strength: Value(0.3),
chance: 0.4,
))),
)],
initial_energy_gain: 5.0,

View File

@ -10,6 +10,7 @@ BasicSummon(
body_type: Male,
)),
scale: None,
use_npc_name: true,
has_health: true,
loadout_config: None,
skillset_config: Some(Rank3),

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 3.5,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Ground,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Lightning,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 25.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Water,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 360.0,
shockwave_speed: 40.0,
shockwave_duration: 0.4,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Piercing,
specifier: Ground,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Steam,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 3.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Fire,

View File

@ -10,6 +10,7 @@ BasicSummon(
body_type: Male,
)),
scale: None,
use_npc_name: true,
has_health: true,
loadout_config: Some(ClockworkSummon),
skillset_config: None,

View File

@ -8,6 +8,7 @@ BasicSummon(
body: Object(Flamethrower),
scale: None,
has_health: true,
use_npc_name: true,
loadout_config: None,
skillset_config: None,
),

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 3.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Water,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Fire,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Ice,

View File

@ -0,0 +1,24 @@
BasicMelee(
energy_cost: 0,
buildup_duration: 0.7,
swing_duration: 0.15,
recover_duration: 0.6,
melee_constructor: (
kind: Bash(
damage: 30,
poise: 100,
knockback: 0,
energy_regen: 0,
),
range: 8.0,
angle: 100.0,
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 5,
strength: Value(0.7),
chance: 1.0,
))),
multi_target: Some(Normal),
),
ori_modifier: 0.8,
)

View File

@ -1,21 +1,21 @@
BasicMelee(
energy_cost: 0,
buildup_duration: 0.9,
buildup_duration: 0.5,
swing_duration: 0.1,
recover_duration: 0.7,
melee_constructor: (
kind: Slash(
damage: 85.0,
damage: 60.0,
poise: 5.0,
knockback: 5.0,
energy_regen: 10.0,
),
range: 5.0,
range: 7.0,
angle: 75.0,
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 1.0,
strength: DamageFraction(0.1),
strength: Value(0.5),
chance: 0.3,
))),
multi_target: Some(Normal),

View File

@ -1,24 +1,24 @@
Shockwave(
energy_cost: 0,
buildup_duration: 2.0,
buildup_duration: 1.8,
swing_duration: 0.12,
recover_duration: 1.5,
damage: 45.0,
damage: 50.0,
poise_damage: 30,
knockback: (strength: 0.0, direction: TowardsUp),
shockwave_angle: 240.0,
shockwave_angle: 220.0,
shockwave_vertical_angle: 360.0,
shockwave_speed: 200.0,
shockwave_duration: 0.15,
requires_ground: false,
move_efficiency: 0.0,
dodgeable: No,
move_efficiency: 0.2,
damage_kind: Piercing,
specifier: IceSpikes,
ori_rate: 0.0,
ori_rate: 0.1,
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 2.0,
strength: DamageFraction(0.3),
strength: Value(3.0),
chance: 1.0,
))),
)

View File

@ -9,6 +9,7 @@ BasicSummon(
species: Boreal,
body_type: Male,
)),
use_npc_name: true,
scale: None,
has_health: true,
loadout_config: Some(BorealSummon),

View File

@ -9,8 +9,8 @@ BasicRanged(
min_falloff: 0.1,
),
projectile_body: Object(IceBomb),
projectile_speed: 25.0,
projectile_speed: 40.0,
num_projectiles: 5,
projectile_spread: 0.07,
projectile_spread: 0.05,
move_efficiency: 0.3,
)

View File

@ -3,8 +3,8 @@ SpriteSummon(
cast_duration: 0.1,
recover_duration: 1.1,
sprite: IceSpike,
del_timeout: Some((2, 5)),
summon_distance: (2, 12),
sparseness: 0.95,
del_timeout: Some((5, 15)),
summon_distance: (2, 18),
sparseness: 0.96,
angle: 360,
)

View File

@ -0,0 +1,12 @@
SpriteSummon(
buildup_duration: 0.4,
cast_duration: 0.1,
recover_duration: 1.1,
sprite: IceSpike,
del_timeout: Some((5, 15)),
summon_distance: (0, 5),
sparseness: 0.7,
angle: 360,
move_efficiency: 1.0,
anchor: Target,
)

View File

@ -9,16 +9,16 @@ LeapShockwave(
knockback: (strength: 3.0, direction: Up),
shockwave_angle: 360.0,
shockwave_vertical_angle: 15.0,
shockwave_speed: 20.0,
shockwave_duration: 0.8,
requires_ground: true,
shockwave_speed: 30.0,
shockwave_duration: 1.2,
dodgeable: Jump,
move_efficiency: 0.2,
damage_kind: Piercing,
specifier: IceSpikes,
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 1.0,
strength: DamageFraction(0.1),
strength: Value(1.2),
chance: 1.0,
))),
forward_leap_strength: 45.0,

View File

@ -0,0 +1,28 @@
SpinMelee(
buildup_duration: 1.1,
swing_duration: 0.4,
recover_duration: 0.6,
melee_constructor: (
kind: Bash(
damage: 45.0,
poise: 30.0,
knockback: 55.0,
energy_regen: 0.0,
),
range: 20.5,
angle: 360.0,
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 5.0,
strength: Value(0.3),
chance: 1.0,
))),
multi_target: Some(Normal),
),
energy_cost: 0,
is_infinite: false,
movement_behavior: Stationary,
forward_speed: 0.0,
num_spins: 3,
specifier: Some(Whirlwind),
)

View File

@ -5,17 +5,17 @@ BasicMelee(
recover_duration: 0.8,
melee_constructor: (
kind: Slash(
damage: 90.0,
damage: 70.0,
poise: 20.0,
knockback: 5.0,
energy_regen: 5.0,
),
range: 5.0,
range: 7.0,
angle: 120.0,
damage_effect: Some(Buff((
kind: Frozen,
dur_secs: 1.0,
strength: DamageFraction(0.1),
strength: Value(0.5),
chance: 0.5,
))),
multi_target: Some(Normal),

View File

@ -9,6 +9,7 @@ BasicSummon(
species: Husk,
body_type: Male,
)),
use_npc_name: true,
scale: None,
has_health: true,
loadout_config: Some(HuskSummon),

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 15.0,
shockwave_speed: 20.0,
shockwave_duration: 0.8,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.2,
damage_kind: Piercing,
specifier: IceSpikes,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Ink,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 65.0,
shockwave_duration: 1.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.05,
damage_kind: Crushing,
specifier: Ground,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 30.0,
shockwave_speed: 10.0,
shockwave_duration: 5.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Water,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 20.0,
shockwave_duration: 0.5,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.1,
damage_kind: Crushing,
specifier: Ground,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Poison,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 15.0,
shockwave_duration: 2.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.0,
damage_kind: Crushing,
specifier: Ground,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 15.0,
shockwave_speed: 15.0,
shockwave_duration: 3.0,
requires_ground: true,
dodgeable: Jump,
move_efficiency: 0.2,
damage_kind: Piercing,
specifier: IceSpikes,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90,
shockwave_speed: 10,
shockwave_duration: 1,
requires_ground: false,
dodgeable: Roll,
move_efficiency: 0,
damage_kind: Energy,
specifier: Fire,

View File

@ -10,7 +10,7 @@ Shockwave(
shockwave_vertical_angle: 90.0,
shockwave_speed: 30.0,
shockwave_duration: 0.5,
requires_ground: false,
dodgeable: Roll,
move_efficiency: 0.1,
damage_kind: Energy,
specifier: Fire,

View File

@ -1159,11 +1159,11 @@
),
husk: (
keyword: "husk",
generic: "Husk"
generic: "Cultist Husk"
),
boreal: (
keyword: "boreal",
generic: "Boreal",
generic: "Boreal Warrior",
),
bushly: (
keyword: "bushly",

View File

@ -83,6 +83,7 @@ const int GIGA_SNOW = 42;
const int CYCLOPS_CHARGE = 43;
const int PORTAL_FIZZ = 45;
const int INK = 46;
const int WHIRLWIND = 47;
// meters per second squared (acceleration)
const float earth_gravity = 9.807;
@ -703,6 +704,15 @@ void main() {
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
);
break;
case WHIRLWIND:
f_reflect = 0.0;
attr = Attr(
spiral_motion(vec3(0, 0, 3), abs(rand0) * 3 + percent() * 20.5, percent(), -8.0 + (rand0 * 3), rand1 * 360.),
vec3((-2.5 * (1 - slow_start(0.05)))),
vec4(vec3(1.3, 1.8, 2), 1),
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
);
break;
default:
attr = Attr(
linear_motion(

View File

@ -40,6 +40,7 @@ pub enum AttackSource {
Beam,
GroundShockwave,
AirShockwave,
UndodgeableShockwave,
Explosion,
}
@ -948,7 +949,9 @@ impl From<AttackSource> for DamageSource {
AttackSource::Melee => DamageSource::Melee,
AttackSource::Projectile => DamageSource::Projectile,
AttackSource::Explosion => DamageSource::Explosion,
AttackSource::AirShockwave | AttackSource::GroundShockwave => DamageSource::Shockwave,
AttackSource::AirShockwave
| AttackSource::GroundShockwave
| AttackSource::UndodgeableShockwave => DamageSource::Shockwave,
AttackSource::Beam => DamageSource::Energy,
}
}

View File

@ -25,6 +25,7 @@ use crate::{
resources::Secs,
states::{
behavior::JoinData,
sprite_summon::SpriteSummonAnchor,
utils::{AbilityInfo, ComboConsumption, ScalingKind, StageSection},
*,
},
@ -33,9 +34,11 @@ use crate::{
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use std::{convert::TryFrom, time::Duration};
use std::{borrow::Cow, convert::TryFrom, time::Duration};
pub const MAX_ABILITIES: usize = 5;
use super::shockwave::ShockwaveDodgeable;
pub const BASE_ABILITY_LIMIT: usize = 5;
pub type AuxiliaryKey = (Option<ToolKind>, Option<ToolKind>);
// TODO: Potentially look into storing previous ability sets for weapon
@ -48,7 +51,8 @@ pub struct ActiveAbilities {
pub primary: PrimaryAbility,
pub secondary: SecondaryAbility,
pub movement: MovementAbility,
pub auxiliary_sets: HashMap<AuxiliaryKey, [AuxiliaryAbility; MAX_ABILITIES]>,
pub limit: Option<usize>,
pub auxiliary_sets: HashMap<AuxiliaryKey, Vec<AuxiliaryAbility>>,
}
impl Component for ActiveAbilities {
@ -62,19 +66,35 @@ impl Default for ActiveAbilities {
primary: PrimaryAbility::Tool,
secondary: SecondaryAbility::Tool,
movement: MovementAbility::Species,
limit: None,
auxiliary_sets: HashMap::new(),
}
}
}
impl ActiveAbilities {
pub fn new(auxiliary_sets: HashMap<AuxiliaryKey, [AuxiliaryAbility; MAX_ABILITIES]>) -> Self {
pub fn from_auxiliary(
auxiliary_sets: HashMap<AuxiliaryKey, Vec<AuxiliaryAbility>>,
limit: Option<usize>,
) -> Self {
// Discard any sets that exceed the limit
ActiveAbilities {
auxiliary_sets,
auxiliary_sets: auxiliary_sets
.into_iter()
.filter(|(_, set)| limit.map_or(true, |limit| set.len() == limit))
.collect(),
limit,
..Self::default()
}
}
pub fn default_limited(limit: usize) -> Self {
ActiveAbilities {
limit: Some(limit),
..Default::default()
}
}
pub fn change_ability(
&mut self,
slot: usize,
@ -86,7 +106,7 @@ impl ActiveAbilities {
let auxiliary_set = self
.auxiliary_sets
.entry(auxiliary_key)
.or_insert(Self::default_ability_set(inventory, skill_set));
.or_insert(Self::default_ability_set(inventory, skill_set, self.limit));
if let Some(ability) = auxiliary_set.get_mut(slot) {
*ability = new_ability;
}
@ -111,13 +131,13 @@ impl ActiveAbilities {
&self,
inv: Option<&Inventory>,
skill_set: Option<&SkillSet>,
) -> [AuxiliaryAbility; MAX_ABILITIES] {
) -> Cow<Vec<AuxiliaryAbility>> {
let aux_key = Self::active_auxiliary_key(inv);
self.auxiliary_sets
.get(&aux_key)
.copied()
.unwrap_or_else(|| Self::default_ability_set(inv, skill_set))
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(Self::default_ability_set(inv, skill_set, self.limit)))
}
pub fn get_ability(
@ -311,7 +331,8 @@ impl ActiveAbilities {
fn default_ability_set<'a>(
inv: Option<&'a Inventory>,
skill_set: Option<&'a SkillSet>,
) -> [AuxiliaryAbility; MAX_ABILITIES] {
limit: Option<usize>,
) -> Vec<AuxiliaryAbility> {
let mut iter = Self::iter_available_abilities(inv, skill_set, EquipSlot::ActiveMainhand)
.map(AuxiliaryAbility::MainWeapon)
.chain(
@ -319,7 +340,13 @@ impl ActiveAbilities {
.map(AuxiliaryAbility::OffWeapon),
);
[(); MAX_ABILITIES].map(|()| iter.next().unwrap_or(AuxiliaryAbility::Empty))
if let Some(limit) = limit {
(0..limit)
.map(|_| iter.next().unwrap_or(AuxiliaryAbility::Empty))
.collect()
} else {
iter.collect()
}
}
}
@ -790,7 +817,7 @@ pub enum CharacterAbility {
shockwave_vertical_angle: f32,
shockwave_speed: f32,
shockwave_duration: f32,
requires_ground: bool,
dodgeable: ShockwaveDodgeable,
move_efficiency: f32,
damage_kind: DamageKind,
specifier: comp::shockwave::FrontendSpecifier,
@ -863,7 +890,7 @@ pub enum CharacterAbility {
shockwave_vertical_angle: f32,
shockwave_speed: f32,
shockwave_duration: f32,
requires_ground: bool,
dodgeable: ShockwaveDodgeable,
move_efficiency: f32,
damage_kind: DamageKind,
specifier: comp::shockwave::FrontendSpecifier,
@ -944,6 +971,10 @@ pub enum CharacterAbility {
sparseness: f64,
angle: f32,
#[serde(default)]
anchor: SpriteSummonAnchor,
#[serde(default)]
move_efficiency: f32,
#[serde(default)]
meta: AbilityMeta,
},
Music {
@ -1348,7 +1379,7 @@ impl CharacterAbility {
shockwave_vertical_angle: _,
shockwave_speed: _,
ref mut shockwave_duration,
requires_ground: _,
dodgeable: _,
move_efficiency: _,
damage_kind: _,
specifier: _,
@ -1467,7 +1498,7 @@ impl CharacterAbility {
shockwave_vertical_angle: _,
shockwave_speed: _,
ref mut shockwave_duration,
requires_ground: _,
dodgeable: _,
move_efficiency: _,
damage_kind: _,
specifier: _,
@ -1592,6 +1623,8 @@ impl CharacterAbility {
summon_distance: (ref mut inner_dist, ref mut outer_dist),
sparseness: _,
angle: _,
anchor: _,
move_efficiency: _,
meta: _,
} => {
// TODO: Figure out how/if power should affect this
@ -2462,7 +2495,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
shockwave_vertical_angle,
shockwave_speed,
shockwave_duration,
requires_ground,
dodgeable,
move_efficiency,
damage_kind,
specifier,
@ -2483,7 +2516,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
shockwave_vertical_angle: *shockwave_vertical_angle,
shockwave_speed: *shockwave_speed,
shockwave_duration: Duration::from_secs_f32(*shockwave_duration),
requires_ground: *requires_ground,
dodgeable: *dodgeable,
move_efficiency: *move_efficiency,
damage_kind: *damage_kind,
specifier: *specifier,
@ -2654,7 +2687,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
shockwave_vertical_angle,
shockwave_speed,
shockwave_duration,
requires_ground,
dodgeable,
move_efficiency,
damage_kind,
specifier,
@ -2673,7 +2706,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
shockwave_vertical_angle: *shockwave_vertical_angle,
shockwave_speed: *shockwave_speed,
shockwave_duration: Duration::from_secs_f32(*shockwave_duration),
requires_ground: *requires_ground,
dodgeable: *dodgeable,
move_efficiency: *move_efficiency,
damage_effect: *damage_effect,
ability_info,
@ -2821,6 +2854,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
summon_distance,
sparseness,
angle,
anchor,
move_efficiency,
meta: _,
} => CharacterState::SpriteSummon(sprite_summon::Data {
static_data: sprite_summon::StaticData {
@ -2832,6 +2867,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
summon_distance: *summon_distance,
sparseness: *sparseness,
angle: *angle,
anchor: *anchor,
move_efficiency: *move_efficiency,
ability_info,
},
timer: Duration::default(),

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
arthropod, biped_small, bird_medium, humanoid, quadruped_low, quadruped_medium,
quadruped_small, ship, Body, UtteranceKind,
arthropod, biped_large, biped_small, bird_medium, humanoid, quadruped_low,
quadruped_medium, quadruped_small, ship, Body, UtteranceKind,
},
path::Chaser,
rtsim::{NpcInput, RtSimController},
@ -383,6 +383,10 @@ impl<'a> From<&'a Body> for Psyche {
},
sight_dist: match body {
Body::BirdLarge(_) => 250.0,
Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Gigasfrost => 200.0,
_ => 100.0,
},
_ => 40.0,
},
listen_dist: 30.0,

View File

@ -869,7 +869,7 @@ impl Body {
biped_large::Species::Huskbrute => 800,
biped_large::Species::Cultistwarlord => 250,
biped_large::Species::Cultistwarlock => 250,
biped_large::Species::Gigasfrost => 20000,
biped_large::Species::Gigasfrost => 30000,
biped_large::Species::AdletElder => 1500,
biped_large::Species::Tursus => 300,
biped_large::Species::SeaBishop => 550,
@ -1095,6 +1095,7 @@ impl Body {
Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Mindflayer => 320,
biped_large::Species::Minotaur => 280,
biped_large::Species::Gigasfrost => 800,
_ => 250,
},
Body::BipedSmall(b) => match b.species {

View File

@ -156,7 +156,7 @@ pub enum BuffKind {
/// Results from drinking a potion.
/// Decreases the health gained from subsequent potions.
PotionSickness,
// Changed into another body.
/// Changed into another body.
Polymorphed(Body),
}
@ -428,7 +428,10 @@ pub struct BuffData {
pub strength: f32,
pub duration: Option<Secs>,
pub delay: Option<Secs>,
// Used for buffs that have rider buffs (e.g. Flame, Frigid)
/// Force the buff effects to be applied each tick, ignoring num_ticks
#[serde(default)]
pub force_immediate: bool,
/// Used for buffs that have rider buffs (e.g. Flame, Frigid)
pub secondary_duration: Option<Secs>,
}
@ -437,6 +440,7 @@ impl BuffData {
Self {
strength,
duration,
force_immediate: false,
delay: None,
secondary_duration: None,
}
@ -451,6 +455,12 @@ impl BuffData {
self.secondary_duration = Some(sec_dur);
self
}
/// Force the buff effects to be applied each tick, ignoring num_ticks
pub fn with_force_immediate(mut self, force_immediate: bool) -> Self {
self.force_immediate = force_immediate;
self
}
}
/// De/buff category ID.

View File

@ -920,16 +920,10 @@ impl CharacterState {
AttackSource::Projectile
})
},
CharacterState::Shockwave(data) => Some(if data.static_data.requires_ground {
AttackSource::GroundShockwave
} else {
AttackSource::AirShockwave
}),
CharacterState::LeapShockwave(data) => Some(if data.static_data.requires_ground {
AttackSource::GroundShockwave
} else {
AttackSource::AirShockwave
}),
CharacterState::Shockwave(data) => Some(data.static_data.dodgeable.to_attack_source()),
CharacterState::LeapShockwave(data) => {
Some(data.static_data.dodgeable.to_attack_source())
},
CharacterState::BasicBeam(_) => Some(AttackSource::Beam),
CharacterState::BasicAura(_) => None,
CharacterState::Blink(_) => None,
@ -974,6 +968,7 @@ impl AttackFilters {
AttackSource::Beam => self.beams,
AttackSource::GroundShockwave => self.ground_shockwaves,
AttackSource::AirShockwave => self.air_shockwaves,
AttackSource::UndodgeableShockwave => false,
AttackSource::Explosion => self.explosions,
}
}

View File

@ -41,7 +41,7 @@ pub mod visual;
pub use self::{
ability::{
Ability, AbilityInput, ActiveAbilities, CharacterAbility, CharacterAbilityType, Stance,
MAX_ABILITIES,
BASE_ABILITY_LIMIT,
},
admin::{Admin, AdminRole},
agent::{

View File

@ -10,6 +10,7 @@ use crate::{
uid::Uid,
Explosion, RadiusEffect,
};
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use specs::Component;
use std::time::Duration;
@ -761,10 +762,18 @@ impl ProjectileConstructor {
.with_crit(crit_chance, crit_mult)
.with_effect(knockback)
.with_effect(buff);
let variation = thread_rng().gen::<f32>();
let explosion = Explosion {
effects: vec![
RadiusEffect::Attack(attack),
RadiusEffect::TerrainDestruction(30.0, Rgb::new(0.0, 191.0, 255.0)),
RadiusEffect::TerrainDestruction(
30.0,
Rgb::new(
83.0 - (20.0 * variation),
212.0 - (52.0 * variation),
255.0 - (62.0 * variation),
),
),
],
radius,
reagent: Some(Reagent::White),

View File

@ -1,15 +1,25 @@
use crate::{combat::Attack, uid::Uid};
use crate::{
combat::{Attack, AttackSource},
uid::Uid,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use std::time::Duration;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ShockwaveDodgeable {
Roll,
Jump,
No,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Properties {
pub angle: f32,
pub vertical_angle: f32,
pub speed: f32,
pub attack: Attack,
pub requires_ground: bool,
pub dodgeable: ShockwaveDodgeable,
pub duration: Duration,
pub owner: Option<Uid>,
pub specifier: FrontendSpecifier,
@ -56,3 +66,13 @@ pub enum FrontendSpecifier {
Ink,
Lightning,
}
impl ShockwaveDodgeable {
pub fn to_attack_source(&self) -> AttackSource {
match self {
Self::Roll => AttackSource::AirShockwave,
Self::Jump => AttackSource::GroundShockwave,
Self::No => AttackSource::UndodgeableShockwave,
}
}
}

View File

@ -460,7 +460,7 @@ impl Chaser {
// has been determined, so we start sampling terrain.
// Check for falling off walls and try moving straight
// towards the target if falling is not a danger
let walking_towards_edge = (-3..2).all(|z| {
let walking_towards_edge = (-8..2).all(|z| {
vol.get(
(pos + Vec3::<f32>::from(tgt_dir) * 2.5).map(|e| e as i32)
+ Vec3::unit_z() * z,

View File

@ -8,6 +8,7 @@ pub struct Ray<'a, V: ReadVol, F: FnMut(&V::Vox) -> bool, G: RayForEach<V::Vox>>
from: Vec3<f32>,
to: Vec3<f32>,
until: F,
is_while: bool,
for_each: Option<G>,
max_iter: usize,
ignore_error: bool,
@ -25,6 +26,7 @@ where
from,
to,
until,
is_while: false,
for_each: None,
max_iter: 100,
ignore_error: false,
@ -37,6 +39,20 @@ where
from: self.from,
to: self.to,
until: f,
is_while: false,
for_each: self.for_each,
max_iter: self.max_iter,
ignore_error: self.ignore_error,
}
}
pub fn while_<H: FnMut(&V::Vox) -> bool>(self, f: H) -> Ray<'a, V, H, G> {
Ray {
vol: self.vol,
from: self.from,
to: self.to,
until: f,
is_while: true,
for_each: self.for_each,
max_iter: self.max_iter,
ignore_error: self.ignore_error,
@ -49,6 +65,7 @@ where
vol: self.vol,
from: self.from,
to: self.to,
is_while: self.is_while,
until: self.until,
max_iter: self.max_iter,
ignore_error: self.ignore_error,
@ -86,11 +103,21 @@ where
let vox = self.vol.get(ipos);
// for_each
if let Some(g) = &mut self.for_each {
if let Ok(vox) = vox {
if self.is_while {
let vox = match vox.map(|vox| (vox, (self.until)(vox))) {
Ok((vox, true)) => return (dist, Ok(Some(vox))),
Ok((vox, _)) => Some(vox),
Err(err) if !self.ignore_error => return (dist, Err(err)),
_ => None,
};
if let Some((vox, g)) = vox.zip(self.for_each.as_mut()) {
g(vox, ipos);
}
} else {
// for_each
if let Some((vox, g)) = vox.as_ref().ok().zip(self.for_each.as_mut()) {
g(vox, ipos);
}
match vox.map(|vox| (vox, (self.until)(vox))) {
@ -98,6 +125,7 @@ where
Err(err) if !self.ignore_error => return (dist, Err(err)),
_ => {},
}
}
let deltas =
(dir.map(|e| if e < 0.0 { 0.0 } else { 1.0 }) - pos.map(|e| e.abs().fract())) / dir;

View File

@ -7,6 +7,7 @@ use crate::{
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
},
event::{LocalEvent, NpcBuilder, ServerEvent},
npc::NPC_NAMES,
outcome::Outcome,
skillset_builder::{self, SkillSetBuilder},
states::{
@ -111,7 +112,20 @@ impl CharacterBehavior for Data {
}
};
let stats = comp::Stats::new("Summon".to_string(), body);
let stats = comp::Stats::new(
self.static_data
.summon_info
.use_npc_name
.then(|| {
let all_names = NPC_NAMES.read();
all_names
.get_species_meta(&self.static_data.summon_info.body)
.map(|meta| meta.generic.clone())
})
.flatten()
.unwrap_or_else(|| "Summon".to_string()),
body,
);
let health = self.static_data.summon_info.has_health.then(|| {
let health_level = skill_set
@ -248,6 +262,8 @@ pub struct SummonInfo {
body: comp::Body,
scale: Option<comp::Scale>,
has_health: bool,
#[serde(default)]
use_npc_name: bool,
// TODO: use assets for specifying skills and loadout?
loadout_config: Option<loadout_builder::Preset>,
skillset_config: Option<skillset_builder::Preset>,

View File

@ -3,13 +3,19 @@ use crate::{
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
DamageSource, GroupTarget, Knockback,
},
comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate},
comp::{
character_state::OutputEvents,
item::Reagent,
shockwave::{self, ShockwaveDodgeable},
CharacterState, StateUpdate,
},
event::{LocalEvent, ServerEvent},
outcome::Outcome,
states::{
behavior::{CharacterBehavior, JoinData},
utils::{StageSection, *},
},
Explosion, KnockbackDir, RadiusEffect,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -39,8 +45,8 @@ pub struct StaticData {
pub shockwave_speed: f32,
/// How long the shockwave travels for
pub shockwave_duration: Duration,
/// Whether the shockwave requires the target to be on the ground
pub requires_ground: bool,
/// If the shockwave can be dodged, and in what way
pub dodgeable: ShockwaveDodgeable,
/// Movement speed efficiency
pub move_efficiency: f32,
/// Adds an effect onto the main damage of the attack
@ -173,7 +179,7 @@ impl CharacterBehavior for Data {
speed: self.static_data.shockwave_speed,
duration: self.static_data.shockwave_duration,
attack,
requires_ground: self.static_data.requires_ground,
dodgeable: self.static_data.dodgeable,
owner: Some(*data.uid),
specifier: self.static_data.specifier,
};
@ -185,6 +191,35 @@ impl CharacterBehavior for Data {
// Send local event used for frontend shenanigans
match self.static_data.specifier {
shockwave::FrontendSpecifier::IceSpikes => {
let damage = AttackDamage::new(
Damage {
source: DamageSource::Explosion,
kind: self.static_data.damage_kind,
value: self.static_data.damage / 2.,
},
Some(GroupTarget::OutOfGroup),
rand::random(),
);
let attack = Attack::default().with_damage(damage).with_effect(
AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
direction: KnockbackDir::Away,
strength: 10.,
}),
),
);
let explosion = Explosion {
effects: vec![RadiusEffect::Attack(attack)],
radius: data.body.max_radius() * 3.0,
reagent: Some(Reagent::White),
min_falloff: 0.5,
};
output_events.emit_server(ServerEvent::Explosion {
pos: data.pos.0,
explosion,
owner: Some(*data.uid),
});
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::IceSpikes {
pos: data.pos.0

View File

@ -3,7 +3,11 @@ use crate::{
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
DamageSource, GroupTarget, Knockback,
},
comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate},
comp::{
character_state::OutputEvents,
shockwave::{self, ShockwaveDodgeable},
CharacterState, StateUpdate,
},
event::{LocalEvent, ServerEvent},
outcome::Outcome,
states::{
@ -37,8 +41,8 @@ pub struct StaticData {
pub shockwave_speed: f32,
/// How long the shockwave travels for
pub shockwave_duration: Duration,
/// Whether the shockwave requires the target to be on the ground
pub requires_ground: bool,
/// If the shockwave can be dodged, and in what way
pub dodgeable: ShockwaveDodgeable,
/// Movement speed efficiency
pub move_efficiency: f32,
/// What key is used to press ability
@ -117,7 +121,7 @@ impl CharacterBehavior for Data {
speed: self.static_data.shockwave_speed,
duration: self.static_data.shockwave_duration,
attack,
requires_ground: self.static_data.requires_ground,
dodgeable: self.static_data.dodgeable,
owner: Some(*data.uid),
specifier: self.static_data.specifier,
};

View File

@ -171,4 +171,5 @@ pub enum MovementBehavior {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum FrontendSpecifier {
CultistVortex,
Whirlwind,
}

View File

@ -15,6 +15,13 @@ use serde::{Deserialize, Serialize};
use std::time::Duration;
use vek::*;
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
pub enum SpriteSummonAnchor {
#[default]
Summoner,
Target,
}
/// Separated out to condense update portions of character state
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticData {
@ -31,10 +38,14 @@ pub struct StaticData {
pub del_timeout: Option<(f32, f32)>,
/// Range that sprites are created relative to the summonner
pub summon_distance: (f32, f32),
/// Relative to what should the sprites be summoned?
pub anchor: SpriteSummonAnchor,
/// Chance that sprite is not created on a particular square
pub sparseness: f64,
/// Angle of total coverage, centered on the forward-facing orientation
pub angle: f32,
/// How much we can move
pub move_efficiency: f32,
/// Miscellaneous information about the ability
pub ability_info: AbilityInfo,
}
@ -56,6 +67,17 @@ impl CharacterBehavior for Data {
fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
let mut update = StateUpdate::from(data);
handle_orientation(data, &mut update, 1.0, None);
handle_move(data, &mut update, self.static_data.move_efficiency);
let target_pos = || {
data.controller
.queued_inputs
.get(&self.static_data.ability_info.input)
.or(self.static_data.ability_info.input_attr.as_ref())
.and_then(|input| input.select_pos)
};
match self.stage_section {
StageSection::Buildup => {
if self.timer < self.static_data.buildup_duration {
@ -102,15 +124,23 @@ impl CharacterBehavior for Data {
<= (self.static_data.angle / 2.0)
&& !thread_rng().gen_bool(self.static_data.sparseness)
{
let anchor_pos = match self.static_data.anchor {
SpriteSummonAnchor::Summoner => data.pos.0,
// Use the selected target position, falling back to the
// summoner position
SpriteSummonAnchor::Target => {
target_pos().unwrap_or(data.pos.0)
},
};
// The coordinates of where the sprite is created
let sprite_pos = Vec3::new(
data.pos.0.x.floor() as i32 + point.x,
data.pos.0.y.floor() as i32 + point.y,
data.pos.0.z.floor() as i32,
anchor_pos.x.floor() as i32 + point.x,
anchor_pos.y.floor() as i32 + point.y,
anchor_pos.z.floor() as i32,
);
// Check for collision in z up to 10 blocks up or down
let obstacle_z = data
let (obstacle_z, obstale_z_result) = data
.terrain
.ray(
sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 10.0,
@ -122,11 +152,17 @@ impl CharacterBehavior for Data {
Block::is_solid(b)
&& b.get_sprite() != Some(self.static_data.sprite)
})
.cast()
.0;
.cast();
// z height relative to caster
let z = sprite_pos.z + (10.5 - obstacle_z).ceil() as i32;
let z = sprite_pos.z
+ if let (SpriteSummonAnchor::Target, Ok(None)) =
(&self.static_data.anchor, obstale_z_result)
{
0
} else {
(10.5 - obstacle_z).ceil() as i32
};
// Location sprite will be created
let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z);
@ -154,8 +190,13 @@ impl CharacterBehavior for Data {
});
// Send local event used for frontend shenanigans
if self.static_data.sprite == SpriteKind::IceSpike {
let summoner_pos =
data.pos.0 + *data.ori.look_dir() * data.body.max_radius();
output_events.emit_local(LocalEvent::CreateOutcome(Outcome::IceCrack {
pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
pos: match self.static_data.anchor {
SpriteSummonAnchor::Summoner => summoner_pos,
SpriteSummonAnchor::Target => target_pos().unwrap_or(summoner_pos),
},
}));
}
} else {

View File

@ -396,6 +396,7 @@ impl Block {
BlockKind::WeakRock => Some(0.75),
BlockKind::Snow => Some(0.1),
BlockKind::Ice => Some(0.5),
BlockKind::Wood => Some(4.5),
BlockKind::Lava => None,
_ => self.get_sprite().and_then(|sprite| match sprite {
sprite if sprite.is_container() => None,

View File

@ -161,7 +161,7 @@ impl<'a> System<'a> for Sys {
entity,
buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding,
BuffData::new(1.0, Some(Secs(6.0))),
BuffData::new(1.0, Some(Secs(6.0))).with_force_immediate(true),
Vec::new(),
BuffSource::World,
*read_data.time,
@ -179,7 +179,7 @@ impl<'a> System<'a> for Sys {
entity,
buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding,
BuffData::new(5.0, Some(Secs(3.0))),
BuffData::new(5.0, Some(Secs(3.0))).with_force_immediate(true),
Vec::new(),
BuffSource::World,
*read_data.time,
@ -215,7 +215,7 @@ impl<'a> System<'a> for Sys {
entity,
buff_change: BuffChange::Add(Buff::new(
BuffKind::Bleeding,
BuffData::new(15.0, Some(Secs(0.1))),
BuffData::new(15.0, Some(Secs(0.1))).with_force_immediate(true),
Vec::new(),
BuffSource::World,
*read_data.time,
@ -228,7 +228,7 @@ impl<'a> System<'a> for Sys {
entity,
buff_change: BuffChange::Add(Buff::new(
BuffKind::Frozen,
BuffData::new(0.2, Some(Secs(1.0))),
BuffData::new(0.2, Some(Secs(3.0))),
Vec::new(),
BuffSource::World,
*read_data.time,
@ -361,13 +361,15 @@ impl<'a> System<'a> for Sys {
}
});
let damage_reduction = Damage::compute_damage_reduction(
let infinite_damage_reduction = (Damage::compute_damage_reduction(
None,
read_data.inventories.get(entity),
Some(&stat),
&read_data.msm,
);
if (damage_reduction - 1.0).abs() < f32::EPSILON {
) - 1.0)
.abs()
< f32::EPSILON;
if infinite_damage_reduction {
for (id, buff) in buff_comp.buffs.iter() {
if !buff.kind.is_buff() {
expired_buffs.push(*id);
@ -389,6 +391,10 @@ impl<'a> System<'a> for Sys {
buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
for (buff_kind, (buff_ids, kind_start_time)) in buff_kinds.into_iter() {
let mut active_buff_ids = Vec::new();
if infinite_damage_reduction && !buff_kind.is_buff() {
continue;
}
if buff_kind.stacks() {
// Process all the buffs of this kind
active_buff_ids = buff_ids;
@ -414,6 +420,7 @@ impl<'a> System<'a> for Sys {
execute_effect(
effect,
buff.kind,
&buff.data,
buff.start_time,
kind_start_time,
&read_data,
@ -471,6 +478,7 @@ impl<'a> System<'a> for Sys {
fn execute_effect(
effect: &BuffEffect,
buff_kind: BuffKind,
buff_data: &BuffData,
buff_start_time: Time,
buff_kind_start_time: Time,
read_data: &ReadData,
@ -508,7 +516,9 @@ fn execute_effect(
let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
let whole_ticks = curr_tick - prev_tick;
if buff_will_expire {
if buff_data.force_immediate {
Some((1.0 / tick_dur.0 * dt) as f32)
} else if buff_will_expire {
// If the buff is ending, include the fraction of progress towards the next
// tick.
let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;

View File

@ -1,7 +1,8 @@
use common::{
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
combat::{self, AttackOptions, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
shockwave::ShockwaveDodgeable,
Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori,
PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
},
@ -182,7 +183,10 @@ impl<'a> System<'a> for Sys {
arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b))
}
&& (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle
&& (!shockwave.requires_ground || physics_state_b.on_ground.is_some());
&& match shockwave.dodgeable {
ShockwaveDodgeable::Roll | ShockwaveDodgeable::No => true,
ShockwaveDodgeable::Jump => physics_state_b.on_ground.is_some()
};
if hit {
let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
@ -217,12 +221,10 @@ impl<'a> System<'a> for Sys {
.character_states
.get(target)
.and_then(|cs| cs.attack_immunities())
.map_or(false, |i| {
if shockwave.requires_ground {
i.ground_shockwaves
} else {
i.air_shockwaves
}
.map_or(false, |i| match shockwave.dodgeable {
ShockwaveDodgeable::Roll => i.air_shockwaves,
ShockwaveDodgeable::Jump => i.ground_shockwaves,
ShockwaveDodgeable::No => false,
});
// PvP check
let may_harm = combat::may_harm(
@ -244,11 +246,7 @@ impl<'a> System<'a> for Sys {
dir,
attack_options,
1.0,
if shockwave.requires_ground {
AttackSource::GroundShockwave
} else {
AttackSource::AirShockwave
},
shockwave.dodgeable.to_attack_source(),
*read_data.time,
|e| server_emitter.emit(e),
|o| outcomes_emitter.emit(o),

View File

@ -198,7 +198,7 @@ impl Sentiment {
// remembering the last interaction instead of constant updates.
if rng.gen_bool(
(1.0 / (self.positivity.unsigned_abs() as f32 * DECAY_TIME_FACTOR.powi(2) * dt))
as f64,
.max(1.0) as f64,
) {
self.positivity -= self.positivity.signum();
}

View File

@ -289,9 +289,19 @@ impl Data {
}
// Spawn one monster Gigasfrost into the world
// Try a few times to find a location that's not underwater
if let Some((wpos, chunk)) = (0..10)
if let Some((wpos, chunk)) = (0..100)
.map(|_| world.sim().get_size().map(|sz| rng.gen_range(0..sz as i32)))
.find_map(|pos| Some((pos, world.sim().get(pos).filter(|c| !c.is_underwater())?)))
.find_map(|pos| {
Some((
pos,
world
.sim()
.get(pos)
// This is currently a workaround to force Frost Gigas spawning in cold areas
// TODO: Once more Gigas are implemented remove this
.filter(|c| !c.is_underwater() && c.temp < CONFIG.snow_temp)?,
))
})
.map(|(pos, chunk)| {
let wpos2d = pos.cpos_to_wpos_center();
(

View File

@ -14,7 +14,7 @@ use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::{error, warn};
use vek::{Clamp, Vec2};
use world::site::SiteKind;
use world::{site::SiteKind, CONFIG};
pub struct SimulateNpcs;
@ -132,7 +132,14 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
.map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
} else {
let pos = (0..10)
let is_gigas = matches!(body, Body::BipedLarge(body) if body.species == comp::body::biped_large::Species::Gigasfrost);
let pos = (0..(if is_gigas {
/* More attempts for gigas */
100
} else {
10
}))
.map(|_| {
ctx.world
.sim()
@ -140,10 +147,9 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
.map(|sz| rng.gen_range(0..sz as i32))
})
.find(|pos| {
ctx.world
.sim()
.get(*pos)
.map_or(false, |c| !c.is_underwater())
ctx.world.sim().get(*pos).map_or(false, |c| {
!c.is_underwater() && (!is_gigas || c.temp < CONFIG.snow_temp)
})
})
.unwrap_or(ctx.world.sim().get_size().as_() / 2);
let wpos2d = pos.cpos_to_wpos_center();

View File

@ -14,7 +14,7 @@ use common::{
combat::perception_dist_multiplier_from_stealth,
comp::{
self,
ability::MAX_ABILITIES,
ability::BASE_ABILITY_LIMIT,
agent::{Sound, SoundKind, Target},
inventory::slot::EquipSlot,
item::{
@ -1097,7 +1097,7 @@ impl<'a> AgentData<'a> {
"Frostfang" => Tactic::RandomAbilities {
primary: 1,
secondary: 3,
abilities: [0; MAX_ABILITIES],
abilities: [0; BASE_ABILITY_LIMIT],
},
"Tursus Claws" => Tactic::RandomAbilities {
primary: 2,
@ -1551,9 +1551,14 @@ impl<'a> AgentData<'a> {
read_data,
rng,
),
Tactic::FrostGigas => {
self.handle_frostgigas_attack(agent, controller, &attack_data, tgt_data, read_data)
},
Tactic::FrostGigas => self.handle_frostgigas_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
rng,
),
Tactic::BorealHammer => self.handle_boreal_hammer_attack(
agent,
controller,

View File

@ -5,15 +5,19 @@ use crate::{
};
use common::{
comp::{
ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, MAX_ABILITIES},
ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, BASE_ABILITY_LIMIT},
buff::BuffKind,
item::tool::AbilityContext,
skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent,
Controller, InputKind,
Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction,
ControlEvent, Controller, Fluid, InputKind,
},
path::TraversalConfig,
states::{self_buff, sprite_summon, utils::StageSection},
states::{
self_buff,
sprite_summon::{self, SpriteSummonAnchor},
utils::StageSection,
},
terrain::Block,
util::Dir,
vol::ReadVol,
@ -4190,43 +4194,61 @@ impl<'a> AgentData<'a> {
attack_data: &AttackData,
tgt_data: &TargetData,
read_data: &ReadData,
rng: &mut impl Rng,
) {
const GIGAS_MELEE_RANGE: f32 = 6.0;
const GIGAS_SPIKE_RANGE: f32 = 12.0;
const GIGAS_FREEZE_RANGE: f32 = 20.0;
const GIGAS_LEAP_RANGE: f32 = 30.0;
const MINION_SUMMON_THRESHOLD: f32 = 0.2;
const GIGAS_MELEE_RANGE: f32 = 12.0;
const GIGAS_SPIKE_RANGE: f32 = 16.0;
const ICEBOMB_RANGE: f32 = 70.0;
const GIGAS_LEAP_RANGE: f32 = 50.0;
const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
const FLASHFREEZE_RANGE: f32 = 30.;
#[allow(clippy::enum_variant_names)]
enum ActionStateTimers {
AttackChange,
Bonk,
}
enum ActionStateFCounters {
MinionSummonThreshold = 0,
}
enum ActionStateTimers {
AttackChange = 0,
}
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 2.5 {
agent.action_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
FCounterMinionSummonThreshold = 0,
}
let line_of_sight_with_target = || {
entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
enum ActionStateICounters {
/// An ability that is forced to fully complete until moving on to
/// other attacks.
/// 1 = Leap shockwave, 2 = Flashfreeze, 3 = Spike summon,
/// 4 = Whirlwind, 5 = Remote ice spikes, 6 = Ice bombs
CurrentAbility = 0,
}
let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
let remote_spikes_action = || ControlAction::StartInput {
input: InputKind::Ability(5),
target_entity: None,
select_pos: Some(tgt_data.pos.0),
};
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
// Sets counter at start of combat, using `condition` to keep track of whether
// it was already initialized
if !agent.action_state.initialized {
agent.action_state.counters[ActionStateFCounters::MinionSummonThreshold as usize] =
agent.action_state.counters
[ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
1.0 - MINION_SUMMON_THRESHOLD;
agent.action_state.initialized = true;
} else if health_fraction
< agent.action_state.counters[ActionStateFCounters::MinionSummonThreshold as usize]
}
// Update timers
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
agent.action_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
} else {
agent.action_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
}
agent.action_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
if health_fraction
< agent.action_state.counters
[ActionStateFCounters::FCounterMinionSummonThreshold as usize]
{
// Summon minions at particular thresholds of health
controller.push_basic_input(InputKind::Ability(3));
@ -4234,57 +4256,131 @@ impl<'a> AgentData<'a> {
if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
{
agent.action_state.counters
[ActionStateFCounters::MinionSummonThreshold as usize] -=
[ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
MINION_SUMMON_THRESHOLD;
}
// Continue casting any attacks that are forced to complete
} else if let Some(ability) = Some(
&mut agent.action_state.int_counters[ActionStateICounters::CurrentAbility as usize],
)
.filter(|i| **i != 0)
{
if *ability == 3 && should_use_targeted_spikes() {
*ability = 5
};
let reset = match ability {
// Must be rolled
1 => {
controller.push_basic_input(InputKind::Ability(1));
matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
},
// Attacker will have to run away here
2 => {
controller.push_basic_input(InputKind::Ability(4));
matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
},
// Avoid the spikes!
3 => {
controller.push_basic_input(InputKind::Ability(0));
matches!(self.char_state, CharacterState::SpriteSummon(c)
if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
},
// Long whirlwind attack
4 => {
controller.push_basic_input(InputKind::Ability(7));
matches!(self.char_state, CharacterState::SpinMelee(c) if matches!(c.stage_section, StageSection::Recover))
},
// Remote ice spikes
5 => {
controller.push_action(remote_spikes_action());
matches!(self.char_state, CharacterState::SpriteSummon(c)
if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
},
// Ice bombs
6 => {
controller.push_basic_input(InputKind::Ability(2));
matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
},
// Should never happen
_ => true,
};
if reset {
*ability = 0;
}
// If our target is nearby and above us, potentially cheesing, have a
// chance of summoning remote ice spikes or throwing ice bombs.
// Cheesing from less than 5 blocks away is usually not possible
} else if attack_data.dist_sqrd > 5f32.powi(2)
// Calculate the "cheesing factor" (height of the normalized position difference)
&& (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
// Make it happen at about every 10 seconds!
&& rng.gen_bool((0.2 * read_data.dt.0).min(1.0) as f64)
{
agent.action_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
rng.gen_range(5..=6);
} else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
// Bonk the target every 10-8 s
if agent.action_state.timers[ActionStateTimers::Bonk as usize] > 10. {
controller.push_basic_input(InputKind::Ability(6));
if matches!(self.char_state, CharacterState::BasicMelee(c)
if matches!(c.stage_section, StageSection::Recover) &&
c.static_data.ability_info.ability.map_or(false,
|meta| matches!(meta.ability, Ability::MainWeaponAux(6))
)
) {
agent.action_state.timers[ActionStateTimers::Bonk as usize] =
rng.gen_range(0.0..3.0);
}
// Have a small chance at starting a mixup attack
} else if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
&& rng.gen_bool(0.1 * read_data.dt.0.min(1.0) as f64)
{
agent.action_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
rng.gen_range(1..=4);
// Melee the target, do a whirlwind whenever he is trying to go
// behind or after every 5s
} else if attack_data.angle > 90.0
|| agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
{
// If our target is *very* behind, punish with a whirlwind
if attack_data.angle > 120.0 {
agent.action_state.int_counters
[ActionStateICounters::CurrentAbility as usize] = 4;
} else {
// If the target is in melee range of frost use primary and secondary
// attacks accordingly
if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 1.0 {
// Backhand anyone trying to circle strafe frost
if attack_data.angle > 160.0 {
// Use reorientate strike
controller.push_basic_input(InputKind::Secondary);
// If in front of frost use primary
}
} else {
// Hit them regularly
controller.push_basic_input(InputKind::Primary);
}
} else {
controller.push_basic_input(InputKind::Ability(4));
}
} else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
&& line_of_sight_with_target()
&& agent.action_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
{
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 1.0 {
// Use icespike attack
if should_use_targeted_spikes() {
controller.push_action(remote_spikes_action());
} else {
controller.push_basic_input(InputKind::Ability(0));
} else {
// or Flashfreeze
controller.push_basic_input(InputKind::Ability(4));
}
} else if attack_data.dist_sqrd < GIGAS_FREEZE_RANGE.powi(2)
&& line_of_sight_with_target()
} else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
&& agent.action_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
{
// Use Flashfreeze
controller.push_basic_input(InputKind::Ability(4));
} else if attack_data.dist_sqrd > GIGAS_LEAP_RANGE.powi(2) {
// Use ranged attack (icebombs) when past a certain distance
controller.push_basic_input(InputKind::Ability(2));
} else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2) {
// Use a leap attack (custom comp made by ythern) that goes
// after the furthest entity in range, Angle
// doesn't matter, should be spurratic
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 1.0 {
// Start a leap after either every 3s or our target is not in LoS
} else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
&& agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
{
controller.push_basic_input(InputKind::Ability(1));
} else {
// or icebombs
} else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
&& agent.action_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
{
controller.push_basic_input(InputKind::Ability(2));
// Spawn ice sprites under distant attackers
} else {
controller.push_action(remote_spikes_action());
}
}
agent.action_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
}
// Always attempt to path towards target
self.path_toward_target(
agent,
@ -5486,7 +5582,7 @@ impl<'a> AgentData<'a> {
rng: &mut impl Rng,
primary_weight: u8,
secondary_weight: u8,
ability_weights: [u8; MAX_ABILITIES],
ability_weights: [u8; BASE_ABILITY_LIMIT],
) {
let primary = self.extract_ability(AbilityInput::Primary);
let secondary = self.extract_ability(AbilityInput::Secondary);
@ -5534,7 +5630,7 @@ impl<'a> AgentData<'a> {
let secondary_chance = secondary_weight as f64
/ ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
let ability_chances = {
let mut chances = [0.0; MAX_ABILITIES];
let mut chances = [0.0; BASE_ABILITY_LIMIT];
chances.iter_mut().enumerate().for_each(|(i, chance)| {
*chance = ability_weights[i] as f64
/ (ability_weights

View File

@ -1,7 +1,7 @@
use crate::util::*;
use common::{
comp::{
ability::{CharacterAbility, MAX_ABILITIES},
ability::{CharacterAbility, BASE_ABILITY_LIMIT},
buff::{BuffKind, Buffs},
character_state::AttackFilters,
group,
@ -43,7 +43,6 @@ pub struct AgentData<'a> {
pub body: Option<&'a Body>,
pub inventory: &'a Inventory,
pub skill_set: &'a SkillSet,
#[allow(dead_code)] // may be useful for pathing
pub physics_state: &'a PhysicsState,
pub alignment: Option<&'a Alignment>,
pub traversal_config: TraversalConfig,
@ -182,7 +181,7 @@ pub enum Tactic {
RandomAbilities {
primary: u8,
secondary: u8,
abilities: [u8; MAX_ABILITIES],
abilities: [u8; BASE_ABILITY_LIMIT],
},
// Tool specific tactics

View File

@ -3,7 +3,7 @@ use common::{
character::CharacterId,
comp::{
inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
Waypoint,
Waypoint, BASE_ABILITY_LIMIT,
},
};
use specs::{Entity, WriteExpect};
@ -76,7 +76,7 @@ pub fn create_character(
inventory,
waypoint,
pets: Vec::new(),
active_abilities: Default::default(),
active_abilities: common::comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT),
map_marker,
});
Ok(())

View File

@ -925,19 +925,17 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
let to = pos + dir * power;
let _ = terrain
.ray(from, to)
.until(|block: &Block| {
.while_(|block: &Block| {
ray_energy -=
block.explode_power().unwrap_or(0.0) + rng.gen::<f32>() * 0.1;
// Stop if:
// 1) Block is liquid
// 2) Consumed all energy
// 3) Can't explode block (for example we hit stone wall)
let stop = block.is_liquid()
block.is_liquid()
|| block.explode_power().is_none()
|| ray_energy <= 0.0;
ray_energy -=
block.explode_power().unwrap_or(0.0) + rng.gen::<f32>() * 0.1;
stop
|| ray_energy <= 0.0
})
.for_each(|block: &Block, pos| {
if block.explode_power().is_some() {

View File

@ -438,7 +438,7 @@ pub fn handle_create_sprite(
let state = server.state_mut();
if state.can_set_block(pos) {
let block = state.terrain().get(pos).ok().copied();
if block.map_or(false, |b| (*b).is_air()) {
if block.map_or(false, |b| (*b).is_fluid()) {
let old_block = block.unwrap_or_else(|| Block::air(SpriteKind::Empty));
let new_block = old_block.with_sprite(sprite);
state.set_block(pos, new_block);

View File

@ -271,7 +271,7 @@ pub fn active_abilities_from_db_model(
abilities,
}| {
let mut auxiliary_abilities =
[comp::ability::AuxiliaryAbility::Empty; comp::ability::MAX_ABILITIES];
vec![comp::ability::AuxiliaryAbility::Empty; comp::ability::BASE_ABILITY_LIMIT];
for (empty, ability) in auxiliary_abilities.iter_mut().zip(abilities.into_iter()) {
*empty = aux_ability_from_string(&ability);
}
@ -285,7 +285,10 @@ pub fn active_abilities_from_db_model(
},
)
.collect::<HashMap<_, _>>();
comp::ability::ActiveAbilities::new(ability_sets)
comp::ability::ActiveAbilities::from_auxiliary(
ability_sets,
Some(comp::ability::BASE_ABILITY_LIMIT),
)
}
/// Struct containing item properties in the format that they get persisted to

View File

@ -22,7 +22,7 @@ use common::{
object,
skills::{GeneralSkill, Skill},
ChatType, Content, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence,
PresenceKind,
PresenceKind, BASE_ABILITY_LIMIT,
},
effect::Effect,
link::{Is, Link, LinkHandle},
@ -312,7 +312,11 @@ impl StateExt for State {
.unwrap_or(0),
))
.with(stats)
.with(comp::ActiveAbilities::default())
.with(if body.is_humanoid() {
comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT)
} else {
comp::ActiveAbilities::default()
})
.with(skill_set)
.maybe_with(health)
.with(poise)

View File

@ -201,6 +201,61 @@ impl Animation for AlphaAnimation {
* Quaternion::rotation_y(-1.8 + move1 * -0.4 + move2 * 3.5)
* Quaternion::rotation_z(move1 * -1.0 + move2 * -1.5);
},
Some("common.abilities.custom.gigas_frost.bonk") => {
next.head.orientation = Quaternion::rotation_x(move1 * 0.8 + move2 * -1.2);
next.jaw.position = Vec3::new(0.0, s_a.jaw.0, s_a.jaw.1);
next.jaw.orientation = Quaternion::rotation_x(move2 * -0.3);
next.control_l.position = Vec3::new(-0.5, 4.0, 1.0);
next.control_r.position = Vec3::new(-0.5, 4.0, 1.0);
next.control_l.orientation = Quaternion::rotation_x(PI / 2.0);
next.control_r.orientation = Quaternion::rotation_x(PI / 2.0);
next.weapon_l.position =
Vec3::new(-12.0 + (move1 * 20.0).min(10.0), -1.0, -15.0);
next.weapon_r.position =
Vec3::new(12.0 + (move1 * -20.0).max(-10.0), -1.0, -15.0);
next.weapon_l.orientation = Quaternion::rotation_x(-PI / 2.0 - 0.1)
* Quaternion::rotation_z(move1 * -1.0);
next.weapon_r.orientation = Quaternion::rotation_x(-PI / 2.0 - 0.1)
* Quaternion::rotation_z(move1 * 1.0);
next.shoulder_l.orientation =
Quaternion::rotation_x(-0.3 + move1 * 2.0 + move2 * -1.0);
next.shoulder_r.orientation =
Quaternion::rotation_x(-0.3 + move1 * 2.0 + move2 * -1.0);
next.control.orientation = Quaternion::rotation_x(move1 * 1.5 + move2 * -0.4);
let twist = move1 * 0.6 + move3 * -0.6;
next.upper_torso.position =
Vec3::new(0.0, s_a.upper_torso.0, s_a.upper_torso.1);
next.upper_torso.orientation =
Quaternion::rotation_x(move1 * 0.4 + move2 * -1.1)
* Quaternion::rotation_z(twist * -0.2 + move1 * -0.1 + move2 * 0.3);
next.lower_torso.orientation =
Quaternion::rotation_x(move1 * -0.4 + move2 * 1.1)
* Quaternion::rotation_z(twist);
next.foot_l.position = Vec3::new(
-s_a.foot.0,
s_a.foot.1 + move1 * -7.0 + move2 * 7.0,
s_a.foot.2,
);
next.foot_l.orientation = Quaternion::rotation_x(move1 * -0.8 + move2 * 0.8)
* Quaternion::rotation_z(move1 * 0.3 + move2 * -0.3);
next.foot_r.position = Vec3::new(
s_a.foot.0,
s_a.foot.1 + move1 * 5.0 + move2 * -5.0,
s_a.foot.2,
);
next.foot_r.orientation = Quaternion::rotation_y(move1 * -0.3 + move2 * 0.3)
* Quaternion::rotation_z(move1 * 0.4 + move2 * -0.4);
next.main.position = Vec3::new(-5.0 + (move1 * 20.0).min(10.0), 6.0, 4.0);
next.main.orientation = Quaternion::rotation_y(move1 * 0.4 + move2 * -1.2);
},
_ => {
next.control_l.position = Vec3::new(-1.0, 2.0, 12.0 + move2 * -10.0);
next.control_r.position = Vec3::new(1.0, 2.0, -2.0);

View File

@ -477,10 +477,14 @@ impl SfxMgr {
Outcome::SummonedCreature { pos, body, .. } => {
match body {
Body::BipedSmall(body) => match body.species {
biped_small::Species::Boreal | biped_small::Species::Clockwork => {
biped_small::Species::Clockwork => {
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::DeepLaugh);
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
},
biped_small::Species::Boreal => {
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GigaRoar);
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
},
_ => {},
},
Body::Object(object::Body::Flamethrower) => {

View File

@ -23,7 +23,7 @@ use common::{
combat,
comp::{
self,
ability::{Ability, ActiveAbilities, AuxiliaryAbility, MAX_ABILITIES},
ability::{Ability, ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT},
inventory::{
item::{
item_key::ItemKey,
@ -811,12 +811,12 @@ impl<'a> Widget for Diary<'a> {
state.update(|s| {
s.ids
.active_abilities
.resize(MAX_ABILITIES, &mut ui.widget_id_generator())
.resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator())
});
state.update(|s| {
s.ids
.active_abilities_keys
.resize(MAX_ABILITIES, &mut ui.widget_id_generator())
.resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator())
});
let mut slot_maker = SlotMaker {
@ -844,7 +844,7 @@ impl<'a> Widget for Diary<'a> {
pulse: 0.0,
};
for i in 0..MAX_ABILITIES {
for i in 0..BASE_ABILITY_LIMIT {
let ability_id = self
.active_abilities
.get_ability(

View File

@ -98,6 +98,7 @@ pub enum ParticleMode {
SnowStorm = 44,
PortalFizz = 45,
Ink = 46,
Whirlwind = 47,
}
impl ParticleMode {

View File

@ -11,8 +11,11 @@ use crate::{
use common::{
assets::{AssetExt, DotVoxAsset},
comp::{
self, aura, beam, body, buff, item::Reagent, object, shockwave, BeamSegment, Body,
CharacterState, Ori, Pos, Scale, Shockwave, Vel,
self, aura, beam, body, buff,
item::Reagent,
object,
shockwave::{self, ShockwaveDodgeable},
BeamSegment, Body, CharacterState, Ori, Pos, Scale, Shockwave, Vel,
},
figure::Segment,
outcome::Outcome,
@ -927,6 +930,29 @@ impl ParticleMgr {
}
}
},
states::spin_melee::FrontendSpecifier::Whirlwind => {
if matches!(spin.stage_section, StageSection::Action) {
let time = scene_data.state.get_time();
let mut rng = thread_rng();
self.particles.resize_with(
self.particles.len()
+ 3
+ usize::from(
self.scheduler.heartbeats(Duration::from_millis(5)),
),
|| {
Particle::new(
Duration::from_millis(1000),
time,
ParticleMode::Whirlwind,
interpolated
.pos
.map(|e| e + rng.gen_range(-0.25..0.25)),
)
},
);
}
},
}
}
},
@ -2246,7 +2272,8 @@ impl ParticleMgr {
let new_particle_count = particles_per_length * heartbeats as usize;
self.particles.reserve(new_particle_count);
// higher wave when wave doesn't require ground
let wave = if shockwave.properties.requires_ground {
let wave = if matches!(shockwave.properties.dodgeable, ShockwaveDodgeable::Jump)
{
0.5
} else {
8.0