Frost gigas tweaks

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

View File

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Mutliple singleplayer worlds and map generation UI. - 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

View File

@ -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"): (

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,
))), ))),
) )

View File

@ -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,

View File

@ -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,

View File

@ -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,
))), ))),
), ),

View File

@ -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,

View File

@ -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),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,
), ),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -1,21 +1,21 @@
BasicMelee( 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),

View File

@ -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,
))), ))),
) )

View File

@ -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),

View File

@ -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,
) )

View File

@ -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,
) )

View File

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

View File

@ -9,16 +9,16 @@ LeapShockwave(
knockback: (strength: 3.0, direction: Up), 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,

View File

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

View File

@ -5,17 +5,17 @@ BasicMelee(
recover_duration: 0.8, 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),

View File

@ -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),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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",

View File

@ -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(

View File

@ -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,
} }
} }

View File

@ -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(),

View File

@ -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,

View File

@ -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 {

View File

@ -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.

View File

@ -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,
} }
} }

View File

@ -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::{

View File

@ -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),

View File

@ -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,
}
}
}

View File

@ -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,

View File

@ -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 =

View File

@ -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>,

View File

@ -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

View File

@ -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,
}; };

View File

@ -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,
} }

View File

@ -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 {

View File

@ -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,

View File

@ -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;

View File

@ -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),

View File

@ -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();
} }

View File

@ -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();
( (

View File

@ -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();

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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(())

View File

@ -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() {

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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);

View File

@ -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) => {

View File

@ -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(

View File

@ -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 {

View File

@ -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