diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f8279eda9..e61c075bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Mutliple singleplayer worlds and map generation UI. - New arena building in desert cities, suitable for PVP, also NPCs like to watch the fights too - The loading screen now displays status updates for singleplayer server and client initialization progress +- New Frost Gigas attacks & AI ### Changed @@ -54,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated windowing library, wayland may work better. - Portal model has been updated by @Nectical - Chat command responses sent by the server can now be localized +- Frost Gigas spawns in cold areas (but isn't forced to stay there) +- The ability limit for non-humanoids has been removed ### Removed - Medium and large potions from all loot tables diff --git a/assets/common/abilities/ability_set_manifest.ron b/assets/common/abilities/ability_set_manifest.ron index ed9e0f2547..5f2fbf0812 100644 --- a/assets/common/abilities/ability_set_manifest.ron +++ b/assets/common/abilities/ability_set_manifest.ron @@ -783,6 +783,9 @@ Simple(None, "common.abilities.custom.gigas_frost.ice_volley"), Simple(None, "common.abilities.custom.gigas_frost.frost_summons"), Simple(None, "common.abilities.custom.gigas_frost.flashfreeze"), + Simple(None, "common.abilities.custom.gigas_frost.icespike_targeted"), + Simple(None, "common.abilities.custom.gigas_frost.bonk"), + Simple(None, "common.abilities.custom.gigas_frost.whirlwind"), ], ), Custom("Boreal Bow"): ( diff --git a/assets/common/abilities/custom/arthropods/dagonite/leapshockwave.ron b/assets/common/abilities/custom/arthropods/dagonite/leapshockwave.ron index b61faf2129..08a939780f 100644 --- a/assets/common/abilities/custom/arthropods/dagonite/leapshockwave.ron +++ b/assets/common/abilities/custom/arthropods/dagonite/leapshockwave.ron @@ -11,7 +11,7 @@ LeapShockwave( shockwave_vertical_angle: 15.0, shockwave_speed: 20.0, shockwave_duration: 0.8, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.2, damage_kind: Crushing, specifier: Steam, diff --git a/assets/common/abilities/custom/birdlargefire/fireshockwave.ron b/assets/common/abilities/custom/birdlargefire/fireshockwave.ron index 841c443ccb..9f627d5c96 100644 --- a/assets/common/abilities/custom/birdlargefire/fireshockwave.ron +++ b/assets/common/abilities/custom/birdlargefire/fireshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 20.0, shockwave_duration: 0.5, - requires_ground: false, + dodgeable: Roll, move_efficiency: 0.1, damage_kind: Energy, specifier: Fire, diff --git a/assets/common/abilities/custom/boreal_warrior/bow/charged.ron b/assets/common/abilities/custom/boreal_warrior/bow/charged.ron index fffe006cdd..ca41f8882e 100644 --- a/assets/common/abilities/custom/boreal_warrior/bow/charged.ron +++ b/assets/common/abilities/custom/boreal_warrior/bow/charged.ron @@ -17,7 +17,7 @@ ChargedRanged( damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.1), + strength: Value(0.3), chance: 1.0, ))), move_speed: 0.6, diff --git a/assets/common/abilities/custom/boreal_warrior/bow/repeater.ron b/assets/common/abilities/custom/boreal_warrior/bow/repeater.ron index 07cb6d5b8d..093c131dc8 100644 --- a/assets/common/abilities/custom/boreal_warrior/bow/repeater.ron +++ b/assets/common/abilities/custom/boreal_warrior/bow/repeater.ron @@ -16,7 +16,7 @@ RepeaterRanged( damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.1), + strength: Value(0.3), chance: 1.0, ))), ) \ No newline at end of file diff --git a/assets/common/abilities/custom/boreal_warrior/bow/shotgun.ron b/assets/common/abilities/custom/boreal_warrior/bow/shotgun.ron index f40e0ea066..fe720e665e 100644 --- a/assets/common/abilities/custom/boreal_warrior/bow/shotgun.ron +++ b/assets/common/abilities/custom/boreal_warrior/bow/shotgun.ron @@ -15,7 +15,7 @@ BasicRanged( damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.1), + strength: Value(0.3), chance: 1.0, ))), move_efficiency: 0.3, diff --git a/assets/common/abilities/custom/boreal_warrior/hammer/dash.ron b/assets/common/abilities/custom/boreal_warrior/hammer/dash.ron index 83c72f1ba1..14adbbd4f8 100644 --- a/assets/common/abilities/custom/boreal_warrior/hammer/dash.ron +++ b/assets/common/abilities/custom/boreal_warrior/hammer/dash.ron @@ -18,8 +18,8 @@ DashMelee( damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.1), - chance: 1.0, + strength: Value(0.3), + chance: 0.5, ))), ), energy_drain: 0, diff --git a/assets/common/abilities/custom/boreal_warrior/hammer/leap.ron b/assets/common/abilities/custom/boreal_warrior/hammer/leap.ron index cffca268b7..78260c4586 100644 --- a/assets/common/abilities/custom/boreal_warrior/hammer/leap.ron +++ b/assets/common/abilities/custom/boreal_warrior/hammer/leap.ron @@ -17,7 +17,7 @@ LeapMelee( damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.1), + strength: Value(0.3), chance: 1.0, ))), ), diff --git a/assets/common/abilities/custom/boreal_warrior/hammer/singlestrike.ron b/assets/common/abilities/custom/boreal_warrior/hammer/singlestrike.ron index 3cc7a58275..cf12b4631a 100644 --- a/assets/common/abilities/custom/boreal_warrior/hammer/singlestrike.ron +++ b/assets/common/abilities/custom/boreal_warrior/hammer/singlestrike.ron @@ -17,8 +17,8 @@ ComboMelee( damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.1), - chance: 1.0, + strength: Value(0.3), + chance: 0.4, ))), )], initial_energy_gain: 5.0, diff --git a/assets/common/abilities/custom/cardinal/summondagonites.ron b/assets/common/abilities/custom/cardinal/summondagonites.ron index 0ee53972b0..2bebe26674 100644 --- a/assets/common/abilities/custom/cardinal/summondagonites.ron +++ b/assets/common/abilities/custom/cardinal/summondagonites.ron @@ -10,6 +10,7 @@ BasicSummon( body_type: Male, )), scale: None, + use_npc_name: true, has_health: true, loadout_config: None, skillset_config: Some(Rank3), diff --git a/assets/common/abilities/custom/claygolem/shockwave.ron b/assets/common/abilities/custom/claygolem/shockwave.ron index 63cb59b4dc..b6ff6a5f7c 100644 --- a/assets/common/abilities/custom/claygolem/shockwave.ron +++ b/assets/common/abilities/custom/claygolem/shockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 3.5, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Ground, diff --git a/assets/common/abilities/custom/cloudwyvern/lightningshockwave.ron b/assets/common/abilities/custom/cloudwyvern/lightningshockwave.ron index 52eaab4ac7..f50fc14f98 100644 --- a/assets/common/abilities/custom/cloudwyvern/lightningshockwave.ron +++ b/assets/common/abilities/custom/cloudwyvern/lightningshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Lightning, diff --git a/assets/common/abilities/custom/coralgolem/shockwave.ron b/assets/common/abilities/custom/coralgolem/shockwave.ron index 3803060335..02979caa9c 100644 --- a/assets/common/abilities/custom/coralgolem/shockwave.ron +++ b/assets/common/abilities/custom/coralgolem/shockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 25.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Water, diff --git a/assets/common/abilities/custom/cyclops/hammer_shockwave.ron b/assets/common/abilities/custom/cyclops/hammer_shockwave.ron index 68492262ac..ffd8ed3724 100644 --- a/assets/common/abilities/custom/cyclops/hammer_shockwave.ron +++ b/assets/common/abilities/custom/cyclops/hammer_shockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 360.0, shockwave_speed: 40.0, shockwave_duration: 0.4, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Piercing, specifier: Ground, diff --git a/assets/common/abilities/custom/dagon/steamwave.ron b/assets/common/abilities/custom/dagon/steamwave.ron index b4160242be..acf6cb6b39 100644 --- a/assets/common/abilities/custom/dagon/steamwave.ron +++ b/assets/common/abilities/custom/dagon/steamwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Steam, diff --git a/assets/common/abilities/custom/dwarves/flamekeeper/lavawave.ron b/assets/common/abilities/custom/dwarves/flamekeeper/lavawave.ron index 40505ff2e9..d63306dd5e 100644 --- a/assets/common/abilities/custom/dwarves/flamekeeper/lavawave.ron +++ b/assets/common/abilities/custom/dwarves/flamekeeper/lavawave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 3.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Fire, diff --git a/assets/common/abilities/custom/dwarves/flamekeeper/summon_clockwork.ron b/assets/common/abilities/custom/dwarves/flamekeeper/summon_clockwork.ron index 6c9b628968..4990690a81 100644 --- a/assets/common/abilities/custom/dwarves/flamekeeper/summon_clockwork.ron +++ b/assets/common/abilities/custom/dwarves/flamekeeper/summon_clockwork.ron @@ -10,6 +10,7 @@ BasicSummon( body_type: Male, )), scale: None, + use_npc_name: true, has_health: true, loadout_config: Some(ClockworkSummon), skillset_config: None, diff --git a/assets/common/abilities/custom/dwarves/flamekeeper/summon_flamethrower.ron b/assets/common/abilities/custom/dwarves/flamekeeper/summon_flamethrower.ron index b2231f4b6d..832d9ca4e6 100644 --- a/assets/common/abilities/custom/dwarves/flamekeeper/summon_flamethrower.ron +++ b/assets/common/abilities/custom/dwarves/flamekeeper/summon_flamethrower.ron @@ -8,6 +8,7 @@ BasicSummon( body: Object(Flamethrower), scale: None, has_health: true, + use_npc_name: true, loadout_config: None, skillset_config: None, ), diff --git a/assets/common/abilities/custom/dwarves/hermit_alligator/wave.ron b/assets/common/abilities/custom/dwarves/hermit_alligator/wave.ron index e7e3c02e4f..76cf6fc4b8 100644 --- a/assets/common/abilities/custom/dwarves/hermit_alligator/wave.ron +++ b/assets/common/abilities/custom/dwarves/hermit_alligator/wave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 3.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Water, diff --git a/assets/common/abilities/custom/flamewyvern/fireshockwave.ron b/assets/common/abilities/custom/flamewyvern/fireshockwave.ron index bb4ad6fd3e..3ef4ef8934 100644 --- a/assets/common/abilities/custom/flamewyvern/fireshockwave.ron +++ b/assets/common/abilities/custom/flamewyvern/fireshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Fire, diff --git a/assets/common/abilities/custom/frostwyvern/iceshockwave.ron b/assets/common/abilities/custom/frostwyvern/iceshockwave.ron index a4c1f8162c..277ab59e64 100644 --- a/assets/common/abilities/custom/frostwyvern/iceshockwave.ron +++ b/assets/common/abilities/custom/frostwyvern/iceshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Ice, diff --git a/assets/common/abilities/custom/gigas_frost/bonk.ron b/assets/common/abilities/custom/gigas_frost/bonk.ron new file mode 100644 index 0000000000..5d478e6d5f --- /dev/null +++ b/assets/common/abilities/custom/gigas_frost/bonk.ron @@ -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, +) diff --git a/assets/common/abilities/custom/gigas_frost/cleave.ron b/assets/common/abilities/custom/gigas_frost/cleave.ron index 95b284c8b9..14725893fa 100644 --- a/assets/common/abilities/custom/gigas_frost/cleave.ron +++ b/assets/common/abilities/custom/gigas_frost/cleave.ron @@ -1,21 +1,21 @@ BasicMelee( energy_cost: 0, - buildup_duration: 0.9, + buildup_duration: 0.5, swing_duration: 0.1, recover_duration: 0.7, melee_constructor: ( kind: Slash( - damage: 85.0, + damage: 60.0, poise: 5.0, knockback: 5.0, energy_regen: 10.0, ), - range: 5.0, + range: 7.0, angle: 75.0, damage_effect: Some(Buff(( kind: Frozen, dur_secs: 1.0, - strength: DamageFraction(0.1), + strength: Value(0.5), chance: 0.3, ))), multi_target: Some(Normal), diff --git a/assets/common/abilities/custom/gigas_frost/flashfreeze.ron b/assets/common/abilities/custom/gigas_frost/flashfreeze.ron index b383ebb674..db863fd963 100644 --- a/assets/common/abilities/custom/gigas_frost/flashfreeze.ron +++ b/assets/common/abilities/custom/gigas_frost/flashfreeze.ron @@ -1,24 +1,24 @@ Shockwave( energy_cost: 0, - buildup_duration: 2.0, + buildup_duration: 1.8, swing_duration: 0.12, recover_duration: 1.5, - damage: 45.0, + damage: 50.0, poise_damage: 30, knockback: (strength: 0.0, direction: TowardsUp), - shockwave_angle: 240.0, + shockwave_angle: 220.0, shockwave_vertical_angle: 360.0, shockwave_speed: 200.0, shockwave_duration: 0.15, - requires_ground: false, - move_efficiency: 0.0, + dodgeable: No, + move_efficiency: 0.2, damage_kind: Piercing, specifier: IceSpikes, - ori_rate: 0.0, + ori_rate: 0.1, damage_effect: Some(Buff(( kind: Frozen, dur_secs: 2.0, - strength: DamageFraction(0.3), + strength: Value(3.0), chance: 1.0, ))), ) \ No newline at end of file diff --git a/assets/common/abilities/custom/gigas_frost/frost_summons.ron b/assets/common/abilities/custom/gigas_frost/frost_summons.ron index c2ff3cf16d..f33c006534 100644 --- a/assets/common/abilities/custom/gigas_frost/frost_summons.ron +++ b/assets/common/abilities/custom/gigas_frost/frost_summons.ron @@ -9,6 +9,7 @@ BasicSummon( species: Boreal, body_type: Male, )), + use_npc_name: true, scale: None, has_health: true, loadout_config: Some(BorealSummon), diff --git a/assets/common/abilities/custom/gigas_frost/ice_volley.ron b/assets/common/abilities/custom/gigas_frost/ice_volley.ron index 9b4c91a1fd..24c0c41497 100644 --- a/assets/common/abilities/custom/gigas_frost/ice_volley.ron +++ b/assets/common/abilities/custom/gigas_frost/ice_volley.ron @@ -9,8 +9,8 @@ BasicRanged( min_falloff: 0.1, ), projectile_body: Object(IceBomb), - projectile_speed: 25.0, + projectile_speed: 40.0, num_projectiles: 5, - projectile_spread: 0.07, + projectile_spread: 0.05, move_efficiency: 0.3, ) \ No newline at end of file diff --git a/assets/common/abilities/custom/gigas_frost/icespike_smash.ron b/assets/common/abilities/custom/gigas_frost/icespike_smash.ron index 69047e8275..7578a7c6f9 100644 --- a/assets/common/abilities/custom/gigas_frost/icespike_smash.ron +++ b/assets/common/abilities/custom/gigas_frost/icespike_smash.ron @@ -3,8 +3,8 @@ SpriteSummon( cast_duration: 0.1, recover_duration: 1.1, sprite: IceSpike, - del_timeout: Some((2, 5)), - summon_distance: (2, 12), - sparseness: 0.95, + del_timeout: Some((5, 15)), + summon_distance: (2, 18), + sparseness: 0.96, angle: 360, ) \ No newline at end of file diff --git a/assets/common/abilities/custom/gigas_frost/icespike_targeted.ron b/assets/common/abilities/custom/gigas_frost/icespike_targeted.ron new file mode 100644 index 0000000000..1e78d17886 --- /dev/null +++ b/assets/common/abilities/custom/gigas_frost/icespike_targeted.ron @@ -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, +) \ No newline at end of file diff --git a/assets/common/abilities/custom/gigas_frost/leapshockwave.ron b/assets/common/abilities/custom/gigas_frost/leapshockwave.ron index 40ea424050..f2e02c34a9 100644 --- a/assets/common/abilities/custom/gigas_frost/leapshockwave.ron +++ b/assets/common/abilities/custom/gigas_frost/leapshockwave.ron @@ -9,16 +9,16 @@ LeapShockwave( knockback: (strength: 3.0, direction: Up), shockwave_angle: 360.0, shockwave_vertical_angle: 15.0, - shockwave_speed: 20.0, - shockwave_duration: 0.8, - requires_ground: true, + shockwave_speed: 30.0, + shockwave_duration: 1.2, + dodgeable: Jump, move_efficiency: 0.2, damage_kind: Piercing, specifier: IceSpikes, damage_effect: Some(Buff(( kind: Frozen, dur_secs: 1.0, - strength: DamageFraction(0.1), + strength: Value(1.2), chance: 1.0, ))), forward_leap_strength: 45.0, diff --git a/assets/common/abilities/custom/gigas_frost/whirlwind.ron b/assets/common/abilities/custom/gigas_frost/whirlwind.ron new file mode 100644 index 0000000000..653fb89a17 --- /dev/null +++ b/assets/common/abilities/custom/gigas_frost/whirlwind.ron @@ -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), +) diff --git a/assets/common/abilities/custom/gigas_frost/wide_cleave.ron b/assets/common/abilities/custom/gigas_frost/wide_cleave.ron index 50f1da813c..8164e0d7c9 100644 --- a/assets/common/abilities/custom/gigas_frost/wide_cleave.ron +++ b/assets/common/abilities/custom/gigas_frost/wide_cleave.ron @@ -5,17 +5,17 @@ BasicMelee( recover_duration: 0.8, melee_constructor: ( kind: Slash( - damage: 90.0, + damage: 70.0, poise: 20.0, knockback: 5.0, energy_regen: 5.0, ), - range: 5.0, + range: 7.0, angle: 120.0, damage_effect: Some(Buff(( kind: Frozen, dur_secs: 1.0, - strength: DamageFraction(0.1), + strength: Value(0.5), chance: 0.5, ))), multi_target: Some(Normal), diff --git a/assets/common/abilities/custom/mindflayer/summonminions.ron b/assets/common/abilities/custom/mindflayer/summonminions.ron index af48510a48..f9272fbe43 100644 --- a/assets/common/abilities/custom/mindflayer/summonminions.ron +++ b/assets/common/abilities/custom/mindflayer/summonminions.ron @@ -9,6 +9,7 @@ BasicSummon( species: Husk, body_type: Male, )), + use_npc_name: true, scale: None, has_health: true, loadout_config: Some(HuskSummon), diff --git a/assets/common/abilities/custom/roshwalr/freezeshockwave.ron b/assets/common/abilities/custom/roshwalr/freezeshockwave.ron index a93c2f5efc..b9e9feaa52 100644 --- a/assets/common/abilities/custom/roshwalr/freezeshockwave.ron +++ b/assets/common/abilities/custom/roshwalr/freezeshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 15.0, shockwave_speed: 20.0, shockwave_duration: 0.8, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.2, damage_kind: Piercing, specifier: IceSpikes, diff --git a/assets/common/abilities/custom/seawyvern/inkshockwave.ron b/assets/common/abilities/custom/seawyvern/inkshockwave.ron index 903c5e90c4..4a7ecf8ce1 100644 --- a/assets/common/abilities/custom/seawyvern/inkshockwave.ron +++ b/assets/common/abilities/custom/seawyvern/inkshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Ink, diff --git a/assets/common/abilities/custom/stonegolemfist/shockwave.ron b/assets/common/abilities/custom/stonegolemfist/shockwave.ron index 8c5ce39340..52fe37b10c 100644 --- a/assets/common/abilities/custom/stonegolemfist/shockwave.ron +++ b/assets/common/abilities/custom/stonegolemfist/shockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 65.0, shockwave_duration: 1.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.05, damage_kind: Crushing, specifier: Ground, diff --git a/assets/common/abilities/custom/tidalwarrior/totem_wave.ron b/assets/common/abilities/custom/tidalwarrior/totem_wave.ron index edc56d4ead..81cc0a58ac 100644 --- a/assets/common/abilities/custom/tidalwarrior/totem_wave.ron +++ b/assets/common/abilities/custom/tidalwarrior/totem_wave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 30.0, shockwave_speed: 10.0, shockwave_duration: 5.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Water, diff --git a/assets/common/abilities/custom/treant_sapling/shockwave.ron b/assets/common/abilities/custom/treant_sapling/shockwave.ron index f2d70d1e5d..2a291d00fd 100644 --- a/assets/common/abilities/custom/treant_sapling/shockwave.ron +++ b/assets/common/abilities/custom/treant_sapling/shockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 20.0, shockwave_duration: 0.5, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.1, damage_kind: Crushing, specifier: Ground, diff --git a/assets/common/abilities/custom/wealdwyvern/poisonshockwave.ron b/assets/common/abilities/custom/wealdwyvern/poisonshockwave.ron index 26a148df16..08e6a7ec55 100644 --- a/assets/common/abilities/custom/wealdwyvern/poisonshockwave.ron +++ b/assets/common/abilities/custom/wealdwyvern/poisonshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Poison, diff --git a/assets/common/abilities/custom/woodgolem/shockwave.ron b/assets/common/abilities/custom/woodgolem/shockwave.ron index 664d6dec12..414f1db81e 100644 --- a/assets/common/abilities/custom/woodgolem/shockwave.ron +++ b/assets/common/abilities/custom/woodgolem/shockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 15.0, shockwave_duration: 2.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.0, damage_kind: Crushing, specifier: Ground, diff --git a/assets/common/abilities/custom/yeti/icespikes.ron b/assets/common/abilities/custom/yeti/icespikes.ron index 11baff03c9..b70e2a7550 100644 --- a/assets/common/abilities/custom/yeti/icespikes.ron +++ b/assets/common/abilities/custom/yeti/icespikes.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 15.0, shockwave_speed: 15.0, shockwave_duration: 3.0, - requires_ground: true, + dodgeable: Jump, move_efficiency: 0.2, damage_kind: Piercing, specifier: IceSpikes, diff --git a/assets/common/abilities/gnarling/chieftain/fireshockwave.ron b/assets/common/abilities/gnarling/chieftain/fireshockwave.ron index aa7f8be86e..81bb67dcca 100644 --- a/assets/common/abilities/gnarling/chieftain/fireshockwave.ron +++ b/assets/common/abilities/gnarling/chieftain/fireshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90, shockwave_speed: 10, shockwave_duration: 1, - requires_ground: false, + dodgeable: Roll, move_efficiency: 0, damage_kind: Energy, specifier: Fire, diff --git a/assets/common/abilities/staff/fireshockwave.ron b/assets/common/abilities/staff/fireshockwave.ron index 64dcbf9506..913dc7939b 100644 --- a/assets/common/abilities/staff/fireshockwave.ron +++ b/assets/common/abilities/staff/fireshockwave.ron @@ -10,7 +10,7 @@ Shockwave( shockwave_vertical_angle: 90.0, shockwave_speed: 30.0, shockwave_duration: 0.5, - requires_ground: false, + dodgeable: Roll, move_efficiency: 0.1, damage_kind: Energy, specifier: Fire, diff --git a/assets/common/npc_names.ron b/assets/common/npc_names.ron index c78b18cd9f..6516ea1b79 100644 --- a/assets/common/npc_names.ron +++ b/assets/common/npc_names.ron @@ -1159,11 +1159,11 @@ ), husk: ( keyword: "husk", - generic: "Husk" + generic: "Cultist Husk" ), boreal: ( keyword: "boreal", - generic: "Boreal", + generic: "Boreal Warrior", ), bushly: ( keyword: "bushly", diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 6352fde4b9..436512dfb3 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -83,6 +83,7 @@ const int GIGA_SNOW = 42; const int CYCLOPS_CHARGE = 43; const int PORTAL_FIZZ = 45; const int INK = 46; +const int WHIRLWIND = 47; // meters per second squared (acceleration) const float earth_gravity = 9.807; @@ -703,6 +704,15 @@ void main() { spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9) ); break; + case WHIRLWIND: + f_reflect = 0.0; + attr = Attr( + spiral_motion(vec3(0, 0, 3), abs(rand0) * 3 + percent() * 20.5, percent(), -8.0 + (rand0 * 3), rand1 * 360.), + vec3((-2.5 * (1 - slow_start(0.05)))), + vec4(vec3(1.3, 1.8, 2), 1), + spin_in_axis(vec3(rand6, rand7, rand8), percent() * 10 + 3 * rand9) + ); + break; default: attr = Attr( linear_motion( diff --git a/common/src/combat.rs b/common/src/combat.rs index 37741dd927..9a968e7bd5 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -40,6 +40,7 @@ pub enum AttackSource { Beam, GroundShockwave, AirShockwave, + UndodgeableShockwave, Explosion, } @@ -948,7 +949,9 @@ impl From for DamageSource { AttackSource::Melee => DamageSource::Melee, AttackSource::Projectile => DamageSource::Projectile, AttackSource::Explosion => DamageSource::Explosion, - AttackSource::AirShockwave | AttackSource::GroundShockwave => DamageSource::Shockwave, + AttackSource::AirShockwave + | AttackSource::GroundShockwave + | AttackSource::UndodgeableShockwave => DamageSource::Shockwave, AttackSource::Beam => DamageSource::Energy, } } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 6be4eb753c..19ffd92768 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -25,6 +25,7 @@ use crate::{ resources::Secs, states::{ behavior::JoinData, + sprite_summon::SpriteSummonAnchor, utils::{AbilityInfo, ComboConsumption, ScalingKind, StageSection}, *, }, @@ -33,9 +34,11 @@ use crate::{ use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; -use std::{convert::TryFrom, time::Duration}; +use std::{borrow::Cow, convert::TryFrom, time::Duration}; -pub const MAX_ABILITIES: usize = 5; +use super::shockwave::ShockwaveDodgeable; + +pub const BASE_ABILITY_LIMIT: usize = 5; pub type AuxiliaryKey = (Option, Option); // TODO: Potentially look into storing previous ability sets for weapon @@ -48,7 +51,8 @@ pub struct ActiveAbilities { pub primary: PrimaryAbility, pub secondary: SecondaryAbility, pub movement: MovementAbility, - pub auxiliary_sets: HashMap, + pub limit: Option, + pub auxiliary_sets: HashMap>, } impl Component for ActiveAbilities { @@ -62,19 +66,35 @@ impl Default for ActiveAbilities { primary: PrimaryAbility::Tool, secondary: SecondaryAbility::Tool, movement: MovementAbility::Species, + limit: None, auxiliary_sets: HashMap::new(), } } } impl ActiveAbilities { - pub fn new(auxiliary_sets: HashMap) -> Self { + pub fn from_auxiliary( + auxiliary_sets: HashMap>, + limit: Option, + ) -> Self { + // Discard any sets that exceed the limit ActiveAbilities { - auxiliary_sets, + auxiliary_sets: auxiliary_sets + .into_iter() + .filter(|(_, set)| limit.map_or(true, |limit| set.len() == limit)) + .collect(), + limit, ..Self::default() } } + pub fn default_limited(limit: usize) -> Self { + ActiveAbilities { + limit: Some(limit), + ..Default::default() + } + } + pub fn change_ability( &mut self, slot: usize, @@ -86,7 +106,7 @@ impl ActiveAbilities { let auxiliary_set = self .auxiliary_sets .entry(auxiliary_key) - .or_insert(Self::default_ability_set(inventory, skill_set)); + .or_insert(Self::default_ability_set(inventory, skill_set, self.limit)); if let Some(ability) = auxiliary_set.get_mut(slot) { *ability = new_ability; } @@ -111,13 +131,13 @@ impl ActiveAbilities { &self, inv: Option<&Inventory>, skill_set: Option<&SkillSet>, - ) -> [AuxiliaryAbility; MAX_ABILITIES] { + ) -> Cow> { let aux_key = Self::active_auxiliary_key(inv); self.auxiliary_sets .get(&aux_key) - .copied() - .unwrap_or_else(|| Self::default_ability_set(inv, skill_set)) + .map(Cow::Borrowed) + .unwrap_or_else(|| Cow::Owned(Self::default_ability_set(inv, skill_set, self.limit))) } pub fn get_ability( @@ -311,7 +331,8 @@ impl ActiveAbilities { fn default_ability_set<'a>( inv: Option<&'a Inventory>, skill_set: Option<&'a SkillSet>, - ) -> [AuxiliaryAbility; MAX_ABILITIES] { + limit: Option, + ) -> Vec { let mut iter = Self::iter_available_abilities(inv, skill_set, EquipSlot::ActiveMainhand) .map(AuxiliaryAbility::MainWeapon) .chain( @@ -319,7 +340,13 @@ impl ActiveAbilities { .map(AuxiliaryAbility::OffWeapon), ); - [(); MAX_ABILITIES].map(|()| iter.next().unwrap_or(AuxiliaryAbility::Empty)) + if let Some(limit) = limit { + (0..limit) + .map(|_| iter.next().unwrap_or(AuxiliaryAbility::Empty)) + .collect() + } else { + iter.collect() + } } } @@ -790,7 +817,7 @@ pub enum CharacterAbility { shockwave_vertical_angle: f32, shockwave_speed: f32, shockwave_duration: f32, - requires_ground: bool, + dodgeable: ShockwaveDodgeable, move_efficiency: f32, damage_kind: DamageKind, specifier: comp::shockwave::FrontendSpecifier, @@ -863,7 +890,7 @@ pub enum CharacterAbility { shockwave_vertical_angle: f32, shockwave_speed: f32, shockwave_duration: f32, - requires_ground: bool, + dodgeable: ShockwaveDodgeable, move_efficiency: f32, damage_kind: DamageKind, specifier: comp::shockwave::FrontendSpecifier, @@ -944,6 +971,10 @@ pub enum CharacterAbility { sparseness: f64, angle: f32, #[serde(default)] + anchor: SpriteSummonAnchor, + #[serde(default)] + move_efficiency: f32, + #[serde(default)] meta: AbilityMeta, }, Music { @@ -1348,7 +1379,7 @@ impl CharacterAbility { shockwave_vertical_angle: _, shockwave_speed: _, ref mut shockwave_duration, - requires_ground: _, + dodgeable: _, move_efficiency: _, damage_kind: _, specifier: _, @@ -1467,7 +1498,7 @@ impl CharacterAbility { shockwave_vertical_angle: _, shockwave_speed: _, ref mut shockwave_duration, - requires_ground: _, + dodgeable: _, move_efficiency: _, damage_kind: _, specifier: _, @@ -1592,6 +1623,8 @@ impl CharacterAbility { summon_distance: (ref mut inner_dist, ref mut outer_dist), sparseness: _, angle: _, + anchor: _, + move_efficiency: _, meta: _, } => { // TODO: Figure out how/if power should affect this @@ -2462,7 +2495,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { shockwave_vertical_angle, shockwave_speed, shockwave_duration, - requires_ground, + dodgeable, move_efficiency, damage_kind, specifier, @@ -2483,7 +2516,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { shockwave_vertical_angle: *shockwave_vertical_angle, shockwave_speed: *shockwave_speed, shockwave_duration: Duration::from_secs_f32(*shockwave_duration), - requires_ground: *requires_ground, + dodgeable: *dodgeable, move_efficiency: *move_efficiency, damage_kind: *damage_kind, specifier: *specifier, @@ -2654,7 +2687,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { shockwave_vertical_angle, shockwave_speed, shockwave_duration, - requires_ground, + dodgeable, move_efficiency, damage_kind, specifier, @@ -2673,7 +2706,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { shockwave_vertical_angle: *shockwave_vertical_angle, shockwave_speed: *shockwave_speed, shockwave_duration: Duration::from_secs_f32(*shockwave_duration), - requires_ground: *requires_ground, + dodgeable: *dodgeable, move_efficiency: *move_efficiency, damage_effect: *damage_effect, ability_info, @@ -2821,6 +2854,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { summon_distance, sparseness, angle, + anchor, + move_efficiency, meta: _, } => CharacterState::SpriteSummon(sprite_summon::Data { static_data: sprite_summon::StaticData { @@ -2832,6 +2867,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { summon_distance: *summon_distance, sparseness: *sparseness, angle: *angle, + anchor: *anchor, + move_efficiency: *move_efficiency, ability_info, }, timer: Duration::default(), diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 477105224d..1be191394f 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - arthropod, biped_small, bird_medium, humanoid, quadruped_low, quadruped_medium, - quadruped_small, ship, Body, UtteranceKind, + arthropod, biped_large, biped_small, bird_medium, humanoid, quadruped_low, + quadruped_medium, quadruped_small, ship, Body, UtteranceKind, }, path::Chaser, rtsim::{NpcInput, RtSimController}, @@ -383,6 +383,10 @@ impl<'a> From<&'a Body> for Psyche { }, sight_dist: match body { Body::BirdLarge(_) => 250.0, + Body::BipedLarge(biped_large) => match biped_large.species { + biped_large::Species::Gigasfrost => 200.0, + _ => 100.0, + }, _ => 40.0, }, listen_dist: 30.0, diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index a3fad51819..544daa2609 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -869,7 +869,7 @@ impl Body { biped_large::Species::Huskbrute => 800, biped_large::Species::Cultistwarlord => 250, biped_large::Species::Cultistwarlock => 250, - biped_large::Species::Gigasfrost => 20000, + biped_large::Species::Gigasfrost => 30000, biped_large::Species::AdletElder => 1500, biped_large::Species::Tursus => 300, biped_large::Species::SeaBishop => 550, @@ -1095,6 +1095,7 @@ impl Body { Body::BipedLarge(biped_large) => match biped_large.species { biped_large::Species::Mindflayer => 320, biped_large::Species::Minotaur => 280, + biped_large::Species::Gigasfrost => 800, _ => 250, }, Body::BipedSmall(b) => match b.species { diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs index d416b05837..91bdbbe8a9 100644 --- a/common/src/comp/buff.rs +++ b/common/src/comp/buff.rs @@ -156,7 +156,7 @@ pub enum BuffKind { /// Results from drinking a potion. /// Decreases the health gained from subsequent potions. PotionSickness, - // Changed into another body. + /// Changed into another body. Polymorphed(Body), } @@ -428,7 +428,10 @@ pub struct BuffData { pub strength: f32, pub duration: Option, pub delay: Option, - // 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, } @@ -437,6 +440,7 @@ impl BuffData { Self { strength, duration, + force_immediate: false, delay: None, secondary_duration: None, } @@ -451,6 +455,12 @@ impl BuffData { self.secondary_duration = Some(sec_dur); self } + + /// Force the buff effects to be applied each tick, ignoring num_ticks + pub fn with_force_immediate(mut self, force_immediate: bool) -> Self { + self.force_immediate = force_immediate; + self + } } /// De/buff category ID. diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 48482d123b..a368210804 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -920,16 +920,10 @@ impl CharacterState { AttackSource::Projectile }) }, - CharacterState::Shockwave(data) => Some(if data.static_data.requires_ground { - AttackSource::GroundShockwave - } else { - AttackSource::AirShockwave - }), - CharacterState::LeapShockwave(data) => Some(if data.static_data.requires_ground { - AttackSource::GroundShockwave - } else { - AttackSource::AirShockwave - }), + CharacterState::Shockwave(data) => Some(data.static_data.dodgeable.to_attack_source()), + CharacterState::LeapShockwave(data) => { + Some(data.static_data.dodgeable.to_attack_source()) + }, CharacterState::BasicBeam(_) => Some(AttackSource::Beam), CharacterState::BasicAura(_) => None, CharacterState::Blink(_) => None, @@ -974,6 +968,7 @@ impl AttackFilters { AttackSource::Beam => self.beams, AttackSource::GroundShockwave => self.ground_shockwaves, AttackSource::AirShockwave => self.air_shockwaves, + AttackSource::UndodgeableShockwave => false, AttackSource::Explosion => self.explosions, } } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index bed901532f..fac2ac29e8 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -41,7 +41,7 @@ pub mod visual; pub use self::{ ability::{ Ability, AbilityInput, ActiveAbilities, CharacterAbility, CharacterAbilityType, Stance, - MAX_ABILITIES, + BASE_ABILITY_LIMIT, }, admin::{Admin, AdminRole}, agent::{ diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs index c77843b8c5..c37e77e257 100644 --- a/common/src/comp/projectile.rs +++ b/common/src/comp/projectile.rs @@ -10,6 +10,7 @@ use crate::{ uid::Uid, Explosion, RadiusEffect, }; +use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use specs::Component; use std::time::Duration; @@ -761,10 +762,18 @@ impl ProjectileConstructor { .with_crit(crit_chance, crit_mult) .with_effect(knockback) .with_effect(buff); + let variation = thread_rng().gen::(); let explosion = Explosion { effects: vec![ RadiusEffect::Attack(attack), - RadiusEffect::TerrainDestruction(30.0, Rgb::new(0.0, 191.0, 255.0)), + RadiusEffect::TerrainDestruction( + 30.0, + Rgb::new( + 83.0 - (20.0 * variation), + 212.0 - (52.0 * variation), + 255.0 - (62.0 * variation), + ), + ), ], radius, reagent: Some(Reagent::White), diff --git a/common/src/comp/shockwave.rs b/common/src/comp/shockwave.rs index 9b5bb0d228..f206c69181 100644 --- a/common/src/comp/shockwave.rs +++ b/common/src/comp/shockwave.rs @@ -1,15 +1,25 @@ -use crate::{combat::Attack, uid::Uid}; +use crate::{ + combat::{Attack, AttackSource}, + uid::Uid, +}; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; use std::time::Duration; +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum ShockwaveDodgeable { + Roll, + Jump, + No, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Properties { pub angle: f32, pub vertical_angle: f32, pub speed: f32, pub attack: Attack, - pub requires_ground: bool, + pub dodgeable: ShockwaveDodgeable, pub duration: Duration, pub owner: Option, pub specifier: FrontendSpecifier, @@ -56,3 +66,13 @@ pub enum FrontendSpecifier { Ink, Lightning, } + +impl ShockwaveDodgeable { + pub fn to_attack_source(&self) -> AttackSource { + match self { + Self::Roll => AttackSource::AirShockwave, + Self::Jump => AttackSource::GroundShockwave, + Self::No => AttackSource::UndodgeableShockwave, + } + } +} diff --git a/common/src/path.rs b/common/src/path.rs index 1e967f123e..e3fedc0722 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -460,7 +460,7 @@ impl Chaser { // has been determined, so we start sampling terrain. // Check for falling off walls and try moving straight // towards the target if falling is not a danger - let walking_towards_edge = (-3..2).all(|z| { + let walking_towards_edge = (-8..2).all(|z| { vol.get( (pos + Vec3::::from(tgt_dir) * 2.5).map(|e| e as i32) + Vec3::unit_z() * z, diff --git a/common/src/ray.rs b/common/src/ray.rs index 6f795bd595..51b81de9bd 100644 --- a/common/src/ray.rs +++ b/common/src/ray.rs @@ -8,6 +8,7 @@ pub struct Ray<'a, V: ReadVol, F: FnMut(&V::Vox) -> bool, G: RayForEach> from: Vec3, to: Vec3, until: F, + is_while: bool, for_each: Option, max_iter: usize, ignore_error: bool, @@ -25,6 +26,7 @@ where from, to, until, + is_while: false, for_each: None, max_iter: 100, ignore_error: false, @@ -37,6 +39,20 @@ where from: self.from, to: self.to, until: f, + is_while: false, + for_each: self.for_each, + max_iter: self.max_iter, + ignore_error: self.ignore_error, + } + } + + pub fn while_ bool>(self, f: H) -> Ray<'a, V, H, G> { + Ray { + vol: self.vol, + from: self.from, + to: self.to, + until: f, + is_while: true, for_each: self.for_each, max_iter: self.max_iter, ignore_error: self.ignore_error, @@ -49,6 +65,7 @@ where vol: self.vol, from: self.from, to: self.to, + is_while: self.is_while, until: self.until, max_iter: self.max_iter, ignore_error: self.ignore_error, @@ -86,17 +103,28 @@ where let vox = self.vol.get(ipos); - // for_each - if let Some(g) = &mut self.for_each { - if let Ok(vox) = vox { + if self.is_while { + let vox = match vox.map(|vox| (vox, (self.until)(vox))) { + Ok((vox, true)) => return (dist, Ok(Some(vox))), + Ok((vox, _)) => Some(vox), + Err(err) if !self.ignore_error => return (dist, Err(err)), + _ => None, + }; + + if let Some((vox, g)) = vox.zip(self.for_each.as_mut()) { + g(vox, ipos); + } + } else { + // for_each + if let Some((vox, g)) = vox.as_ref().ok().zip(self.for_each.as_mut()) { g(vox, ipos); } - } - match vox.map(|vox| (vox, (self.until)(vox))) { - Ok((vox, true)) => return (dist, Ok(Some(vox))), - Err(err) if !self.ignore_error => return (dist, Err(err)), - _ => {}, + match vox.map(|vox| (vox, (self.until)(vox))) { + Ok((vox, true)) => return (dist, Ok(Some(vox))), + Err(err) if !self.ignore_error => return (dist, Err(err)), + _ => {}, + } } let deltas = diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index fdda7ed54d..aab980cf90 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -7,6 +7,7 @@ use crate::{ Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate, }, event::{LocalEvent, NpcBuilder, ServerEvent}, + npc::NPC_NAMES, outcome::Outcome, skillset_builder::{self, SkillSetBuilder}, states::{ @@ -111,7 +112,20 @@ impl CharacterBehavior for Data { } }; - let stats = comp::Stats::new("Summon".to_string(), body); + let stats = comp::Stats::new( + self.static_data + .summon_info + .use_npc_name + .then(|| { + let all_names = NPC_NAMES.read(); + all_names + .get_species_meta(&self.static_data.summon_info.body) + .map(|meta| meta.generic.clone()) + }) + .flatten() + .unwrap_or_else(|| "Summon".to_string()), + body, + ); let health = self.static_data.summon_info.has_health.then(|| { let health_level = skill_set @@ -248,6 +262,8 @@ pub struct SummonInfo { body: comp::Body, scale: Option, has_health: bool, + #[serde(default)] + use_npc_name: bool, // TODO: use assets for specifying skills and loadout? loadout_config: Option, skillset_config: Option, diff --git a/common/src/states/leap_shockwave.rs b/common/src/states/leap_shockwave.rs index 905b5a893e..a38af7eee1 100644 --- a/common/src/states/leap_shockwave.rs +++ b/common/src/states/leap_shockwave.rs @@ -3,13 +3,19 @@ use crate::{ Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind, DamageSource, GroupTarget, Knockback, }, - comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate}, + comp::{ + character_state::OutputEvents, + item::Reagent, + shockwave::{self, ShockwaveDodgeable}, + CharacterState, StateUpdate, + }, event::{LocalEvent, ServerEvent}, outcome::Outcome, states::{ behavior::{CharacterBehavior, JoinData}, utils::{StageSection, *}, }, + Explosion, KnockbackDir, RadiusEffect, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -39,8 +45,8 @@ pub struct StaticData { pub shockwave_speed: f32, /// How long the shockwave travels for pub shockwave_duration: Duration, - /// Whether the shockwave requires the target to be on the ground - pub requires_ground: bool, + /// If the shockwave can be dodged, and in what way + pub dodgeable: ShockwaveDodgeable, /// Movement speed efficiency pub move_efficiency: f32, /// Adds an effect onto the main damage of the attack @@ -173,7 +179,7 @@ impl CharacterBehavior for Data { speed: self.static_data.shockwave_speed, duration: self.static_data.shockwave_duration, attack, - requires_ground: self.static_data.requires_ground, + dodgeable: self.static_data.dodgeable, owner: Some(*data.uid), specifier: self.static_data.specifier, }; @@ -185,6 +191,35 @@ impl CharacterBehavior for Data { // Send local event used for frontend shenanigans match self.static_data.specifier { shockwave::FrontendSpecifier::IceSpikes => { + let damage = AttackDamage::new( + Damage { + source: DamageSource::Explosion, + kind: self.static_data.damage_kind, + value: self.static_data.damage / 2., + }, + Some(GroupTarget::OutOfGroup), + rand::random(), + ); + let attack = Attack::default().with_damage(damage).with_effect( + AttackEffect::new( + Some(GroupTarget::OutOfGroup), + CombatEffect::Knockback(Knockback { + direction: KnockbackDir::Away, + strength: 10., + }), + ), + ); + let explosion = Explosion { + effects: vec![RadiusEffect::Attack(attack)], + radius: data.body.max_radius() * 3.0, + reagent: Some(Reagent::White), + min_falloff: 0.5, + }; + output_events.emit_server(ServerEvent::Explosion { + pos: data.pos.0, + explosion, + owner: Some(*data.uid), + }); output_events.emit_local(LocalEvent::CreateOutcome( Outcome::IceSpikes { pos: data.pos.0 diff --git a/common/src/states/shockwave.rs b/common/src/states/shockwave.rs index a342fd3cde..55e8aecfb2 100644 --- a/common/src/states/shockwave.rs +++ b/common/src/states/shockwave.rs @@ -3,7 +3,11 @@ use crate::{ Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind, DamageSource, GroupTarget, Knockback, }, - comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate}, + comp::{ + character_state::OutputEvents, + shockwave::{self, ShockwaveDodgeable}, + CharacterState, StateUpdate, + }, event::{LocalEvent, ServerEvent}, outcome::Outcome, states::{ @@ -37,8 +41,8 @@ pub struct StaticData { pub shockwave_speed: f32, /// How long the shockwave travels for pub shockwave_duration: Duration, - /// Whether the shockwave requires the target to be on the ground - pub requires_ground: bool, + /// If the shockwave can be dodged, and in what way + pub dodgeable: ShockwaveDodgeable, /// Movement speed efficiency pub move_efficiency: f32, /// What key is used to press ability @@ -117,7 +121,7 @@ impl CharacterBehavior for Data { speed: self.static_data.shockwave_speed, duration: self.static_data.shockwave_duration, attack, - requires_ground: self.static_data.requires_ground, + dodgeable: self.static_data.dodgeable, owner: Some(*data.uid), specifier: self.static_data.specifier, }; diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs index ddc5ba7b8c..eacf5de16e 100644 --- a/common/src/states/spin_melee.rs +++ b/common/src/states/spin_melee.rs @@ -171,4 +171,5 @@ pub enum MovementBehavior { #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum FrontendSpecifier { CultistVortex, + Whirlwind, } diff --git a/common/src/states/sprite_summon.rs b/common/src/states/sprite_summon.rs index e0eab796d6..b25205a936 100644 --- a/common/src/states/sprite_summon.rs +++ b/common/src/states/sprite_summon.rs @@ -15,6 +15,13 @@ use serde::{Deserialize, Serialize}; use std::time::Duration; use vek::*; +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Default)] +pub enum SpriteSummonAnchor { + #[default] + Summoner, + Target, +} + /// Separated out to condense update portions of character state #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StaticData { @@ -31,10 +38,14 @@ pub struct StaticData { pub del_timeout: Option<(f32, f32)>, /// Range that sprites are created relative to the summonner pub summon_distance: (f32, f32), + /// Relative to what should the sprites be summoned? + pub anchor: SpriteSummonAnchor, /// Chance that sprite is not created on a particular square pub sparseness: f64, /// Angle of total coverage, centered on the forward-facing orientation pub angle: f32, + /// How much we can move + pub move_efficiency: f32, /// Miscellaneous information about the ability pub ability_info: AbilityInfo, } @@ -56,6 +67,17 @@ impl CharacterBehavior for Data { fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); + handle_orientation(data, &mut update, 1.0, None); + handle_move(data, &mut update, self.static_data.move_efficiency); + + let target_pos = || { + data.controller + .queued_inputs + .get(&self.static_data.ability_info.input) + .or(self.static_data.ability_info.input_attr.as_ref()) + .and_then(|input| input.select_pos) + }; + match self.stage_section { StageSection::Buildup => { if self.timer < self.static_data.buildup_duration { @@ -102,15 +124,23 @@ impl CharacterBehavior for Data { <= (self.static_data.angle / 2.0) && !thread_rng().gen_bool(self.static_data.sparseness) { + let anchor_pos = match self.static_data.anchor { + SpriteSummonAnchor::Summoner => data.pos.0, + // Use the selected target position, falling back to the + // summoner position + SpriteSummonAnchor::Target => { + target_pos().unwrap_or(data.pos.0) + }, + }; // The coordinates of where the sprite is created let sprite_pos = Vec3::new( - data.pos.0.x.floor() as i32 + point.x, - data.pos.0.y.floor() as i32 + point.y, - data.pos.0.z.floor() as i32, + anchor_pos.x.floor() as i32 + point.x, + anchor_pos.y.floor() as i32 + point.y, + anchor_pos.z.floor() as i32, ); // Check for collision in z up to 10 blocks up or down - let obstacle_z = data + let (obstacle_z, obstale_z_result) = data .terrain .ray( sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 10.0, @@ -122,11 +152,17 @@ impl CharacterBehavior for Data { Block::is_solid(b) && b.get_sprite() != Some(self.static_data.sprite) }) - .cast() - .0; + .cast(); // z height relative to caster - let z = sprite_pos.z + (10.5 - obstacle_z).ceil() as i32; + let z = sprite_pos.z + + if let (SpriteSummonAnchor::Target, Ok(None)) = + (&self.static_data.anchor, obstale_z_result) + { + 0 + } else { + (10.5 - obstacle_z).ceil() as i32 + }; // Location sprite will be created let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z); @@ -154,8 +190,13 @@ impl CharacterBehavior for Data { }); // Send local event used for frontend shenanigans if self.static_data.sprite == SpriteKind::IceSpike { + let summoner_pos = + data.pos.0 + *data.ori.look_dir() * data.body.max_radius(); output_events.emit_local(LocalEvent::CreateOutcome(Outcome::IceCrack { - pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()), + pos: match self.static_data.anchor { + SpriteSummonAnchor::Summoner => summoner_pos, + SpriteSummonAnchor::Target => target_pos().unwrap_or(summoner_pos), + }, })); } } else { diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 1ce0462502..8e76e336b7 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -396,6 +396,7 @@ impl Block { BlockKind::WeakRock => Some(0.75), BlockKind::Snow => Some(0.1), BlockKind::Ice => Some(0.5), + BlockKind::Wood => Some(4.5), BlockKind::Lava => None, _ => self.get_sprite().and_then(|sprite| match sprite { sprite if sprite.is_container() => None, diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index d00f3bd549..7ab3ecbf3e 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -161,7 +161,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(1.0, Some(Secs(6.0))), + BuffData::new(1.0, Some(Secs(6.0))).with_force_immediate(true), Vec::new(), BuffSource::World, *read_data.time, @@ -179,7 +179,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(5.0, Some(Secs(3.0))), + BuffData::new(5.0, Some(Secs(3.0))).with_force_immediate(true), Vec::new(), BuffSource::World, *read_data.time, @@ -215,7 +215,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(15.0, Some(Secs(0.1))), + BuffData::new(15.0, Some(Secs(0.1))).with_force_immediate(true), Vec::new(), BuffSource::World, *read_data.time, @@ -228,7 +228,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Frozen, - BuffData::new(0.2, Some(Secs(1.0))), + BuffData::new(0.2, Some(Secs(3.0))), Vec::new(), BuffSource::World, *read_data.time, @@ -361,13 +361,15 @@ impl<'a> System<'a> for Sys { } }); - let damage_reduction = Damage::compute_damage_reduction( + let infinite_damage_reduction = (Damage::compute_damage_reduction( None, read_data.inventories.get(entity), Some(&stat), &read_data.msm, - ); - if (damage_reduction - 1.0).abs() < f32::EPSILON { + ) - 1.0) + .abs() + < f32::EPSILON; + if infinite_damage_reduction { for (id, buff) in buff_comp.buffs.iter() { if !buff.kind.is_buff() { expired_buffs.push(*id); @@ -389,6 +391,10 @@ impl<'a> System<'a> for Sys { buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs()); for (buff_kind, (buff_ids, kind_start_time)) in buff_kinds.into_iter() { let mut active_buff_ids = Vec::new(); + if infinite_damage_reduction && !buff_kind.is_buff() { + continue; + } + if buff_kind.stacks() { // Process all the buffs of this kind active_buff_ids = buff_ids; @@ -414,6 +420,7 @@ impl<'a> System<'a> for Sys { execute_effect( effect, buff.kind, + &buff.data, buff.start_time, kind_start_time, &read_data, @@ -471,6 +478,7 @@ impl<'a> System<'a> for Sys { fn execute_effect( effect: &BuffEffect, buff_kind: BuffKind, + buff_data: &BuffData, buff_start_time: Time, buff_kind_start_time: Time, read_data: &ReadData, @@ -508,7 +516,9 @@ fn execute_effect( let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor(); let whole_ticks = curr_tick - prev_tick; - if buff_will_expire { + if buff_data.force_immediate { + Some((1.0 / tick_dur.0 * dt) as f32) + } else if buff_will_expire { // If the buff is ending, include the fraction of progress towards the next // tick. let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0; diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index 7311bd61a1..837647ebf4 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -1,7 +1,8 @@ use common::{ - combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, + combat::{self, AttackOptions, AttackerInfo, TargetInfo}, comp::{ agent::{Sound, SoundKind}, + shockwave::ShockwaveDodgeable, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats, }, @@ -182,7 +183,10 @@ impl<'a> System<'a> for Sys { arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b)) } && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle - && (!shockwave.requires_ground || physics_state_b.on_ground.is_some()); + && match shockwave.dodgeable { + ShockwaveDodgeable::Roll | ShockwaveDodgeable::No => true, + ShockwaveDodgeable::Jump => physics_state_b.on_ground.is_some() + }; if hit { let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir); @@ -217,12 +221,10 @@ impl<'a> System<'a> for Sys { .character_states .get(target) .and_then(|cs| cs.attack_immunities()) - .map_or(false, |i| { - if shockwave.requires_ground { - i.ground_shockwaves - } else { - i.air_shockwaves - } + .map_or(false, |i| match shockwave.dodgeable { + ShockwaveDodgeable::Roll => i.air_shockwaves, + ShockwaveDodgeable::Jump => i.ground_shockwaves, + ShockwaveDodgeable::No => false, }); // PvP check let may_harm = combat::may_harm( @@ -244,11 +246,7 @@ impl<'a> System<'a> for Sys { dir, attack_options, 1.0, - if shockwave.requires_ground { - AttackSource::GroundShockwave - } else { - AttackSource::AirShockwave - }, + shockwave.dodgeable.to_attack_source(), *read_data.time, |e| server_emitter.emit(e), |o| outcomes_emitter.emit(o), diff --git a/rtsim/src/data/sentiment.rs b/rtsim/src/data/sentiment.rs index de2d14ab99..52804a8d91 100644 --- a/rtsim/src/data/sentiment.rs +++ b/rtsim/src/data/sentiment.rs @@ -198,7 +198,7 @@ impl Sentiment { // remembering the last interaction instead of constant updates. if rng.gen_bool( (1.0 / (self.positivity.unsigned_abs() as f32 * DECAY_TIME_FACTOR.powi(2) * dt)) - as f64, + .max(1.0) as f64, ) { self.positivity -= self.positivity.signum(); } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 5af66d5589..a61e613b12 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -289,9 +289,19 @@ impl Data { } // Spawn one monster Gigasfrost into the world // Try a few times to find a location that's not underwater - if let Some((wpos, chunk)) = (0..10) + if let Some((wpos, chunk)) = (0..100) .map(|_| world.sim().get_size().map(|sz| rng.gen_range(0..sz as i32))) - .find_map(|pos| Some((pos, world.sim().get(pos).filter(|c| !c.is_underwater())?))) + .find_map(|pos| { + Some(( + pos, + world + .sim() + .get(pos) + // This is currently a workaround to force Frost Gigas spawning in cold areas + // TODO: Once more Gigas are implemented remove this + .filter(|c| !c.is_underwater() && c.temp < CONFIG.snow_temp)?, + )) + }) .map(|(pos, chunk)| { let wpos2d = pos.cpos_to_wpos_center(); ( diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index c16641e955..42cb4a04d0 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -14,7 +14,7 @@ use rand::prelude::*; use rand_chacha::ChaChaRng; use tracing::{error, warn}; use vek::{Clamp, Vec2}; -use world::site::SiteKind; +use world::{site::SiteKind, CONFIG}; pub struct SimulateNpcs; @@ -132,7 +132,14 @@ fn on_death(ctx: EventCtx) { .map(|e| e as f32 + 0.5) .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) } else { - let pos = (0..10) + let is_gigas = matches!(body, Body::BipedLarge(body) if body.species == comp::body::biped_large::Species::Gigasfrost); + + let pos = (0..(if is_gigas { + /* More attempts for gigas */ + 100 + } else { + 10 + })) .map(|_| { ctx.world .sim() @@ -140,10 +147,9 @@ fn on_death(ctx: EventCtx) { .map(|sz| rng.gen_range(0..sz as i32)) }) .find(|pos| { - ctx.world - .sim() - .get(*pos) - .map_or(false, |c| !c.is_underwater()) + ctx.world.sim().get(*pos).map_or(false, |c| { + !c.is_underwater() && (!is_gigas || c.temp < CONFIG.snow_temp) + }) }) .unwrap_or(ctx.world.sim().get_size().as_() / 2); let wpos2d = pos.cpos_to_wpos_center(); diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 4e360044c1..1c48dbefff 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -14,7 +14,7 @@ use common::{ combat::perception_dist_multiplier_from_stealth, comp::{ self, - ability::MAX_ABILITIES, + ability::BASE_ABILITY_LIMIT, agent::{Sound, SoundKind, Target}, inventory::slot::EquipSlot, item::{ @@ -1097,7 +1097,7 @@ impl<'a> AgentData<'a> { "Frostfang" => Tactic::RandomAbilities { primary: 1, secondary: 3, - abilities: [0; MAX_ABILITIES], + abilities: [0; BASE_ABILITY_LIMIT], }, "Tursus Claws" => Tactic::RandomAbilities { primary: 2, @@ -1551,9 +1551,14 @@ impl<'a> AgentData<'a> { read_data, rng, ), - Tactic::FrostGigas => { - self.handle_frostgigas_attack(agent, controller, &attack_data, tgt_data, read_data) - }, + Tactic::FrostGigas => self.handle_frostgigas_attack( + agent, + controller, + &attack_data, + tgt_data, + read_data, + rng, + ), Tactic::BorealHammer => self.handle_boreal_hammer_attack( agent, controller, diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index fc867d8e2b..8d254818d9 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -5,15 +5,19 @@ use crate::{ }; use common::{ comp::{ - ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, MAX_ABILITIES}, + ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, BASE_ABILITY_LIMIT}, buff::BuffKind, item::tool::AbilityContext, skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, - AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent, - Controller, InputKind, + Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, + ControlEvent, Controller, Fluid, InputKind, }, path::TraversalConfig, - states::{self_buff, sprite_summon, utils::StageSection}, + states::{ + self_buff, + sprite_summon::{self, SpriteSummonAnchor}, + utils::StageSection, + }, terrain::Block, util::Dir, vol::ReadVol, @@ -4190,43 +4194,61 @@ impl<'a> AgentData<'a> { attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, + rng: &mut impl Rng, ) { - const GIGAS_MELEE_RANGE: f32 = 6.0; - const GIGAS_SPIKE_RANGE: f32 = 12.0; - const GIGAS_FREEZE_RANGE: f32 = 20.0; - const GIGAS_LEAP_RANGE: f32 = 30.0; - const MINION_SUMMON_THRESHOLD: f32 = 0.2; + const GIGAS_MELEE_RANGE: f32 = 12.0; + const GIGAS_SPIKE_RANGE: f32 = 16.0; + const ICEBOMB_RANGE: f32 = 70.0; + const GIGAS_LEAP_RANGE: f32 = 50.0; + const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.; + const FLASHFREEZE_RANGE: f32 = 30.; + + #[allow(clippy::enum_variant_names)] + enum ActionStateTimers { + AttackChange, + Bonk, + } enum ActionStateFCounters { - MinionSummonThreshold = 0, - } - enum ActionStateTimers { - AttackChange = 0, - } - if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 2.5 { - agent.action_state.timers[ActionStateTimers::AttackChange as usize] = 0.0; + FCounterMinionSummonThreshold = 0, } - let line_of_sight_with_target = || { - entities_have_line_of_sight( - self.pos, - self.body, - self.scale, - tgt_data.pos, - tgt_data.body, - tgt_data.scale, - read_data, - ) + enum ActionStateICounters { + /// An ability that is forced to fully complete until moving on to + /// other attacks. + /// 1 = Leap shockwave, 2 = Flashfreeze, 3 = Spike summon, + /// 4 = Whirlwind, 5 = Remote ice spikes, 6 = Ice bombs + CurrentAbility = 0, + } + + let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0); + let remote_spikes_action = || ControlAction::StartInput { + input: InputKind::Ability(5), + target_entity: None, + select_pos: Some(tgt_data.pos.0), }; + let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.action_state.initialized { - agent.action_state.counters[ActionStateFCounters::MinionSummonThreshold as usize] = + agent.action_state.counters + [ActionStateFCounters::FCounterMinionSummonThreshold as usize] = 1.0 - MINION_SUMMON_THRESHOLD; agent.action_state.initialized = true; - } else if health_fraction - < agent.action_state.counters[ActionStateFCounters::MinionSummonThreshold as usize] + } + + // Update timers + if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 { + agent.action_state.timers[ActionStateTimers::AttackChange as usize] = 0.0; + } else { + agent.action_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0; + } + agent.action_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0; + + if health_fraction + < agent.action_state.counters + [ActionStateFCounters::FCounterMinionSummonThreshold as usize] { // Summon minions at particular thresholds of health controller.push_basic_input(InputKind::Ability(3)); @@ -4234,57 +4256,131 @@ impl<'a> AgentData<'a> { if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.action_state.counters - [ActionStateFCounters::MinionSummonThreshold as usize] -= + [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -= MINION_SUMMON_THRESHOLD; } - } else { - // If the target is in melee range of frost use primary and secondary - // attacks accordingly - if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) { - if agent.action_state.timers[ActionStateTimers::AttackChange as usize] > 1.0 { - // Backhand anyone trying to circle strafe frost - if attack_data.angle > 160.0 { - // Use reorientate strike - controller.push_basic_input(InputKind::Secondary); - // If in front of frost use primary - } else { - // Hit them regularly - controller.push_basic_input(InputKind::Primary); - } - } else { - controller.push_basic_input(InputKind::Ability(4)); - } - } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2) - && line_of_sight_with_target() - { - 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 { + // Continue casting any attacks that are forced to complete + } else if let Some(ability) = Some( + &mut agent.action_state.int_counters[ActionStateICounters::CurrentAbility as usize], + ) + .filter(|i| **i != 0) + { + if *ability == 3 && should_use_targeted_spikes() { + *ability = 5 + }; + + let reset = match ability { + // Must be rolled + 1 => { controller.push_basic_input(InputKind::Ability(1)); - } else { - // or icebombs + matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover)) + }, + // Attacker will have to run away here + 2 => { + controller.push_basic_input(InputKind::Ability(4)); + matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover)) + }, + // Avoid the spikes! + 3 => { + controller.push_basic_input(InputKind::Ability(0)); + matches!(self.char_state, CharacterState::SpriteSummon(c) + if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner))) + }, + // Long whirlwind attack + 4 => { + controller.push_basic_input(InputKind::Ability(7)); + matches!(self.char_state, CharacterState::SpinMelee(c) if matches!(c.stage_section, StageSection::Recover)) + }, + // Remote ice spikes + 5 => { + controller.push_action(remote_spikes_action()); + matches!(self.char_state, CharacterState::SpriteSummon(c) + if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target))) + }, + // Ice bombs + 6 => { controller.push_basic_input(InputKind::Ability(2)); - } + matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover)) + }, + // Should never happen + _ => true, + }; + + if reset { + *ability = 0; } - 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 self.path_toward_target( agent, @@ -5486,7 +5582,7 @@ impl<'a> AgentData<'a> { rng: &mut impl Rng, primary_weight: u8, secondary_weight: u8, - ability_weights: [u8; MAX_ABILITIES], + ability_weights: [u8; BASE_ABILITY_LIMIT], ) { let primary = self.extract_ability(AbilityInput::Primary); let secondary = self.extract_ability(AbilityInput::Secondary); @@ -5534,7 +5630,7 @@ impl<'a> AgentData<'a> { let secondary_chance = secondary_weight as f64 / ((secondary_weight + ability_weights.iter().sum::()) as f64).max(0.01); let ability_chances = { - let mut chances = [0.0; MAX_ABILITIES]; + let mut chances = [0.0; BASE_ABILITY_LIMIT]; chances.iter_mut().enumerate().for_each(|(i, chance)| { *chance = ability_weights[i] as f64 / (ability_weights diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index a5537684e9..f521704f5d 100755 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -1,7 +1,7 @@ use crate::util::*; use common::{ comp::{ - ability::{CharacterAbility, MAX_ABILITIES}, + ability::{CharacterAbility, BASE_ABILITY_LIMIT}, buff::{BuffKind, Buffs}, character_state::AttackFilters, group, @@ -43,7 +43,6 @@ pub struct AgentData<'a> { pub body: Option<&'a Body>, pub inventory: &'a Inventory, pub skill_set: &'a SkillSet, - #[allow(dead_code)] // may be useful for pathing pub physics_state: &'a PhysicsState, pub alignment: Option<&'a Alignment>, pub traversal_config: TraversalConfig, @@ -182,7 +181,7 @@ pub enum Tactic { RandomAbilities { primary: u8, secondary: u8, - abilities: [u8; MAX_ABILITIES], + abilities: [u8; BASE_ABILITY_LIMIT], }, // Tool specific tactics diff --git a/server/src/character_creator.rs b/server/src/character_creator.rs index f1366def6c..2d24d1ae4f 100644 --- a/server/src/character_creator.rs +++ b/server/src/character_creator.rs @@ -3,7 +3,7 @@ use common::{ character::CharacterId, comp::{ inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats, - Waypoint, + Waypoint, BASE_ABILITY_LIMIT, }, }; use specs::{Entity, WriteExpect}; @@ -76,7 +76,7 @@ pub fn create_character( inventory, waypoint, pets: Vec::new(), - active_abilities: Default::default(), + active_abilities: common::comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT), map_marker, }); Ok(()) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index e1ed415f40..3bc58b6e69 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -925,19 +925,17 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o let to = pos + dir * power; let _ = terrain .ray(from, to) - .until(|block: &Block| { + .while_(|block: &Block| { + ray_energy -= + block.explode_power().unwrap_or(0.0) + rng.gen::() * 0.1; + // Stop if: // 1) Block is liquid // 2) Consumed all energy // 3) Can't explode block (for example we hit stone wall) - let stop = block.is_liquid() + block.is_liquid() || block.explode_power().is_none() - || ray_energy <= 0.0; - - ray_energy -= - block.explode_power().unwrap_or(0.0) + rng.gen::() * 0.1; - - stop + || ray_energy <= 0.0 }) .for_each(|block: &Block, pos| { if block.explode_power().is_some() { diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index fe2260c80a..1d86cf141a 100755 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -438,7 +438,7 @@ pub fn handle_create_sprite( let state = server.state_mut(); if state.can_set_block(pos) { let block = state.terrain().get(pos).ok().copied(); - if block.map_or(false, |b| (*b).is_air()) { + if block.map_or(false, |b| (*b).is_fluid()) { let old_block = block.unwrap_or_else(|| Block::air(SpriteKind::Empty)); let new_block = old_block.with_sprite(sprite); state.set_block(pos, new_block); diff --git a/server/src/persistence/json_models.rs b/server/src/persistence/json_models.rs index 13370b29bd..58f80ea109 100644 --- a/server/src/persistence/json_models.rs +++ b/server/src/persistence/json_models.rs @@ -271,7 +271,7 @@ pub fn active_abilities_from_db_model( abilities, }| { let mut auxiliary_abilities = - [comp::ability::AuxiliaryAbility::Empty; comp::ability::MAX_ABILITIES]; + vec![comp::ability::AuxiliaryAbility::Empty; comp::ability::BASE_ABILITY_LIMIT]; for (empty, ability) in auxiliary_abilities.iter_mut().zip(abilities.into_iter()) { *empty = aux_ability_from_string(&ability); } @@ -285,7 +285,10 @@ pub fn active_abilities_from_db_model( }, ) .collect::>(); - 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 diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 288d25ff59..94af2adaed 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -22,7 +22,7 @@ use common::{ object, skills::{GeneralSkill, Skill}, ChatType, Content, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence, - PresenceKind, + PresenceKind, BASE_ABILITY_LIMIT, }, effect::Effect, link::{Is, Link, LinkHandle}, @@ -312,7 +312,11 @@ impl StateExt for State { .unwrap_or(0), )) .with(stats) - .with(comp::ActiveAbilities::default()) + .with(if body.is_humanoid() { + comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT) + } else { + comp::ActiveAbilities::default() + }) .with(skill_set) .maybe_with(health) .with(poise) diff --git a/voxygen/anim/src/biped_large/alpha.rs b/voxygen/anim/src/biped_large/alpha.rs index f384b9e2f3..ebc8f5f84a 100644 --- a/voxygen/anim/src/biped_large/alpha.rs +++ b/voxygen/anim/src/biped_large/alpha.rs @@ -201,6 +201,61 @@ impl Animation for AlphaAnimation { * Quaternion::rotation_y(-1.8 + move1 * -0.4 + move2 * 3.5) * Quaternion::rotation_z(move1 * -1.0 + move2 * -1.5); }, + Some("common.abilities.custom.gigas_frost.bonk") => { + next.head.orientation = Quaternion::rotation_x(move1 * 0.8 + move2 * -1.2); + next.jaw.position = Vec3::new(0.0, s_a.jaw.0, s_a.jaw.1); + next.jaw.orientation = Quaternion::rotation_x(move2 * -0.3); + next.control_l.position = Vec3::new(-0.5, 4.0, 1.0); + next.control_r.position = Vec3::new(-0.5, 4.0, 1.0); + next.control_l.orientation = Quaternion::rotation_x(PI / 2.0); + next.control_r.orientation = Quaternion::rotation_x(PI / 2.0); + next.weapon_l.position = + Vec3::new(-12.0 + (move1 * 20.0).min(10.0), -1.0, -15.0); + next.weapon_r.position = + Vec3::new(12.0 + (move1 * -20.0).max(-10.0), -1.0, -15.0); + + next.weapon_l.orientation = Quaternion::rotation_x(-PI / 2.0 - 0.1) + * Quaternion::rotation_z(move1 * -1.0); + next.weapon_r.orientation = Quaternion::rotation_x(-PI / 2.0 - 0.1) + * Quaternion::rotation_z(move1 * 1.0); + + next.shoulder_l.orientation = + Quaternion::rotation_x(-0.3 + move1 * 2.0 + move2 * -1.0); + + next.shoulder_r.orientation = + Quaternion::rotation_x(-0.3 + move1 * 2.0 + move2 * -1.0); + + next.control.orientation = Quaternion::rotation_x(move1 * 1.5 + move2 * -0.4); + + let twist = move1 * 0.6 + move3 * -0.6; + next.upper_torso.position = + Vec3::new(0.0, s_a.upper_torso.0, s_a.upper_torso.1); + next.upper_torso.orientation = + Quaternion::rotation_x(move1 * 0.4 + move2 * -1.1) + * Quaternion::rotation_z(twist * -0.2 + move1 * -0.1 + move2 * 0.3); + + next.lower_torso.orientation = + Quaternion::rotation_x(move1 * -0.4 + move2 * 1.1) + * Quaternion::rotation_z(twist); + + next.foot_l.position = Vec3::new( + -s_a.foot.0, + s_a.foot.1 + move1 * -7.0 + move2 * 7.0, + s_a.foot.2, + ); + next.foot_l.orientation = Quaternion::rotation_x(move1 * -0.8 + move2 * 0.8) + * Quaternion::rotation_z(move1 * 0.3 + move2 * -0.3); + + next.foot_r.position = Vec3::new( + s_a.foot.0, + s_a.foot.1 + move1 * 5.0 + move2 * -5.0, + s_a.foot.2, + ); + next.foot_r.orientation = Quaternion::rotation_y(move1 * -0.3 + move2 * 0.3) + * Quaternion::rotation_z(move1 * 0.4 + move2 * -0.4); + next.main.position = Vec3::new(-5.0 + (move1 * 20.0).min(10.0), 6.0, 4.0); + next.main.orientation = Quaternion::rotation_y(move1 * 0.4 + move2 * -1.2); + }, _ => { next.control_l.position = Vec3::new(-1.0, 2.0, 12.0 + move2 * -10.0); next.control_r.position = Vec3::new(1.0, 2.0, -2.0); diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index b42705cee3..24ffeedc92 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -477,10 +477,14 @@ impl SfxMgr { Outcome::SummonedCreature { pos, body, .. } => { match body { Body::BipedSmall(body) => match body.species { - biped_small::Species::Boreal | biped_small::Species::Clockwork => { + biped_small::Species::Clockwork => { let sfx_trigger_item = triggers.get_key_value(&SfxEvent::DeepLaugh); audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater); }, + biped_small::Species::Boreal => { + let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GigaRoar); + audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater); + }, _ => {}, }, Body::Object(object::Body::Flamethrower) => { diff --git a/voxygen/src/hud/diary.rs b/voxygen/src/hud/diary.rs index ebe262af66..5324adcb11 100644 --- a/voxygen/src/hud/diary.rs +++ b/voxygen/src/hud/diary.rs @@ -23,7 +23,7 @@ use common::{ combat, comp::{ self, - ability::{Ability, ActiveAbilities, AuxiliaryAbility, MAX_ABILITIES}, + ability::{Ability, ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT}, inventory::{ item::{ item_key::ItemKey, @@ -811,12 +811,12 @@ impl<'a> Widget for Diary<'a> { state.update(|s| { s.ids .active_abilities - .resize(MAX_ABILITIES, &mut ui.widget_id_generator()) + .resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator()) }); state.update(|s| { s.ids .active_abilities_keys - .resize(MAX_ABILITIES, &mut ui.widget_id_generator()) + .resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator()) }); let mut slot_maker = SlotMaker { @@ -844,7 +844,7 @@ impl<'a> Widget for Diary<'a> { pulse: 0.0, }; - for i in 0..MAX_ABILITIES { + for i in 0..BASE_ABILITY_LIMIT { let ability_id = self .active_abilities .get_ability( diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index d5c8d6d50a..ee53301edc 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -98,6 +98,7 @@ pub enum ParticleMode { SnowStorm = 44, PortalFizz = 45, Ink = 46, + Whirlwind = 47, } impl ParticleMode { diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index d9395d1775..cec34b2daf 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -11,8 +11,11 @@ use crate::{ use common::{ assets::{AssetExt, DotVoxAsset}, comp::{ - self, aura, beam, body, buff, item::Reagent, object, shockwave, BeamSegment, Body, - CharacterState, Ori, Pos, Scale, Shockwave, Vel, + self, aura, beam, body, buff, + item::Reagent, + object, + shockwave::{self, ShockwaveDodgeable}, + BeamSegment, Body, CharacterState, Ori, Pos, Scale, Shockwave, Vel, }, figure::Segment, outcome::Outcome, @@ -927,6 +930,29 @@ impl ParticleMgr { } } }, + states::spin_melee::FrontendSpecifier::Whirlwind => { + if matches!(spin.stage_section, StageSection::Action) { + let time = scene_data.state.get_time(); + let mut rng = thread_rng(); + self.particles.resize_with( + self.particles.len() + + 3 + + usize::from( + self.scheduler.heartbeats(Duration::from_millis(5)), + ), + || { + Particle::new( + Duration::from_millis(1000), + time, + ParticleMode::Whirlwind, + interpolated + .pos + .map(|e| e + rng.gen_range(-0.25..0.25)), + ) + }, + ); + } + }, } } }, @@ -2246,7 +2272,8 @@ impl ParticleMgr { let new_particle_count = particles_per_length * heartbeats as usize; self.particles.reserve(new_particle_count); // higher wave when wave doesn't require ground - let wave = if shockwave.properties.requires_ground { + let wave = if matches!(shockwave.properties.dodgeable, ShockwaveDodgeable::Jump) + { 0.5 } else { 8.0