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.
|
- Mutliple singleplayer worlds and map generation UI.
|
||||||
- New arena building in desert cities, suitable for PVP, also NPCs like to watch the fights too
|
- 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
|
- The loading screen now displays status updates for singleplayer server and client initialization progress
|
||||||
|
- New Frost Gigas attacks & AI
|
||||||
|
|
||||||
### Changed
|
### 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.
|
- Updated windowing library, wayland may work better.
|
||||||
- Portal model has been updated by @Nectical
|
- Portal model has been updated by @Nectical
|
||||||
- Chat command responses sent by the server can now be localized
|
- 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
|
### Removed
|
||||||
- Medium and large potions from all loot tables
|
- 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.ice_volley"),
|
||||||
Simple(None, "common.abilities.custom.gigas_frost.frost_summons"),
|
Simple(None, "common.abilities.custom.gigas_frost.frost_summons"),
|
||||||
Simple(None, "common.abilities.custom.gigas_frost.flashfreeze"),
|
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"): (
|
Custom("Boreal Bow"): (
|
||||||
|
@ -11,7 +11,7 @@ LeapShockwave(
|
|||||||
shockwave_vertical_angle: 15.0,
|
shockwave_vertical_angle: 15.0,
|
||||||
shockwave_speed: 20.0,
|
shockwave_speed: 20.0,
|
||||||
shockwave_duration: 0.8,
|
shockwave_duration: 0.8,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.2,
|
move_efficiency: 0.2,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Steam,
|
specifier: Steam,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 20.0,
|
shockwave_speed: 20.0,
|
||||||
shockwave_duration: 0.5,
|
shockwave_duration: 0.5,
|
||||||
requires_ground: false,
|
dodgeable: Roll,
|
||||||
move_efficiency: 0.1,
|
move_efficiency: 0.1,
|
||||||
damage_kind: Energy,
|
damage_kind: Energy,
|
||||||
specifier: Fire,
|
specifier: Fire,
|
||||||
|
@ -17,7 +17,7 @@ ChargedRanged(
|
|||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.3),
|
||||||
chance: 1.0,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
move_speed: 0.6,
|
move_speed: 0.6,
|
||||||
|
@ -16,7 +16,7 @@ RepeaterRanged(
|
|||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.3),
|
||||||
chance: 1.0,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
)
|
)
|
@ -15,7 +15,7 @@ BasicRanged(
|
|||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.3),
|
||||||
chance: 1.0,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
move_efficiency: 0.3,
|
move_efficiency: 0.3,
|
||||||
|
@ -18,8 +18,8 @@ DashMelee(
|
|||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.3),
|
||||||
chance: 1.0,
|
chance: 0.5,
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
energy_drain: 0,
|
energy_drain: 0,
|
||||||
|
@ -17,7 +17,7 @@ LeapMelee(
|
|||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.3),
|
||||||
chance: 1.0,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
|
@ -17,8 +17,8 @@ ComboMelee(
|
|||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.3),
|
||||||
chance: 1.0,
|
chance: 0.4,
|
||||||
))),
|
))),
|
||||||
)],
|
)],
|
||||||
initial_energy_gain: 5.0,
|
initial_energy_gain: 5.0,
|
||||||
|
@ -10,6 +10,7 @@ BasicSummon(
|
|||||||
body_type: Male,
|
body_type: Male,
|
||||||
)),
|
)),
|
||||||
scale: None,
|
scale: None,
|
||||||
|
use_npc_name: true,
|
||||||
has_health: true,
|
has_health: true,
|
||||||
loadout_config: None,
|
loadout_config: None,
|
||||||
skillset_config: Some(Rank3),
|
skillset_config: Some(Rank3),
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 3.5,
|
shockwave_duration: 3.5,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Ground,
|
specifier: Ground,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Lightning,
|
specifier: Lightning,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 25.0,
|
shockwave_speed: 25.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Water,
|
specifier: Water,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 360.0,
|
shockwave_vertical_angle: 360.0,
|
||||||
shockwave_speed: 40.0,
|
shockwave_speed: 40.0,
|
||||||
shockwave_duration: 0.4,
|
shockwave_duration: 0.4,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Piercing,
|
damage_kind: Piercing,
|
||||||
specifier: Ground,
|
specifier: Ground,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Steam,
|
specifier: Steam,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 3.0,
|
shockwave_duration: 3.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Fire,
|
specifier: Fire,
|
||||||
|
@ -10,6 +10,7 @@ BasicSummon(
|
|||||||
body_type: Male,
|
body_type: Male,
|
||||||
)),
|
)),
|
||||||
scale: None,
|
scale: None,
|
||||||
|
use_npc_name: true,
|
||||||
has_health: true,
|
has_health: true,
|
||||||
loadout_config: Some(ClockworkSummon),
|
loadout_config: Some(ClockworkSummon),
|
||||||
skillset_config: None,
|
skillset_config: None,
|
||||||
|
@ -8,6 +8,7 @@ BasicSummon(
|
|||||||
body: Object(Flamethrower),
|
body: Object(Flamethrower),
|
||||||
scale: None,
|
scale: None,
|
||||||
has_health: true,
|
has_health: true,
|
||||||
|
use_npc_name: true,
|
||||||
loadout_config: None,
|
loadout_config: None,
|
||||||
skillset_config: None,
|
skillset_config: None,
|
||||||
),
|
),
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 3.0,
|
shockwave_duration: 3.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Water,
|
specifier: Water,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Fire,
|
specifier: Fire,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Ice,
|
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(
|
BasicMelee(
|
||||||
energy_cost: 0,
|
energy_cost: 0,
|
||||||
buildup_duration: 0.9,
|
buildup_duration: 0.5,
|
||||||
swing_duration: 0.1,
|
swing_duration: 0.1,
|
||||||
recover_duration: 0.7,
|
recover_duration: 0.7,
|
||||||
melee_constructor: (
|
melee_constructor: (
|
||||||
kind: Slash(
|
kind: Slash(
|
||||||
damage: 85.0,
|
damage: 60.0,
|
||||||
poise: 5.0,
|
poise: 5.0,
|
||||||
knockback: 5.0,
|
knockback: 5.0,
|
||||||
energy_regen: 10.0,
|
energy_regen: 10.0,
|
||||||
),
|
),
|
||||||
range: 5.0,
|
range: 7.0,
|
||||||
angle: 75.0,
|
angle: 75.0,
|
||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 1.0,
|
dur_secs: 1.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.5),
|
||||||
chance: 0.3,
|
chance: 0.3,
|
||||||
))),
|
))),
|
||||||
multi_target: Some(Normal),
|
multi_target: Some(Normal),
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
Shockwave(
|
Shockwave(
|
||||||
energy_cost: 0,
|
energy_cost: 0,
|
||||||
buildup_duration: 2.0,
|
buildup_duration: 1.8,
|
||||||
swing_duration: 0.12,
|
swing_duration: 0.12,
|
||||||
recover_duration: 1.5,
|
recover_duration: 1.5,
|
||||||
damage: 45.0,
|
damage: 50.0,
|
||||||
poise_damage: 30,
|
poise_damage: 30,
|
||||||
knockback: (strength: 0.0, direction: TowardsUp),
|
knockback: (strength: 0.0, direction: TowardsUp),
|
||||||
shockwave_angle: 240.0,
|
shockwave_angle: 220.0,
|
||||||
shockwave_vertical_angle: 360.0,
|
shockwave_vertical_angle: 360.0,
|
||||||
shockwave_speed: 200.0,
|
shockwave_speed: 200.0,
|
||||||
shockwave_duration: 0.15,
|
shockwave_duration: 0.15,
|
||||||
requires_ground: false,
|
dodgeable: No,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.2,
|
||||||
damage_kind: Piercing,
|
damage_kind: Piercing,
|
||||||
specifier: IceSpikes,
|
specifier: IceSpikes,
|
||||||
ori_rate: 0.0,
|
ori_rate: 0.1,
|
||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 2.0,
|
dur_secs: 2.0,
|
||||||
strength: DamageFraction(0.3),
|
strength: Value(3.0),
|
||||||
chance: 1.0,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
)
|
)
|
@ -9,6 +9,7 @@ BasicSummon(
|
|||||||
species: Boreal,
|
species: Boreal,
|
||||||
body_type: Male,
|
body_type: Male,
|
||||||
)),
|
)),
|
||||||
|
use_npc_name: true,
|
||||||
scale: None,
|
scale: None,
|
||||||
has_health: true,
|
has_health: true,
|
||||||
loadout_config: Some(BorealSummon),
|
loadout_config: Some(BorealSummon),
|
||||||
|
@ -9,8 +9,8 @@ BasicRanged(
|
|||||||
min_falloff: 0.1,
|
min_falloff: 0.1,
|
||||||
),
|
),
|
||||||
projectile_body: Object(IceBomb),
|
projectile_body: Object(IceBomb),
|
||||||
projectile_speed: 25.0,
|
projectile_speed: 40.0,
|
||||||
num_projectiles: 5,
|
num_projectiles: 5,
|
||||||
projectile_spread: 0.07,
|
projectile_spread: 0.05,
|
||||||
move_efficiency: 0.3,
|
move_efficiency: 0.3,
|
||||||
)
|
)
|
@ -3,8 +3,8 @@ SpriteSummon(
|
|||||||
cast_duration: 0.1,
|
cast_duration: 0.1,
|
||||||
recover_duration: 1.1,
|
recover_duration: 1.1,
|
||||||
sprite: IceSpike,
|
sprite: IceSpike,
|
||||||
del_timeout: Some((2, 5)),
|
del_timeout: Some((5, 15)),
|
||||||
summon_distance: (2, 12),
|
summon_distance: (2, 18),
|
||||||
sparseness: 0.95,
|
sparseness: 0.96,
|
||||||
angle: 360,
|
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),
|
knockback: (strength: 3.0, direction: Up),
|
||||||
shockwave_angle: 360.0,
|
shockwave_angle: 360.0,
|
||||||
shockwave_vertical_angle: 15.0,
|
shockwave_vertical_angle: 15.0,
|
||||||
shockwave_speed: 20.0,
|
shockwave_speed: 30.0,
|
||||||
shockwave_duration: 0.8,
|
shockwave_duration: 1.2,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.2,
|
move_efficiency: 0.2,
|
||||||
damage_kind: Piercing,
|
damage_kind: Piercing,
|
||||||
specifier: IceSpikes,
|
specifier: IceSpikes,
|
||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 1.0,
|
dur_secs: 1.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(1.2),
|
||||||
chance: 1.0,
|
chance: 1.0,
|
||||||
))),
|
))),
|
||||||
forward_leap_strength: 45.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,
|
recover_duration: 0.8,
|
||||||
melee_constructor: (
|
melee_constructor: (
|
||||||
kind: Slash(
|
kind: Slash(
|
||||||
damage: 90.0,
|
damage: 70.0,
|
||||||
poise: 20.0,
|
poise: 20.0,
|
||||||
knockback: 5.0,
|
knockback: 5.0,
|
||||||
energy_regen: 5.0,
|
energy_regen: 5.0,
|
||||||
),
|
),
|
||||||
range: 5.0,
|
range: 7.0,
|
||||||
angle: 120.0,
|
angle: 120.0,
|
||||||
damage_effect: Some(Buff((
|
damage_effect: Some(Buff((
|
||||||
kind: Frozen,
|
kind: Frozen,
|
||||||
dur_secs: 1.0,
|
dur_secs: 1.0,
|
||||||
strength: DamageFraction(0.1),
|
strength: Value(0.5),
|
||||||
chance: 0.5,
|
chance: 0.5,
|
||||||
))),
|
))),
|
||||||
multi_target: Some(Normal),
|
multi_target: Some(Normal),
|
||||||
|
@ -9,6 +9,7 @@ BasicSummon(
|
|||||||
species: Husk,
|
species: Husk,
|
||||||
body_type: Male,
|
body_type: Male,
|
||||||
)),
|
)),
|
||||||
|
use_npc_name: true,
|
||||||
scale: None,
|
scale: None,
|
||||||
has_health: true,
|
has_health: true,
|
||||||
loadout_config: Some(HuskSummon),
|
loadout_config: Some(HuskSummon),
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 15.0,
|
shockwave_vertical_angle: 15.0,
|
||||||
shockwave_speed: 20.0,
|
shockwave_speed: 20.0,
|
||||||
shockwave_duration: 0.8,
|
shockwave_duration: 0.8,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.2,
|
move_efficiency: 0.2,
|
||||||
damage_kind: Piercing,
|
damage_kind: Piercing,
|
||||||
specifier: IceSpikes,
|
specifier: IceSpikes,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Ink,
|
specifier: Ink,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 65.0,
|
shockwave_speed: 65.0,
|
||||||
shockwave_duration: 1.0,
|
shockwave_duration: 1.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.05,
|
move_efficiency: 0.05,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Ground,
|
specifier: Ground,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 30.0,
|
shockwave_vertical_angle: 30.0,
|
||||||
shockwave_speed: 10.0,
|
shockwave_speed: 10.0,
|
||||||
shockwave_duration: 5.0,
|
shockwave_duration: 5.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Water,
|
specifier: Water,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 20.0,
|
shockwave_speed: 20.0,
|
||||||
shockwave_duration: 0.5,
|
shockwave_duration: 0.5,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.1,
|
move_efficiency: 0.1,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Ground,
|
specifier: Ground,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Poison,
|
specifier: Poison,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 2.0,
|
shockwave_duration: 2.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.0,
|
move_efficiency: 0.0,
|
||||||
damage_kind: Crushing,
|
damage_kind: Crushing,
|
||||||
specifier: Ground,
|
specifier: Ground,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 15.0,
|
shockwave_vertical_angle: 15.0,
|
||||||
shockwave_speed: 15.0,
|
shockwave_speed: 15.0,
|
||||||
shockwave_duration: 3.0,
|
shockwave_duration: 3.0,
|
||||||
requires_ground: true,
|
dodgeable: Jump,
|
||||||
move_efficiency: 0.2,
|
move_efficiency: 0.2,
|
||||||
damage_kind: Piercing,
|
damage_kind: Piercing,
|
||||||
specifier: IceSpikes,
|
specifier: IceSpikes,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90,
|
shockwave_vertical_angle: 90,
|
||||||
shockwave_speed: 10,
|
shockwave_speed: 10,
|
||||||
shockwave_duration: 1,
|
shockwave_duration: 1,
|
||||||
requires_ground: false,
|
dodgeable: Roll,
|
||||||
move_efficiency: 0,
|
move_efficiency: 0,
|
||||||
damage_kind: Energy,
|
damage_kind: Energy,
|
||||||
specifier: Fire,
|
specifier: Fire,
|
||||||
|
@ -10,7 +10,7 @@ Shockwave(
|
|||||||
shockwave_vertical_angle: 90.0,
|
shockwave_vertical_angle: 90.0,
|
||||||
shockwave_speed: 30.0,
|
shockwave_speed: 30.0,
|
||||||
shockwave_duration: 0.5,
|
shockwave_duration: 0.5,
|
||||||
requires_ground: false,
|
dodgeable: Roll,
|
||||||
move_efficiency: 0.1,
|
move_efficiency: 0.1,
|
||||||
damage_kind: Energy,
|
damage_kind: Energy,
|
||||||
specifier: Fire,
|
specifier: Fire,
|
||||||
|
@ -1159,11 +1159,11 @@
|
|||||||
),
|
),
|
||||||
husk: (
|
husk: (
|
||||||
keyword: "husk",
|
keyword: "husk",
|
||||||
generic: "Husk"
|
generic: "Cultist Husk"
|
||||||
),
|
),
|
||||||
boreal: (
|
boreal: (
|
||||||
keyword: "boreal",
|
keyword: "boreal",
|
||||||
generic: "Boreal",
|
generic: "Boreal Warrior",
|
||||||
),
|
),
|
||||||
bushly: (
|
bushly: (
|
||||||
keyword: "bushly",
|
keyword: "bushly",
|
||||||
|
@ -83,6 +83,7 @@ const int GIGA_SNOW = 42;
|
|||||||
const int CYCLOPS_CHARGE = 43;
|
const int CYCLOPS_CHARGE = 43;
|
||||||
const int PORTAL_FIZZ = 45;
|
const int PORTAL_FIZZ = 45;
|
||||||
const int INK = 46;
|
const int INK = 46;
|
||||||
|
const int WHIRLWIND = 47;
|
||||||
|
|
||||||
// meters per second squared (acceleration)
|
// meters per second squared (acceleration)
|
||||||
const float earth_gravity = 9.807;
|
const float earth_gravity = 9.807;
|
||||||
@ -703,6 +704,15 @@ void main() {
|
|||||||
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
|
spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9)
|
||||||
);
|
);
|
||||||
break;
|
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:
|
default:
|
||||||
attr = Attr(
|
attr = Attr(
|
||||||
linear_motion(
|
linear_motion(
|
||||||
|
@ -40,6 +40,7 @@ pub enum AttackSource {
|
|||||||
Beam,
|
Beam,
|
||||||
GroundShockwave,
|
GroundShockwave,
|
||||||
AirShockwave,
|
AirShockwave,
|
||||||
|
UndodgeableShockwave,
|
||||||
Explosion,
|
Explosion,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -948,7 +949,9 @@ impl From<AttackSource> for DamageSource {
|
|||||||
AttackSource::Melee => DamageSource::Melee,
|
AttackSource::Melee => DamageSource::Melee,
|
||||||
AttackSource::Projectile => DamageSource::Projectile,
|
AttackSource::Projectile => DamageSource::Projectile,
|
||||||
AttackSource::Explosion => DamageSource::Explosion,
|
AttackSource::Explosion => DamageSource::Explosion,
|
||||||
AttackSource::AirShockwave | AttackSource::GroundShockwave => DamageSource::Shockwave,
|
AttackSource::AirShockwave
|
||||||
|
| AttackSource::GroundShockwave
|
||||||
|
| AttackSource::UndodgeableShockwave => DamageSource::Shockwave,
|
||||||
AttackSource::Beam => DamageSource::Energy,
|
AttackSource::Beam => DamageSource::Energy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ use crate::{
|
|||||||
resources::Secs,
|
resources::Secs,
|
||||||
states::{
|
states::{
|
||||||
behavior::JoinData,
|
behavior::JoinData,
|
||||||
|
sprite_summon::SpriteSummonAnchor,
|
||||||
utils::{AbilityInfo, ComboConsumption, ScalingKind, StageSection},
|
utils::{AbilityInfo, ComboConsumption, ScalingKind, StageSection},
|
||||||
*,
|
*,
|
||||||
},
|
},
|
||||||
@ -33,9 +34,11 @@ use crate::{
|
|||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specs::{Component, DerefFlaggedStorage};
|
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>);
|
pub type AuxiliaryKey = (Option<ToolKind>, Option<ToolKind>);
|
||||||
|
|
||||||
// TODO: Potentially look into storing previous ability sets for weapon
|
// TODO: Potentially look into storing previous ability sets for weapon
|
||||||
@ -48,7 +51,8 @@ pub struct ActiveAbilities {
|
|||||||
pub primary: PrimaryAbility,
|
pub primary: PrimaryAbility,
|
||||||
pub secondary: SecondaryAbility,
|
pub secondary: SecondaryAbility,
|
||||||
pub movement: MovementAbility,
|
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 {
|
impl Component for ActiveAbilities {
|
||||||
@ -62,19 +66,35 @@ impl Default for ActiveAbilities {
|
|||||||
primary: PrimaryAbility::Tool,
|
primary: PrimaryAbility::Tool,
|
||||||
secondary: SecondaryAbility::Tool,
|
secondary: SecondaryAbility::Tool,
|
||||||
movement: MovementAbility::Species,
|
movement: MovementAbility::Species,
|
||||||
|
limit: None,
|
||||||
auxiliary_sets: HashMap::new(),
|
auxiliary_sets: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveAbilities {
|
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 {
|
ActiveAbilities {
|
||||||
auxiliary_sets,
|
auxiliary_sets: auxiliary_sets
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, set)| limit.map_or(true, |limit| set.len() == limit))
|
||||||
|
.collect(),
|
||||||
|
limit,
|
||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_limited(limit: usize) -> Self {
|
||||||
|
ActiveAbilities {
|
||||||
|
limit: Some(limit),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn change_ability(
|
pub fn change_ability(
|
||||||
&mut self,
|
&mut self,
|
||||||
slot: usize,
|
slot: usize,
|
||||||
@ -86,7 +106,7 @@ impl ActiveAbilities {
|
|||||||
let auxiliary_set = self
|
let auxiliary_set = self
|
||||||
.auxiliary_sets
|
.auxiliary_sets
|
||||||
.entry(auxiliary_key)
|
.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) {
|
if let Some(ability) = auxiliary_set.get_mut(slot) {
|
||||||
*ability = new_ability;
|
*ability = new_ability;
|
||||||
}
|
}
|
||||||
@ -111,13 +131,13 @@ impl ActiveAbilities {
|
|||||||
&self,
|
&self,
|
||||||
inv: Option<&Inventory>,
|
inv: Option<&Inventory>,
|
||||||
skill_set: Option<&SkillSet>,
|
skill_set: Option<&SkillSet>,
|
||||||
) -> [AuxiliaryAbility; MAX_ABILITIES] {
|
) -> Cow<Vec<AuxiliaryAbility>> {
|
||||||
let aux_key = Self::active_auxiliary_key(inv);
|
let aux_key = Self::active_auxiliary_key(inv);
|
||||||
|
|
||||||
self.auxiliary_sets
|
self.auxiliary_sets
|
||||||
.get(&aux_key)
|
.get(&aux_key)
|
||||||
.copied()
|
.map(Cow::Borrowed)
|
||||||
.unwrap_or_else(|| Self::default_ability_set(inv, skill_set))
|
.unwrap_or_else(|| Cow::Owned(Self::default_ability_set(inv, skill_set, self.limit)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ability(
|
pub fn get_ability(
|
||||||
@ -311,7 +331,8 @@ impl ActiveAbilities {
|
|||||||
fn default_ability_set<'a>(
|
fn default_ability_set<'a>(
|
||||||
inv: Option<&'a Inventory>,
|
inv: Option<&'a Inventory>,
|
||||||
skill_set: Option<&'a SkillSet>,
|
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)
|
let mut iter = Self::iter_available_abilities(inv, skill_set, EquipSlot::ActiveMainhand)
|
||||||
.map(AuxiliaryAbility::MainWeapon)
|
.map(AuxiliaryAbility::MainWeapon)
|
||||||
.chain(
|
.chain(
|
||||||
@ -319,7 +340,13 @@ impl ActiveAbilities {
|
|||||||
.map(AuxiliaryAbility::OffWeapon),
|
.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_vertical_angle: f32,
|
||||||
shockwave_speed: f32,
|
shockwave_speed: f32,
|
||||||
shockwave_duration: f32,
|
shockwave_duration: f32,
|
||||||
requires_ground: bool,
|
dodgeable: ShockwaveDodgeable,
|
||||||
move_efficiency: f32,
|
move_efficiency: f32,
|
||||||
damage_kind: DamageKind,
|
damage_kind: DamageKind,
|
||||||
specifier: comp::shockwave::FrontendSpecifier,
|
specifier: comp::shockwave::FrontendSpecifier,
|
||||||
@ -863,7 +890,7 @@ pub enum CharacterAbility {
|
|||||||
shockwave_vertical_angle: f32,
|
shockwave_vertical_angle: f32,
|
||||||
shockwave_speed: f32,
|
shockwave_speed: f32,
|
||||||
shockwave_duration: f32,
|
shockwave_duration: f32,
|
||||||
requires_ground: bool,
|
dodgeable: ShockwaveDodgeable,
|
||||||
move_efficiency: f32,
|
move_efficiency: f32,
|
||||||
damage_kind: DamageKind,
|
damage_kind: DamageKind,
|
||||||
specifier: comp::shockwave::FrontendSpecifier,
|
specifier: comp::shockwave::FrontendSpecifier,
|
||||||
@ -944,6 +971,10 @@ pub enum CharacterAbility {
|
|||||||
sparseness: f64,
|
sparseness: f64,
|
||||||
angle: f32,
|
angle: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
anchor: SpriteSummonAnchor,
|
||||||
|
#[serde(default)]
|
||||||
|
move_efficiency: f32,
|
||||||
|
#[serde(default)]
|
||||||
meta: AbilityMeta,
|
meta: AbilityMeta,
|
||||||
},
|
},
|
||||||
Music {
|
Music {
|
||||||
@ -1348,7 +1379,7 @@ impl CharacterAbility {
|
|||||||
shockwave_vertical_angle: _,
|
shockwave_vertical_angle: _,
|
||||||
shockwave_speed: _,
|
shockwave_speed: _,
|
||||||
ref mut shockwave_duration,
|
ref mut shockwave_duration,
|
||||||
requires_ground: _,
|
dodgeable: _,
|
||||||
move_efficiency: _,
|
move_efficiency: _,
|
||||||
damage_kind: _,
|
damage_kind: _,
|
||||||
specifier: _,
|
specifier: _,
|
||||||
@ -1467,7 +1498,7 @@ impl CharacterAbility {
|
|||||||
shockwave_vertical_angle: _,
|
shockwave_vertical_angle: _,
|
||||||
shockwave_speed: _,
|
shockwave_speed: _,
|
||||||
ref mut shockwave_duration,
|
ref mut shockwave_duration,
|
||||||
requires_ground: _,
|
dodgeable: _,
|
||||||
move_efficiency: _,
|
move_efficiency: _,
|
||||||
damage_kind: _,
|
damage_kind: _,
|
||||||
specifier: _,
|
specifier: _,
|
||||||
@ -1592,6 +1623,8 @@ impl CharacterAbility {
|
|||||||
summon_distance: (ref mut inner_dist, ref mut outer_dist),
|
summon_distance: (ref mut inner_dist, ref mut outer_dist),
|
||||||
sparseness: _,
|
sparseness: _,
|
||||||
angle: _,
|
angle: _,
|
||||||
|
anchor: _,
|
||||||
|
move_efficiency: _,
|
||||||
meta: _,
|
meta: _,
|
||||||
} => {
|
} => {
|
||||||
// TODO: Figure out how/if power should affect this
|
// TODO: Figure out how/if power should affect this
|
||||||
@ -2462,7 +2495,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
|
|||||||
shockwave_vertical_angle,
|
shockwave_vertical_angle,
|
||||||
shockwave_speed,
|
shockwave_speed,
|
||||||
shockwave_duration,
|
shockwave_duration,
|
||||||
requires_ground,
|
dodgeable,
|
||||||
move_efficiency,
|
move_efficiency,
|
||||||
damage_kind,
|
damage_kind,
|
||||||
specifier,
|
specifier,
|
||||||
@ -2483,7 +2516,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
|
|||||||
shockwave_vertical_angle: *shockwave_vertical_angle,
|
shockwave_vertical_angle: *shockwave_vertical_angle,
|
||||||
shockwave_speed: *shockwave_speed,
|
shockwave_speed: *shockwave_speed,
|
||||||
shockwave_duration: Duration::from_secs_f32(*shockwave_duration),
|
shockwave_duration: Duration::from_secs_f32(*shockwave_duration),
|
||||||
requires_ground: *requires_ground,
|
dodgeable: *dodgeable,
|
||||||
move_efficiency: *move_efficiency,
|
move_efficiency: *move_efficiency,
|
||||||
damage_kind: *damage_kind,
|
damage_kind: *damage_kind,
|
||||||
specifier: *specifier,
|
specifier: *specifier,
|
||||||
@ -2654,7 +2687,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
|
|||||||
shockwave_vertical_angle,
|
shockwave_vertical_angle,
|
||||||
shockwave_speed,
|
shockwave_speed,
|
||||||
shockwave_duration,
|
shockwave_duration,
|
||||||
requires_ground,
|
dodgeable,
|
||||||
move_efficiency,
|
move_efficiency,
|
||||||
damage_kind,
|
damage_kind,
|
||||||
specifier,
|
specifier,
|
||||||
@ -2673,7 +2706,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
|
|||||||
shockwave_vertical_angle: *shockwave_vertical_angle,
|
shockwave_vertical_angle: *shockwave_vertical_angle,
|
||||||
shockwave_speed: *shockwave_speed,
|
shockwave_speed: *shockwave_speed,
|
||||||
shockwave_duration: Duration::from_secs_f32(*shockwave_duration),
|
shockwave_duration: Duration::from_secs_f32(*shockwave_duration),
|
||||||
requires_ground: *requires_ground,
|
dodgeable: *dodgeable,
|
||||||
move_efficiency: *move_efficiency,
|
move_efficiency: *move_efficiency,
|
||||||
damage_effect: *damage_effect,
|
damage_effect: *damage_effect,
|
||||||
ability_info,
|
ability_info,
|
||||||
@ -2821,6 +2854,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
|
|||||||
summon_distance,
|
summon_distance,
|
||||||
sparseness,
|
sparseness,
|
||||||
angle,
|
angle,
|
||||||
|
anchor,
|
||||||
|
move_efficiency,
|
||||||
meta: _,
|
meta: _,
|
||||||
} => CharacterState::SpriteSummon(sprite_summon::Data {
|
} => CharacterState::SpriteSummon(sprite_summon::Data {
|
||||||
static_data: sprite_summon::StaticData {
|
static_data: sprite_summon::StaticData {
|
||||||
@ -2832,6 +2867,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
|
|||||||
summon_distance: *summon_distance,
|
summon_distance: *summon_distance,
|
||||||
sparseness: *sparseness,
|
sparseness: *sparseness,
|
||||||
angle: *angle,
|
angle: *angle,
|
||||||
|
anchor: *anchor,
|
||||||
|
move_efficiency: *move_efficiency,
|
||||||
ability_info,
|
ability_info,
|
||||||
},
|
},
|
||||||
timer: Duration::default(),
|
timer: Duration::default(),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
comp::{
|
comp::{
|
||||||
arthropod, biped_small, bird_medium, humanoid, quadruped_low, quadruped_medium,
|
arthropod, biped_large, biped_small, bird_medium, humanoid, quadruped_low,
|
||||||
quadruped_small, ship, Body, UtteranceKind,
|
quadruped_medium, quadruped_small, ship, Body, UtteranceKind,
|
||||||
},
|
},
|
||||||
path::Chaser,
|
path::Chaser,
|
||||||
rtsim::{NpcInput, RtSimController},
|
rtsim::{NpcInput, RtSimController},
|
||||||
@ -383,6 +383,10 @@ impl<'a> From<&'a Body> for Psyche {
|
|||||||
},
|
},
|
||||||
sight_dist: match body {
|
sight_dist: match body {
|
||||||
Body::BirdLarge(_) => 250.0,
|
Body::BirdLarge(_) => 250.0,
|
||||||
|
Body::BipedLarge(biped_large) => match biped_large.species {
|
||||||
|
biped_large::Species::Gigasfrost => 200.0,
|
||||||
|
_ => 100.0,
|
||||||
|
},
|
||||||
_ => 40.0,
|
_ => 40.0,
|
||||||
},
|
},
|
||||||
listen_dist: 30.0,
|
listen_dist: 30.0,
|
||||||
|
@ -869,7 +869,7 @@ impl Body {
|
|||||||
biped_large::Species::Huskbrute => 800,
|
biped_large::Species::Huskbrute => 800,
|
||||||
biped_large::Species::Cultistwarlord => 250,
|
biped_large::Species::Cultistwarlord => 250,
|
||||||
biped_large::Species::Cultistwarlock => 250,
|
biped_large::Species::Cultistwarlock => 250,
|
||||||
biped_large::Species::Gigasfrost => 20000,
|
biped_large::Species::Gigasfrost => 30000,
|
||||||
biped_large::Species::AdletElder => 1500,
|
biped_large::Species::AdletElder => 1500,
|
||||||
biped_large::Species::Tursus => 300,
|
biped_large::Species::Tursus => 300,
|
||||||
biped_large::Species::SeaBishop => 550,
|
biped_large::Species::SeaBishop => 550,
|
||||||
@ -1095,6 +1095,7 @@ impl Body {
|
|||||||
Body::BipedLarge(biped_large) => match biped_large.species {
|
Body::BipedLarge(biped_large) => match biped_large.species {
|
||||||
biped_large::Species::Mindflayer => 320,
|
biped_large::Species::Mindflayer => 320,
|
||||||
biped_large::Species::Minotaur => 280,
|
biped_large::Species::Minotaur => 280,
|
||||||
|
biped_large::Species::Gigasfrost => 800,
|
||||||
_ => 250,
|
_ => 250,
|
||||||
},
|
},
|
||||||
Body::BipedSmall(b) => match b.species {
|
Body::BipedSmall(b) => match b.species {
|
||||||
|
@ -156,7 +156,7 @@ pub enum BuffKind {
|
|||||||
/// Results from drinking a potion.
|
/// Results from drinking a potion.
|
||||||
/// Decreases the health gained from subsequent potions.
|
/// Decreases the health gained from subsequent potions.
|
||||||
PotionSickness,
|
PotionSickness,
|
||||||
// Changed into another body.
|
/// Changed into another body.
|
||||||
Polymorphed(Body),
|
Polymorphed(Body),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,7 +428,10 @@ pub struct BuffData {
|
|||||||
pub strength: f32,
|
pub strength: f32,
|
||||||
pub duration: Option<Secs>,
|
pub duration: Option<Secs>,
|
||||||
pub delay: 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>,
|
pub secondary_duration: Option<Secs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,6 +440,7 @@ impl BuffData {
|
|||||||
Self {
|
Self {
|
||||||
strength,
|
strength,
|
||||||
duration,
|
duration,
|
||||||
|
force_immediate: false,
|
||||||
delay: None,
|
delay: None,
|
||||||
secondary_duration: None,
|
secondary_duration: None,
|
||||||
}
|
}
|
||||||
@ -451,6 +455,12 @@ impl BuffData {
|
|||||||
self.secondary_duration = Some(sec_dur);
|
self.secondary_duration = Some(sec_dur);
|
||||||
self
|
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.
|
/// De/buff category ID.
|
||||||
|
@ -920,16 +920,10 @@ impl CharacterState {
|
|||||||
AttackSource::Projectile
|
AttackSource::Projectile
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
CharacterState::Shockwave(data) => Some(if data.static_data.requires_ground {
|
CharacterState::Shockwave(data) => Some(data.static_data.dodgeable.to_attack_source()),
|
||||||
AttackSource::GroundShockwave
|
CharacterState::LeapShockwave(data) => {
|
||||||
} else {
|
Some(data.static_data.dodgeable.to_attack_source())
|
||||||
AttackSource::AirShockwave
|
},
|
||||||
}),
|
|
||||||
CharacterState::LeapShockwave(data) => Some(if data.static_data.requires_ground {
|
|
||||||
AttackSource::GroundShockwave
|
|
||||||
} else {
|
|
||||||
AttackSource::AirShockwave
|
|
||||||
}),
|
|
||||||
CharacterState::BasicBeam(_) => Some(AttackSource::Beam),
|
CharacterState::BasicBeam(_) => Some(AttackSource::Beam),
|
||||||
CharacterState::BasicAura(_) => None,
|
CharacterState::BasicAura(_) => None,
|
||||||
CharacterState::Blink(_) => None,
|
CharacterState::Blink(_) => None,
|
||||||
@ -974,6 +968,7 @@ impl AttackFilters {
|
|||||||
AttackSource::Beam => self.beams,
|
AttackSource::Beam => self.beams,
|
||||||
AttackSource::GroundShockwave => self.ground_shockwaves,
|
AttackSource::GroundShockwave => self.ground_shockwaves,
|
||||||
AttackSource::AirShockwave => self.air_shockwaves,
|
AttackSource::AirShockwave => self.air_shockwaves,
|
||||||
|
AttackSource::UndodgeableShockwave => false,
|
||||||
AttackSource::Explosion => self.explosions,
|
AttackSource::Explosion => self.explosions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ pub mod visual;
|
|||||||
pub use self::{
|
pub use self::{
|
||||||
ability::{
|
ability::{
|
||||||
Ability, AbilityInput, ActiveAbilities, CharacterAbility, CharacterAbilityType, Stance,
|
Ability, AbilityInput, ActiveAbilities, CharacterAbility, CharacterAbilityType, Stance,
|
||||||
MAX_ABILITIES,
|
BASE_ABILITY_LIMIT,
|
||||||
},
|
},
|
||||||
admin::{Admin, AdminRole},
|
admin::{Admin, AdminRole},
|
||||||
agent::{
|
agent::{
|
||||||
|
@ -10,6 +10,7 @@ use crate::{
|
|||||||
uid::Uid,
|
uid::Uid,
|
||||||
Explosion, RadiusEffect,
|
Explosion, RadiusEffect,
|
||||||
};
|
};
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specs::Component;
|
use specs::Component;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -761,10 +762,18 @@ impl ProjectileConstructor {
|
|||||||
.with_crit(crit_chance, crit_mult)
|
.with_crit(crit_chance, crit_mult)
|
||||||
.with_effect(knockback)
|
.with_effect(knockback)
|
||||||
.with_effect(buff);
|
.with_effect(buff);
|
||||||
|
let variation = thread_rng().gen::<f32>();
|
||||||
let explosion = Explosion {
|
let explosion = Explosion {
|
||||||
effects: vec![
|
effects: vec![
|
||||||
RadiusEffect::Attack(attack),
|
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,
|
radius,
|
||||||
reagent: Some(Reagent::White),
|
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 serde::{Deserialize, Serialize};
|
||||||
use specs::{Component, DerefFlaggedStorage};
|
use specs::{Component, DerefFlaggedStorage};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum ShockwaveDodgeable {
|
||||||
|
Roll,
|
||||||
|
Jump,
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Properties {
|
pub struct Properties {
|
||||||
pub angle: f32,
|
pub angle: f32,
|
||||||
pub vertical_angle: f32,
|
pub vertical_angle: f32,
|
||||||
pub speed: f32,
|
pub speed: f32,
|
||||||
pub attack: Attack,
|
pub attack: Attack,
|
||||||
pub requires_ground: bool,
|
pub dodgeable: ShockwaveDodgeable,
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
pub owner: Option<Uid>,
|
pub owner: Option<Uid>,
|
||||||
pub specifier: FrontendSpecifier,
|
pub specifier: FrontendSpecifier,
|
||||||
@ -56,3 +66,13 @@ pub enum FrontendSpecifier {
|
|||||||
Ink,
|
Ink,
|
||||||
Lightning,
|
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.
|
// has been determined, so we start sampling terrain.
|
||||||
// Check for falling off walls and try moving straight
|
// Check for falling off walls and try moving straight
|
||||||
// towards the target if falling is not a danger
|
// 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(
|
vol.get(
|
||||||
(pos + Vec3::<f32>::from(tgt_dir) * 2.5).map(|e| e as i32)
|
(pos + Vec3::<f32>::from(tgt_dir) * 2.5).map(|e| e as i32)
|
||||||
+ Vec3::unit_z() * z,
|
+ 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>,
|
from: Vec3<f32>,
|
||||||
to: Vec3<f32>,
|
to: Vec3<f32>,
|
||||||
until: F,
|
until: F,
|
||||||
|
is_while: bool,
|
||||||
for_each: Option<G>,
|
for_each: Option<G>,
|
||||||
max_iter: usize,
|
max_iter: usize,
|
||||||
ignore_error: bool,
|
ignore_error: bool,
|
||||||
@ -25,6 +26,7 @@ where
|
|||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
until,
|
until,
|
||||||
|
is_while: false,
|
||||||
for_each: None,
|
for_each: None,
|
||||||
max_iter: 100,
|
max_iter: 100,
|
||||||
ignore_error: false,
|
ignore_error: false,
|
||||||
@ -37,6 +39,20 @@ where
|
|||||||
from: self.from,
|
from: self.from,
|
||||||
to: self.to,
|
to: self.to,
|
||||||
until: f,
|
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,
|
for_each: self.for_each,
|
||||||
max_iter: self.max_iter,
|
max_iter: self.max_iter,
|
||||||
ignore_error: self.ignore_error,
|
ignore_error: self.ignore_error,
|
||||||
@ -49,6 +65,7 @@ where
|
|||||||
vol: self.vol,
|
vol: self.vol,
|
||||||
from: self.from,
|
from: self.from,
|
||||||
to: self.to,
|
to: self.to,
|
||||||
|
is_while: self.is_while,
|
||||||
until: self.until,
|
until: self.until,
|
||||||
max_iter: self.max_iter,
|
max_iter: self.max_iter,
|
||||||
ignore_error: self.ignore_error,
|
ignore_error: self.ignore_error,
|
||||||
@ -86,17 +103,28 @@ where
|
|||||||
|
|
||||||
let vox = self.vol.get(ipos);
|
let vox = self.vol.get(ipos);
|
||||||
|
|
||||||
// for_each
|
if self.is_while {
|
||||||
if let Some(g) = &mut self.for_each {
|
let vox = match vox.map(|vox| (vox, (self.until)(vox))) {
|
||||||
if let Ok(vox) = 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);
|
g(vox, ipos);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match vox.map(|vox| (vox, (self.until)(vox))) {
|
match vox.map(|vox| (vox, (self.until)(vox))) {
|
||||||
Ok((vox, true)) => return (dist, Ok(Some(vox))),
|
Ok((vox, true)) => return (dist, Ok(Some(vox))),
|
||||||
Err(err) if !self.ignore_error => return (dist, Err(err)),
|
Err(err) if !self.ignore_error => return (dist, Err(err)),
|
||||||
_ => {},
|
_ => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let deltas =
|
let deltas =
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
|
Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
|
||||||
},
|
},
|
||||||
event::{LocalEvent, NpcBuilder, ServerEvent},
|
event::{LocalEvent, NpcBuilder, ServerEvent},
|
||||||
|
npc::NPC_NAMES,
|
||||||
outcome::Outcome,
|
outcome::Outcome,
|
||||||
skillset_builder::{self, SkillSetBuilder},
|
skillset_builder::{self, SkillSetBuilder},
|
||||||
states::{
|
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 = self.static_data.summon_info.has_health.then(|| {
|
||||||
let health_level = skill_set
|
let health_level = skill_set
|
||||||
@ -248,6 +262,8 @@ pub struct SummonInfo {
|
|||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
scale: Option<comp::Scale>,
|
scale: Option<comp::Scale>,
|
||||||
has_health: bool,
|
has_health: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
use_npc_name: bool,
|
||||||
// TODO: use assets for specifying skills and loadout?
|
// TODO: use assets for specifying skills and loadout?
|
||||||
loadout_config: Option<loadout_builder::Preset>,
|
loadout_config: Option<loadout_builder::Preset>,
|
||||||
skillset_config: Option<skillset_builder::Preset>,
|
skillset_config: Option<skillset_builder::Preset>,
|
||||||
|
@ -3,13 +3,19 @@ use crate::{
|
|||||||
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
|
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
|
||||||
DamageSource, GroupTarget, Knockback,
|
DamageSource, GroupTarget, Knockback,
|
||||||
},
|
},
|
||||||
comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate},
|
comp::{
|
||||||
|
character_state::OutputEvents,
|
||||||
|
item::Reagent,
|
||||||
|
shockwave::{self, ShockwaveDodgeable},
|
||||||
|
CharacterState, StateUpdate,
|
||||||
|
},
|
||||||
event::{LocalEvent, ServerEvent},
|
event::{LocalEvent, ServerEvent},
|
||||||
outcome::Outcome,
|
outcome::Outcome,
|
||||||
states::{
|
states::{
|
||||||
behavior::{CharacterBehavior, JoinData},
|
behavior::{CharacterBehavior, JoinData},
|
||||||
utils::{StageSection, *},
|
utils::{StageSection, *},
|
||||||
},
|
},
|
||||||
|
Explosion, KnockbackDir, RadiusEffect,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -39,8 +45,8 @@ pub struct StaticData {
|
|||||||
pub shockwave_speed: f32,
|
pub shockwave_speed: f32,
|
||||||
/// How long the shockwave travels for
|
/// How long the shockwave travels for
|
||||||
pub shockwave_duration: Duration,
|
pub shockwave_duration: Duration,
|
||||||
/// Whether the shockwave requires the target to be on the ground
|
/// If the shockwave can be dodged, and in what way
|
||||||
pub requires_ground: bool,
|
pub dodgeable: ShockwaveDodgeable,
|
||||||
/// Movement speed efficiency
|
/// Movement speed efficiency
|
||||||
pub move_efficiency: f32,
|
pub move_efficiency: f32,
|
||||||
/// Adds an effect onto the main damage of the attack
|
/// Adds an effect onto the main damage of the attack
|
||||||
@ -173,7 +179,7 @@ impl CharacterBehavior for Data {
|
|||||||
speed: self.static_data.shockwave_speed,
|
speed: self.static_data.shockwave_speed,
|
||||||
duration: self.static_data.shockwave_duration,
|
duration: self.static_data.shockwave_duration,
|
||||||
attack,
|
attack,
|
||||||
requires_ground: self.static_data.requires_ground,
|
dodgeable: self.static_data.dodgeable,
|
||||||
owner: Some(*data.uid),
|
owner: Some(*data.uid),
|
||||||
specifier: self.static_data.specifier,
|
specifier: self.static_data.specifier,
|
||||||
};
|
};
|
||||||
@ -185,6 +191,35 @@ impl CharacterBehavior for Data {
|
|||||||
// Send local event used for frontend shenanigans
|
// Send local event used for frontend shenanigans
|
||||||
match self.static_data.specifier {
|
match self.static_data.specifier {
|
||||||
shockwave::FrontendSpecifier::IceSpikes => {
|
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(
|
output_events.emit_local(LocalEvent::CreateOutcome(
|
||||||
Outcome::IceSpikes {
|
Outcome::IceSpikes {
|
||||||
pos: data.pos.0
|
pos: data.pos.0
|
||||||
|
@ -3,7 +3,11 @@ use crate::{
|
|||||||
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
|
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
|
||||||
DamageSource, GroupTarget, Knockback,
|
DamageSource, GroupTarget, Knockback,
|
||||||
},
|
},
|
||||||
comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate},
|
comp::{
|
||||||
|
character_state::OutputEvents,
|
||||||
|
shockwave::{self, ShockwaveDodgeable},
|
||||||
|
CharacterState, StateUpdate,
|
||||||
|
},
|
||||||
event::{LocalEvent, ServerEvent},
|
event::{LocalEvent, ServerEvent},
|
||||||
outcome::Outcome,
|
outcome::Outcome,
|
||||||
states::{
|
states::{
|
||||||
@ -37,8 +41,8 @@ pub struct StaticData {
|
|||||||
pub shockwave_speed: f32,
|
pub shockwave_speed: f32,
|
||||||
/// How long the shockwave travels for
|
/// How long the shockwave travels for
|
||||||
pub shockwave_duration: Duration,
|
pub shockwave_duration: Duration,
|
||||||
/// Whether the shockwave requires the target to be on the ground
|
/// If the shockwave can be dodged, and in what way
|
||||||
pub requires_ground: bool,
|
pub dodgeable: ShockwaveDodgeable,
|
||||||
/// Movement speed efficiency
|
/// Movement speed efficiency
|
||||||
pub move_efficiency: f32,
|
pub move_efficiency: f32,
|
||||||
/// What key is used to press ability
|
/// What key is used to press ability
|
||||||
@ -117,7 +121,7 @@ impl CharacterBehavior for Data {
|
|||||||
speed: self.static_data.shockwave_speed,
|
speed: self.static_data.shockwave_speed,
|
||||||
duration: self.static_data.shockwave_duration,
|
duration: self.static_data.shockwave_duration,
|
||||||
attack,
|
attack,
|
||||||
requires_ground: self.static_data.requires_ground,
|
dodgeable: self.static_data.dodgeable,
|
||||||
owner: Some(*data.uid),
|
owner: Some(*data.uid),
|
||||||
specifier: self.static_data.specifier,
|
specifier: self.static_data.specifier,
|
||||||
};
|
};
|
||||||
|
@ -171,4 +171,5 @@ pub enum MovementBehavior {
|
|||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum FrontendSpecifier {
|
pub enum FrontendSpecifier {
|
||||||
CultistVortex,
|
CultistVortex,
|
||||||
|
Whirlwind,
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,13 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use vek::*;
|
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
|
/// Separated out to condense update portions of character state
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct StaticData {
|
pub struct StaticData {
|
||||||
@ -31,10 +38,14 @@ pub struct StaticData {
|
|||||||
pub del_timeout: Option<(f32, f32)>,
|
pub del_timeout: Option<(f32, f32)>,
|
||||||
/// Range that sprites are created relative to the summonner
|
/// Range that sprites are created relative to the summonner
|
||||||
pub summon_distance: (f32, f32),
|
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
|
/// Chance that sprite is not created on a particular square
|
||||||
pub sparseness: f64,
|
pub sparseness: f64,
|
||||||
/// Angle of total coverage, centered on the forward-facing orientation
|
/// Angle of total coverage, centered on the forward-facing orientation
|
||||||
pub angle: f32,
|
pub angle: f32,
|
||||||
|
/// How much we can move
|
||||||
|
pub move_efficiency: f32,
|
||||||
/// Miscellaneous information about the ability
|
/// Miscellaneous information about the ability
|
||||||
pub ability_info: AbilityInfo,
|
pub ability_info: AbilityInfo,
|
||||||
}
|
}
|
||||||
@ -56,6 +67,17 @@ impl CharacterBehavior for Data {
|
|||||||
fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
|
fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
|
||||||
let mut update = StateUpdate::from(data);
|
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 {
|
match self.stage_section {
|
||||||
StageSection::Buildup => {
|
StageSection::Buildup => {
|
||||||
if self.timer < self.static_data.buildup_duration {
|
if self.timer < self.static_data.buildup_duration {
|
||||||
@ -102,15 +124,23 @@ impl CharacterBehavior for Data {
|
|||||||
<= (self.static_data.angle / 2.0)
|
<= (self.static_data.angle / 2.0)
|
||||||
&& !thread_rng().gen_bool(self.static_data.sparseness)
|
&& !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
|
// The coordinates of where the sprite is created
|
||||||
let sprite_pos = Vec3::new(
|
let sprite_pos = Vec3::new(
|
||||||
data.pos.0.x.floor() as i32 + point.x,
|
anchor_pos.x.floor() as i32 + point.x,
|
||||||
data.pos.0.y.floor() as i32 + point.y,
|
anchor_pos.y.floor() as i32 + point.y,
|
||||||
data.pos.0.z.floor() as i32,
|
anchor_pos.z.floor() as i32,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for collision in z up to 10 blocks up or down
|
// Check for collision in z up to 10 blocks up or down
|
||||||
let obstacle_z = data
|
let (obstacle_z, obstale_z_result) = data
|
||||||
.terrain
|
.terrain
|
||||||
.ray(
|
.ray(
|
||||||
sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 10.0,
|
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)
|
Block::is_solid(b)
|
||||||
&& b.get_sprite() != Some(self.static_data.sprite)
|
&& b.get_sprite() != Some(self.static_data.sprite)
|
||||||
})
|
})
|
||||||
.cast()
|
.cast();
|
||||||
.0;
|
|
||||||
|
|
||||||
// z height relative to caster
|
// 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
|
// Location sprite will be created
|
||||||
let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z);
|
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
|
// Send local event used for frontend shenanigans
|
||||||
if self.static_data.sprite == SpriteKind::IceSpike {
|
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 {
|
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 {
|
} else {
|
||||||
|
@ -396,6 +396,7 @@ impl Block {
|
|||||||
BlockKind::WeakRock => Some(0.75),
|
BlockKind::WeakRock => Some(0.75),
|
||||||
BlockKind::Snow => Some(0.1),
|
BlockKind::Snow => Some(0.1),
|
||||||
BlockKind::Ice => Some(0.5),
|
BlockKind::Ice => Some(0.5),
|
||||||
|
BlockKind::Wood => Some(4.5),
|
||||||
BlockKind::Lava => None,
|
BlockKind::Lava => None,
|
||||||
_ => self.get_sprite().and_then(|sprite| match sprite {
|
_ => self.get_sprite().and_then(|sprite| match sprite {
|
||||||
sprite if sprite.is_container() => None,
|
sprite if sprite.is_container() => None,
|
||||||
|
@ -161,7 +161,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
entity,
|
entity,
|
||||||
buff_change: BuffChange::Add(Buff::new(
|
buff_change: BuffChange::Add(Buff::new(
|
||||||
BuffKind::Bleeding,
|
BuffKind::Bleeding,
|
||||||
BuffData::new(1.0, Some(Secs(6.0))),
|
BuffData::new(1.0, Some(Secs(6.0))).with_force_immediate(true),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
BuffSource::World,
|
BuffSource::World,
|
||||||
*read_data.time,
|
*read_data.time,
|
||||||
@ -179,7 +179,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
entity,
|
entity,
|
||||||
buff_change: BuffChange::Add(Buff::new(
|
buff_change: BuffChange::Add(Buff::new(
|
||||||
BuffKind::Bleeding,
|
BuffKind::Bleeding,
|
||||||
BuffData::new(5.0, Some(Secs(3.0))),
|
BuffData::new(5.0, Some(Secs(3.0))).with_force_immediate(true),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
BuffSource::World,
|
BuffSource::World,
|
||||||
*read_data.time,
|
*read_data.time,
|
||||||
@ -215,7 +215,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
entity,
|
entity,
|
||||||
buff_change: BuffChange::Add(Buff::new(
|
buff_change: BuffChange::Add(Buff::new(
|
||||||
BuffKind::Bleeding,
|
BuffKind::Bleeding,
|
||||||
BuffData::new(15.0, Some(Secs(0.1))),
|
BuffData::new(15.0, Some(Secs(0.1))).with_force_immediate(true),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
BuffSource::World,
|
BuffSource::World,
|
||||||
*read_data.time,
|
*read_data.time,
|
||||||
@ -228,7 +228,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
entity,
|
entity,
|
||||||
buff_change: BuffChange::Add(Buff::new(
|
buff_change: BuffChange::Add(Buff::new(
|
||||||
BuffKind::Frozen,
|
BuffKind::Frozen,
|
||||||
BuffData::new(0.2, Some(Secs(1.0))),
|
BuffData::new(0.2, Some(Secs(3.0))),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
BuffSource::World,
|
BuffSource::World,
|
||||||
*read_data.time,
|
*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,
|
None,
|
||||||
read_data.inventories.get(entity),
|
read_data.inventories.get(entity),
|
||||||
Some(&stat),
|
Some(&stat),
|
||||||
&read_data.msm,
|
&read_data.msm,
|
||||||
);
|
) - 1.0)
|
||||||
if (damage_reduction - 1.0).abs() < f32::EPSILON {
|
.abs()
|
||||||
|
< f32::EPSILON;
|
||||||
|
if infinite_damage_reduction {
|
||||||
for (id, buff) in buff_comp.buffs.iter() {
|
for (id, buff) in buff_comp.buffs.iter() {
|
||||||
if !buff.kind.is_buff() {
|
if !buff.kind.is_buff() {
|
||||||
expired_buffs.push(*id);
|
expired_buffs.push(*id);
|
||||||
@ -389,6 +391,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
|
buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
|
||||||
for (buff_kind, (buff_ids, kind_start_time)) in buff_kinds.into_iter() {
|
for (buff_kind, (buff_ids, kind_start_time)) in buff_kinds.into_iter() {
|
||||||
let mut active_buff_ids = Vec::new();
|
let mut active_buff_ids = Vec::new();
|
||||||
|
if infinite_damage_reduction && !buff_kind.is_buff() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if buff_kind.stacks() {
|
if buff_kind.stacks() {
|
||||||
// Process all the buffs of this kind
|
// Process all the buffs of this kind
|
||||||
active_buff_ids = buff_ids;
|
active_buff_ids = buff_ids;
|
||||||
@ -414,6 +420,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
execute_effect(
|
execute_effect(
|
||||||
effect,
|
effect,
|
||||||
buff.kind,
|
buff.kind,
|
||||||
|
&buff.data,
|
||||||
buff.start_time,
|
buff.start_time,
|
||||||
kind_start_time,
|
kind_start_time,
|
||||||
&read_data,
|
&read_data,
|
||||||
@ -471,6 +478,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
fn execute_effect(
|
fn execute_effect(
|
||||||
effect: &BuffEffect,
|
effect: &BuffEffect,
|
||||||
buff_kind: BuffKind,
|
buff_kind: BuffKind,
|
||||||
|
buff_data: &BuffData,
|
||||||
buff_start_time: Time,
|
buff_start_time: Time,
|
||||||
buff_kind_start_time: Time,
|
buff_kind_start_time: Time,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
@ -508,7 +516,9 @@ fn execute_effect(
|
|||||||
let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
|
let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
|
||||||
let whole_ticks = curr_tick - prev_tick;
|
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
|
// If the buff is ending, include the fraction of progress towards the next
|
||||||
// tick.
|
// tick.
|
||||||
let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;
|
let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use common::{
|
use common::{
|
||||||
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
|
combat::{self, AttackOptions, AttackerInfo, TargetInfo},
|
||||||
comp::{
|
comp::{
|
||||||
agent::{Sound, SoundKind},
|
agent::{Sound, SoundKind},
|
||||||
|
shockwave::ShockwaveDodgeable,
|
||||||
Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori,
|
Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori,
|
||||||
PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
|
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))
|
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
|
&& (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 {
|
if hit {
|
||||||
let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
|
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
|
.character_states
|
||||||
.get(target)
|
.get(target)
|
||||||
.and_then(|cs| cs.attack_immunities())
|
.and_then(|cs| cs.attack_immunities())
|
||||||
.map_or(false, |i| {
|
.map_or(false, |i| match shockwave.dodgeable {
|
||||||
if shockwave.requires_ground {
|
ShockwaveDodgeable::Roll => i.air_shockwaves,
|
||||||
i.ground_shockwaves
|
ShockwaveDodgeable::Jump => i.ground_shockwaves,
|
||||||
} else {
|
ShockwaveDodgeable::No => false,
|
||||||
i.air_shockwaves
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// PvP check
|
// PvP check
|
||||||
let may_harm = combat::may_harm(
|
let may_harm = combat::may_harm(
|
||||||
@ -244,11 +246,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
dir,
|
dir,
|
||||||
attack_options,
|
attack_options,
|
||||||
1.0,
|
1.0,
|
||||||
if shockwave.requires_ground {
|
shockwave.dodgeable.to_attack_source(),
|
||||||
AttackSource::GroundShockwave
|
|
||||||
} else {
|
|
||||||
AttackSource::AirShockwave
|
|
||||||
},
|
|
||||||
*read_data.time,
|
*read_data.time,
|
||||||
|e| server_emitter.emit(e),
|
|e| server_emitter.emit(e),
|
||||||
|o| outcomes_emitter.emit(o),
|
|o| outcomes_emitter.emit(o),
|
||||||
|
@ -198,7 +198,7 @@ impl Sentiment {
|
|||||||
// remembering the last interaction instead of constant updates.
|
// remembering the last interaction instead of constant updates.
|
||||||
if rng.gen_bool(
|
if rng.gen_bool(
|
||||||
(1.0 / (self.positivity.unsigned_abs() as f32 * DECAY_TIME_FACTOR.powi(2) * dt))
|
(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();
|
self.positivity -= self.positivity.signum();
|
||||||
}
|
}
|
||||||
|
@ -289,9 +289,19 @@ impl Data {
|
|||||||
}
|
}
|
||||||
// Spawn one monster Gigasfrost into the world
|
// Spawn one monster Gigasfrost into the world
|
||||||
// Try a few times to find a location that's not underwater
|
// 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)))
|
.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)| {
|
.map(|(pos, chunk)| {
|
||||||
let wpos2d = pos.cpos_to_wpos_center();
|
let wpos2d = pos.cpos_to_wpos_center();
|
||||||
(
|
(
|
||||||
|
@ -14,7 +14,7 @@ use rand::prelude::*;
|
|||||||
use rand_chacha::ChaChaRng;
|
use rand_chacha::ChaChaRng;
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
use vek::{Clamp, Vec2};
|
use vek::{Clamp, Vec2};
|
||||||
use world::site::SiteKind;
|
use world::{site::SiteKind, CONFIG};
|
||||||
|
|
||||||
pub struct SimulateNpcs;
|
pub struct SimulateNpcs;
|
||||||
|
|
||||||
@ -132,7 +132,14 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
|||||||
.map(|e| e as f32 + 0.5)
|
.map(|e| e as f32 + 0.5)
|
||||||
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||||
} else {
|
} 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(|_| {
|
.map(|_| {
|
||||||
ctx.world
|
ctx.world
|
||||||
.sim()
|
.sim()
|
||||||
@ -140,10 +147,9 @@ fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
|
|||||||
.map(|sz| rng.gen_range(0..sz as i32))
|
.map(|sz| rng.gen_range(0..sz as i32))
|
||||||
})
|
})
|
||||||
.find(|pos| {
|
.find(|pos| {
|
||||||
ctx.world
|
ctx.world.sim().get(*pos).map_or(false, |c| {
|
||||||
.sim()
|
!c.is_underwater() && (!is_gigas || c.temp < CONFIG.snow_temp)
|
||||||
.get(*pos)
|
})
|
||||||
.map_or(false, |c| !c.is_underwater())
|
|
||||||
})
|
})
|
||||||
.unwrap_or(ctx.world.sim().get_size().as_() / 2);
|
.unwrap_or(ctx.world.sim().get_size().as_() / 2);
|
||||||
let wpos2d = pos.cpos_to_wpos_center();
|
let wpos2d = pos.cpos_to_wpos_center();
|
||||||
|
@ -14,7 +14,7 @@ use common::{
|
|||||||
combat::perception_dist_multiplier_from_stealth,
|
combat::perception_dist_multiplier_from_stealth,
|
||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
ability::MAX_ABILITIES,
|
ability::BASE_ABILITY_LIMIT,
|
||||||
agent::{Sound, SoundKind, Target},
|
agent::{Sound, SoundKind, Target},
|
||||||
inventory::slot::EquipSlot,
|
inventory::slot::EquipSlot,
|
||||||
item::{
|
item::{
|
||||||
@ -1097,7 +1097,7 @@ impl<'a> AgentData<'a> {
|
|||||||
"Frostfang" => Tactic::RandomAbilities {
|
"Frostfang" => Tactic::RandomAbilities {
|
||||||
primary: 1,
|
primary: 1,
|
||||||
secondary: 3,
|
secondary: 3,
|
||||||
abilities: [0; MAX_ABILITIES],
|
abilities: [0; BASE_ABILITY_LIMIT],
|
||||||
},
|
},
|
||||||
"Tursus Claws" => Tactic::RandomAbilities {
|
"Tursus Claws" => Tactic::RandomAbilities {
|
||||||
primary: 2,
|
primary: 2,
|
||||||
@ -1551,9 +1551,14 @@ impl<'a> AgentData<'a> {
|
|||||||
read_data,
|
read_data,
|
||||||
rng,
|
rng,
|
||||||
),
|
),
|
||||||
Tactic::FrostGigas => {
|
Tactic::FrostGigas => self.handle_frostgigas_attack(
|
||||||
self.handle_frostgigas_attack(agent, controller, &attack_data, tgt_data, read_data)
|
agent,
|
||||||
},
|
controller,
|
||||||
|
&attack_data,
|
||||||
|
tgt_data,
|
||||||
|
read_data,
|
||||||
|
rng,
|
||||||
|
),
|
||||||
Tactic::BorealHammer => self.handle_boreal_hammer_attack(
|
Tactic::BorealHammer => self.handle_boreal_hammer_attack(
|
||||||
agent,
|
agent,
|
||||||
controller,
|
controller,
|
||||||
|
@ -5,15 +5,19 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, MAX_ABILITIES},
|
ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, BASE_ABILITY_LIMIT},
|
||||||
buff::BuffKind,
|
buff::BuffKind,
|
||||||
item::tool::AbilityContext,
|
item::tool::AbilityContext,
|
||||||
skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
|
skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
|
||||||
AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent,
|
Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction,
|
||||||
Controller, InputKind,
|
ControlEvent, Controller, Fluid, InputKind,
|
||||||
},
|
},
|
||||||
path::TraversalConfig,
|
path::TraversalConfig,
|
||||||
states::{self_buff, sprite_summon, utils::StageSection},
|
states::{
|
||||||
|
self_buff,
|
||||||
|
sprite_summon::{self, SpriteSummonAnchor},
|
||||||
|
utils::StageSection,
|
||||||
|
},
|
||||||
terrain::Block,
|
terrain::Block,
|
||||||
util::Dir,
|
util::Dir,
|
||||||
vol::ReadVol,
|
vol::ReadVol,
|
||||||
@ -4190,43 +4194,61 @@ impl<'a> AgentData<'a> {
|
|||||||
attack_data: &AttackData,
|
attack_data: &AttackData,
|
||||||
tgt_data: &TargetData,
|
tgt_data: &TargetData,
|
||||||
read_data: &ReadData,
|
read_data: &ReadData,
|
||||||
|
rng: &mut impl Rng,
|
||||||
) {
|
) {
|
||||||
const GIGAS_MELEE_RANGE: f32 = 6.0;
|
const GIGAS_MELEE_RANGE: f32 = 12.0;
|
||||||
const GIGAS_SPIKE_RANGE: f32 = 12.0;
|
const GIGAS_SPIKE_RANGE: f32 = 16.0;
|
||||||
const GIGAS_FREEZE_RANGE: f32 = 20.0;
|
const ICEBOMB_RANGE: f32 = 70.0;
|
||||||
const GIGAS_LEAP_RANGE: f32 = 30.0;
|
const GIGAS_LEAP_RANGE: f32 = 50.0;
|
||||||
const MINION_SUMMON_THRESHOLD: f32 = 0.2;
|
const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
|
||||||
|
const FLASHFREEZE_RANGE: f32 = 30.;
|
||||||
|
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
enum ActionStateTimers {
|
||||||
|
AttackChange,
|
||||||
|
Bonk,
|
||||||
|
}
|
||||||
|
|
||||||
enum ActionStateFCounters {
|
enum ActionStateFCounters {
|
||||||
MinionSummonThreshold = 0,
|
FCounterMinionSummonThreshold = 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_of_sight_with_target = || {
|
enum ActionStateICounters {
|
||||||
entities_have_line_of_sight(
|
/// An ability that is forced to fully complete until moving on to
|
||||||
self.pos,
|
/// other attacks.
|
||||||
self.body,
|
/// 1 = Leap shockwave, 2 = Flashfreeze, 3 = Spike summon,
|
||||||
self.scale,
|
/// 4 = Whirlwind, 5 = Remote ice spikes, 6 = Ice bombs
|
||||||
tgt_data.pos,
|
CurrentAbility = 0,
|
||||||
tgt_data.body,
|
}
|
||||||
tgt_data.scale,
|
|
||||||
read_data,
|
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());
|
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
|
// Sets counter at start of combat, using `condition` to keep track of whether
|
||||||
// it was already initialized
|
// it was already initialized
|
||||||
if !agent.action_state.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;
|
1.0 - MINION_SUMMON_THRESHOLD;
|
||||||
agent.action_state.initialized = true;
|
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
|
// Summon minions at particular thresholds of health
|
||||||
controller.push_basic_input(InputKind::Ability(3));
|
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))
|
if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
|
||||||
{
|
{
|
||||||
agent.action_state.counters
|
agent.action_state.counters
|
||||||
[ActionStateFCounters::MinionSummonThreshold as usize] -=
|
[ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
|
||||||
MINION_SUMMON_THRESHOLD;
|
MINION_SUMMON_THRESHOLD;
|
||||||
}
|
}
|
||||||
} else {
|
// Continue casting any attacks that are forced to complete
|
||||||
// If the target is in melee range of frost use primary and secondary
|
} else if let Some(ability) = Some(
|
||||||
// attacks accordingly
|
&mut agent.action_state.int_counters[ActionStateICounters::CurrentAbility as usize],
|
||||||
if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
|
)
|
||||||
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 1.0 {
|
.filter(|i| **i != 0)
|
||||||
// Backhand anyone trying to circle strafe frost
|
{
|
||||||
if attack_data.angle > 160.0 {
|
if *ability == 3 && should_use_targeted_spikes() {
|
||||||
// Use reorientate strike
|
*ability = 5
|
||||||
controller.push_basic_input(InputKind::Secondary);
|
};
|
||||||
// If in front of frost use primary
|
|
||||||
} else {
|
let reset = match ability {
|
||||||
// Hit them regularly
|
// Must be rolled
|
||||||
controller.push_basic_input(InputKind::Primary);
|
1 => {
|
||||||
}
|
|
||||||
} else {
|
|
||||||
controller.push_basic_input(InputKind::Ability(4));
|
|
||||||
}
|
|
||||||
} else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
|
|
||||||
&& line_of_sight_with_target()
|
|
||||||
{
|
|
||||||
if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 1.0 {
|
|
||||||
// Use icespike attack
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
// 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 {
|
|
||||||
controller.push_basic_input(InputKind::Ability(1));
|
controller.push_basic_input(InputKind::Ability(1));
|
||||||
} else {
|
matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
|
||||||
// or icebombs
|
},
|
||||||
|
// 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));
|
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;
|
||||||
}
|
}
|
||||||
agent.action_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.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 {
|
||||||
|
controller.push_basic_input(InputKind::Secondary);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
controller.push_basic_input(InputKind::Primary);
|
||||||
|
}
|
||||||
|
} else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
|
||||||
|
&& agent.action_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
|
||||||
|
{
|
||||||
|
if should_use_targeted_spikes() {
|
||||||
|
controller.push_action(remote_spikes_action());
|
||||||
|
} else {
|
||||||
|
controller.push_basic_input(InputKind::Ability(0));
|
||||||
|
}
|
||||||
|
} else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
|
||||||
|
&& agent.action_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
|
||||||
|
{
|
||||||
|
controller.push_basic_input(InputKind::Ability(4));
|
||||||
|
// 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 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always attempt to path towards target
|
// Always attempt to path towards target
|
||||||
self.path_toward_target(
|
self.path_toward_target(
|
||||||
agent,
|
agent,
|
||||||
@ -5486,7 +5582,7 @@ impl<'a> AgentData<'a> {
|
|||||||
rng: &mut impl Rng,
|
rng: &mut impl Rng,
|
||||||
primary_weight: u8,
|
primary_weight: u8,
|
||||||
secondary_weight: u8,
|
secondary_weight: u8,
|
||||||
ability_weights: [u8; MAX_ABILITIES],
|
ability_weights: [u8; BASE_ABILITY_LIMIT],
|
||||||
) {
|
) {
|
||||||
let primary = self.extract_ability(AbilityInput::Primary);
|
let primary = self.extract_ability(AbilityInput::Primary);
|
||||||
let secondary = self.extract_ability(AbilityInput::Secondary);
|
let secondary = self.extract_ability(AbilityInput::Secondary);
|
||||||
@ -5534,7 +5630,7 @@ impl<'a> AgentData<'a> {
|
|||||||
let secondary_chance = secondary_weight as f64
|
let secondary_chance = secondary_weight as f64
|
||||||
/ ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
|
/ ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
|
||||||
let ability_chances = {
|
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)| {
|
chances.iter_mut().enumerate().for_each(|(i, chance)| {
|
||||||
*chance = ability_weights[i] as f64
|
*chance = ability_weights[i] as f64
|
||||||
/ (ability_weights
|
/ (ability_weights
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::util::*;
|
use crate::util::*;
|
||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
ability::{CharacterAbility, MAX_ABILITIES},
|
ability::{CharacterAbility, BASE_ABILITY_LIMIT},
|
||||||
buff::{BuffKind, Buffs},
|
buff::{BuffKind, Buffs},
|
||||||
character_state::AttackFilters,
|
character_state::AttackFilters,
|
||||||
group,
|
group,
|
||||||
@ -43,7 +43,6 @@ pub struct AgentData<'a> {
|
|||||||
pub body: Option<&'a Body>,
|
pub body: Option<&'a Body>,
|
||||||
pub inventory: &'a Inventory,
|
pub inventory: &'a Inventory,
|
||||||
pub skill_set: &'a SkillSet,
|
pub skill_set: &'a SkillSet,
|
||||||
#[allow(dead_code)] // may be useful for pathing
|
|
||||||
pub physics_state: &'a PhysicsState,
|
pub physics_state: &'a PhysicsState,
|
||||||
pub alignment: Option<&'a Alignment>,
|
pub alignment: Option<&'a Alignment>,
|
||||||
pub traversal_config: TraversalConfig,
|
pub traversal_config: TraversalConfig,
|
||||||
@ -182,7 +181,7 @@ pub enum Tactic {
|
|||||||
RandomAbilities {
|
RandomAbilities {
|
||||||
primary: u8,
|
primary: u8,
|
||||||
secondary: u8,
|
secondary: u8,
|
||||||
abilities: [u8; MAX_ABILITIES],
|
abilities: [u8; BASE_ABILITY_LIMIT],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tool specific tactics
|
// Tool specific tactics
|
||||||
|
@ -3,7 +3,7 @@ use common::{
|
|||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
comp::{
|
comp::{
|
||||||
inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
|
inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
|
||||||
Waypoint,
|
Waypoint, BASE_ABILITY_LIMIT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use specs::{Entity, WriteExpect};
|
use specs::{Entity, WriteExpect};
|
||||||
@ -76,7 +76,7 @@ pub fn create_character(
|
|||||||
inventory,
|
inventory,
|
||||||
waypoint,
|
waypoint,
|
||||||
pets: Vec::new(),
|
pets: Vec::new(),
|
||||||
active_abilities: Default::default(),
|
active_abilities: common::comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT),
|
||||||
map_marker,
|
map_marker,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -925,19 +925,17 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
|
|||||||
let to = pos + dir * power;
|
let to = pos + dir * power;
|
||||||
let _ = terrain
|
let _ = terrain
|
||||||
.ray(from, to)
|
.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:
|
// Stop if:
|
||||||
// 1) Block is liquid
|
// 1) Block is liquid
|
||||||
// 2) Consumed all energy
|
// 2) Consumed all energy
|
||||||
// 3) Can't explode block (for example we hit stone wall)
|
// 3) Can't explode block (for example we hit stone wall)
|
||||||
let stop = block.is_liquid()
|
block.is_liquid()
|
||||||
|| block.explode_power().is_none()
|
|| block.explode_power().is_none()
|
||||||
|| ray_energy <= 0.0;
|
|| ray_energy <= 0.0
|
||||||
|
|
||||||
ray_energy -=
|
|
||||||
block.explode_power().unwrap_or(0.0) + rng.gen::<f32>() * 0.1;
|
|
||||||
|
|
||||||
stop
|
|
||||||
})
|
})
|
||||||
.for_each(|block: &Block, pos| {
|
.for_each(|block: &Block, pos| {
|
||||||
if block.explode_power().is_some() {
|
if block.explode_power().is_some() {
|
||||||
|
@ -438,7 +438,7 @@ pub fn handle_create_sprite(
|
|||||||
let state = server.state_mut();
|
let state = server.state_mut();
|
||||||
if state.can_set_block(pos) {
|
if state.can_set_block(pos) {
|
||||||
let block = state.terrain().get(pos).ok().copied();
|
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 old_block = block.unwrap_or_else(|| Block::air(SpriteKind::Empty));
|
||||||
let new_block = old_block.with_sprite(sprite);
|
let new_block = old_block.with_sprite(sprite);
|
||||||
state.set_block(pos, new_block);
|
state.set_block(pos, new_block);
|
||||||
|
@ -271,7 +271,7 @@ pub fn active_abilities_from_db_model(
|
|||||||
abilities,
|
abilities,
|
||||||
}| {
|
}| {
|
||||||
let mut auxiliary_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()) {
|
for (empty, ability) in auxiliary_abilities.iter_mut().zip(abilities.into_iter()) {
|
||||||
*empty = aux_ability_from_string(&ability);
|
*empty = aux_ability_from_string(&ability);
|
||||||
}
|
}
|
||||||
@ -285,7 +285,10 @@ pub fn active_abilities_from_db_model(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect::<HashMap<_, _>>();
|
.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
|
/// Struct containing item properties in the format that they get persisted to
|
||||||
|
@ -22,7 +22,7 @@ use common::{
|
|||||||
object,
|
object,
|
||||||
skills::{GeneralSkill, Skill},
|
skills::{GeneralSkill, Skill},
|
||||||
ChatType, Content, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence,
|
ChatType, Content, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence,
|
||||||
PresenceKind,
|
PresenceKind, BASE_ABILITY_LIMIT,
|
||||||
},
|
},
|
||||||
effect::Effect,
|
effect::Effect,
|
||||||
link::{Is, Link, LinkHandle},
|
link::{Is, Link, LinkHandle},
|
||||||
@ -312,7 +312,11 @@ impl StateExt for State {
|
|||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
))
|
))
|
||||||
.with(stats)
|
.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)
|
.with(skill_set)
|
||||||
.maybe_with(health)
|
.maybe_with(health)
|
||||||
.with(poise)
|
.with(poise)
|
||||||
|
@ -201,6 +201,61 @@ impl Animation for AlphaAnimation {
|
|||||||
* Quaternion::rotation_y(-1.8 + move1 * -0.4 + move2 * 3.5)
|
* Quaternion::rotation_y(-1.8 + move1 * -0.4 + move2 * 3.5)
|
||||||
* Quaternion::rotation_z(move1 * -1.0 + move2 * -1.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_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);
|
next.control_r.position = Vec3::new(1.0, 2.0, -2.0);
|
||||||
|
@ -477,10 +477,14 @@ impl SfxMgr {
|
|||||||
Outcome::SummonedCreature { pos, body, .. } => {
|
Outcome::SummonedCreature { pos, body, .. } => {
|
||||||
match body {
|
match body {
|
||||||
Body::BipedSmall(body) => match body.species {
|
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);
|
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::DeepLaugh);
|
||||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
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) => {
|
Body::Object(object::Body::Flamethrower) => {
|
||||||
|
@ -23,7 +23,7 @@ use common::{
|
|||||||
combat,
|
combat,
|
||||||
comp::{
|
comp::{
|
||||||
self,
|
self,
|
||||||
ability::{Ability, ActiveAbilities, AuxiliaryAbility, MAX_ABILITIES},
|
ability::{Ability, ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT},
|
||||||
inventory::{
|
inventory::{
|
||||||
item::{
|
item::{
|
||||||
item_key::ItemKey,
|
item_key::ItemKey,
|
||||||
@ -811,12 +811,12 @@ impl<'a> Widget for Diary<'a> {
|
|||||||
state.update(|s| {
|
state.update(|s| {
|
||||||
s.ids
|
s.ids
|
||||||
.active_abilities
|
.active_abilities
|
||||||
.resize(MAX_ABILITIES, &mut ui.widget_id_generator())
|
.resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator())
|
||||||
});
|
});
|
||||||
state.update(|s| {
|
state.update(|s| {
|
||||||
s.ids
|
s.ids
|
||||||
.active_abilities_keys
|
.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 {
|
let mut slot_maker = SlotMaker {
|
||||||
@ -844,7 +844,7 @@ impl<'a> Widget for Diary<'a> {
|
|||||||
pulse: 0.0,
|
pulse: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
for i in 0..MAX_ABILITIES {
|
for i in 0..BASE_ABILITY_LIMIT {
|
||||||
let ability_id = self
|
let ability_id = self
|
||||||
.active_abilities
|
.active_abilities
|
||||||
.get_ability(
|
.get_ability(
|
||||||
|
@ -98,6 +98,7 @@ pub enum ParticleMode {
|
|||||||
SnowStorm = 44,
|
SnowStorm = 44,
|
||||||
PortalFizz = 45,
|
PortalFizz = 45,
|
||||||
Ink = 46,
|
Ink = 46,
|
||||||
|
Whirlwind = 47,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParticleMode {
|
impl ParticleMode {
|
||||||
|
@ -11,8 +11,11 @@ use crate::{
|
|||||||
use common::{
|
use common::{
|
||||||
assets::{AssetExt, DotVoxAsset},
|
assets::{AssetExt, DotVoxAsset},
|
||||||
comp::{
|
comp::{
|
||||||
self, aura, beam, body, buff, item::Reagent, object, shockwave, BeamSegment, Body,
|
self, aura, beam, body, buff,
|
||||||
CharacterState, Ori, Pos, Scale, Shockwave, Vel,
|
item::Reagent,
|
||||||
|
object,
|
||||||
|
shockwave::{self, ShockwaveDodgeable},
|
||||||
|
BeamSegment, Body, CharacterState, Ori, Pos, Scale, Shockwave, Vel,
|
||||||
},
|
},
|
||||||
figure::Segment,
|
figure::Segment,
|
||||||
outcome::Outcome,
|
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;
|
let new_particle_count = particles_per_length * heartbeats as usize;
|
||||||
self.particles.reserve(new_particle_count);
|
self.particles.reserve(new_particle_count);
|
||||||
// higher wave when wave doesn't require ground
|
// 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
|
0.5
|
||||||
} else {
|
} else {
|
||||||
8.0
|
8.0
|
||||||
|
Loading…
Reference in New Issue
Block a user