mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Frost gigas tweaks
This commit is contained in:
parent
c1e7f9af50
commit
8a5f237e9c
@ -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
|
||||
|
@ -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"): (
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
))),
|
||||
)
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
))),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -10,6 +10,7 @@ BasicSummon(
|
||||
body_type: Male,
|
||||
)),
|
||||
scale: None,
|
||||
use_npc_name: true,
|
||||
has_health: true,
|
||||
loadout_config: None,
|
||||
skillset_config: Some(Rank3),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -10,6 +10,7 @@ BasicSummon(
|
||||
body_type: Male,
|
||||
)),
|
||||
scale: None,
|
||||
use_npc_name: true,
|
||||
has_health: true,
|
||||
loadout_config: Some(ClockworkSummon),
|
||||
skillset_config: None,
|
||||
|
@ -8,6 +8,7 @@ BasicSummon(
|
||||
body: Object(Flamethrower),
|
||||
scale: None,
|
||||
has_health: true,
|
||||
use_npc_name: true,
|
||||
loadout_config: None,
|
||||
skillset_config: None,
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
24
assets/common/abilities/custom/gigas_frost/bonk.ron
Normal file
24
assets/common/abilities/custom/gigas_frost/bonk.ron
Normal 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,
|
||||
)
|
@ -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),
|
||||
|
@ -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,
|
||||
))),
|
||||
)
|
@ -9,6 +9,7 @@ BasicSummon(
|
||||
species: Boreal,
|
||||
body_type: Male,
|
||||
)),
|
||||
use_npc_name: true,
|
||||
scale: None,
|
||||
has_health: true,
|
||||
loadout_config: Some(BorealSummon),
|
||||
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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,
|
||||
|
28
assets/common/abilities/custom/gigas_frost/whirlwind.ron
Normal file
28
assets/common/abilities/custom/gigas_frost/whirlwind.ron
Normal 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),
|
||||
)
|
@ -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),
|
||||
|
@ -9,6 +9,7 @@ BasicSummon(
|
||||
species: Husk,
|
||||
body_type: Male,
|
||||
)),
|
||||
use_npc_name: true,
|
||||
scale: None,
|
||||
has_health: true,
|
||||
loadout_config: Some(HuskSummon),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -1159,11 +1159,11 @@
|
||||
),
|
||||
husk: (
|
||||
keyword: "husk",
|
||||
generic: "Husk"
|
||||
generic: "Cultist Husk"
|
||||
),
|
||||
boreal: (
|
||||
keyword: "boreal",
|
||||
generic: "Boreal",
|
||||
generic: "Boreal Warrior",
|
||||
),
|
||||
bushly: (
|
||||
keyword: "bushly",
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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::{
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -171,4 +171,5 @@ pub enum MovementBehavior {
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum FrontendSpecifier {
|
||||
CultistVortex,
|
||||
Whirlwind,
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
(
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(())
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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) => {
|
||||
|
@ -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(
|
||||
|
@ -98,6 +98,7 @@ pub enum ParticleMode {
|
||||
SnowStorm = 44,
|
||||
PortalFizz = 45,
|
||||
Ink = 46,
|
||||
Whirlwind = 47,
|
||||
}
|
||||
|
||||
impl ParticleMode {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user