mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'horblegorble/gnarling-bosses' into 'master'
broad updates to wooden golem, gnarling chieftain, harvester See merge request veloren/veloren!4485
This commit is contained in:
commit
9c129e8c24
@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Rebalanced cultist dungeon loot tables; among other things, the drop Ankh of Life from Mindflayer is now 25x more frequent.
|
||||
- Improved Mindflayer anticheese measures.
|
||||
- Recipe pricing with NPCs.
|
||||
- Broad entity and loot updates to Gnarling Fortifications
|
||||
|
||||
### Removed
|
||||
|
||||
|
@ -771,8 +771,9 @@
|
||||
primary: Simple(None, "common.abilities.custom.harvester.scythe"),
|
||||
secondary: Simple(None, "common.abilities.custom.harvester.firebreath"),
|
||||
abilities: [
|
||||
Simple(None, "common.abilities.custom.harvester.ensnaringvines"),
|
||||
Simple(None, "common.abilities.custom.harvester.explodingpumpkin"),
|
||||
Simple(None, "common.abilities.custom.harvester.ensnaringvines_sparse"),
|
||||
Simple(None, "common.abilities.custom.harvester.ensnaringvines_dense"),
|
||||
],
|
||||
),
|
||||
// TODO: Allow ability sets to expand other ability sets
|
||||
|
@ -0,0 +1,10 @@
|
||||
SpriteSummon(
|
||||
buildup_duration: 0.9,
|
||||
cast_duration: 0.6,
|
||||
recover_duration: 0.5,
|
||||
sprite: EnsnaringVines,
|
||||
del_timeout: None,
|
||||
summon_distance: (0, 25),
|
||||
sparseness: 0.6,
|
||||
angle: 360,
|
||||
)
|
@ -4,7 +4,7 @@ SpriteSummon(
|
||||
recover_duration: 0.3,
|
||||
sprite: EnsnaringVines,
|
||||
del_timeout: None,
|
||||
summon_distance: (0, 25),
|
||||
sparseness: 0.67,
|
||||
summon_distance: (0, 30),
|
||||
sparseness: 0.8,
|
||||
angle: 360,
|
||||
)
|
@ -1,21 +1,21 @@
|
||||
BasicRanged(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.75,
|
||||
buildup_duration: 1.0,
|
||||
recover_duration: 1.6,
|
||||
projectile: (
|
||||
kind: Explosive(
|
||||
radius: 5,
|
||||
radius: 7.2,
|
||||
min_falloff: 0.6,
|
||||
reagent: Some(Red),
|
||||
terrain: Some((5, Black)),
|
||||
),
|
||||
attack: Some((
|
||||
damage: 37.5,
|
||||
knockback: Some(25),
|
||||
damage: 20,
|
||||
knockback: Some(22),
|
||||
energy: 0,
|
||||
buff: Some((
|
||||
kind: Burning,
|
||||
dur_secs: 5,
|
||||
dur_secs: 4,
|
||||
strength: DamageFraction(0.2),
|
||||
chance: 1.0,
|
||||
)),
|
||||
|
@ -2,18 +2,18 @@ BasicBeam(
|
||||
buildup_duration: 1.4,
|
||||
recover_duration: 0.9,
|
||||
beam_duration: 1.0,
|
||||
damage: 9.0,
|
||||
damage: 5.0,
|
||||
tick_rate: 1.5,
|
||||
range: 20.0,
|
||||
max_angle: 15.0,
|
||||
damage_effect: Some(Buff((
|
||||
kind: Burning,
|
||||
dur_secs: 10.0,
|
||||
strength: DamageFraction(1.0),
|
||||
dur_secs: 3.0,
|
||||
strength: DamageFraction(0.8),
|
||||
chance: 1.0,
|
||||
))),
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
ori_rate: 0.3,
|
||||
ori_rate: 0.7,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
|
@ -1,19 +1,19 @@
|
||||
BasicMelee(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.9,
|
||||
swing_duration: 0.1,
|
||||
buildup_duration: 1.0,
|
||||
swing_duration: 0.2,
|
||||
hit_timing: 0.5,
|
||||
recover_duration: 1.2,
|
||||
recover_duration: 1.0,
|
||||
melee_constructor: (
|
||||
kind: Slash(
|
||||
damage: 21.0,
|
||||
damage: 15.0,
|
||||
poise: 10.0,
|
||||
knockback: 10.0,
|
||||
energy_regen: 0.0,
|
||||
),
|
||||
range: 4.0,
|
||||
angle: 60.0,
|
||||
range: 4.5,
|
||||
angle: 50.0,
|
||||
multi_target: Some(Normal),
|
||||
),
|
||||
ori_modifier: 1.0,
|
||||
ori_modifier: 0.5,
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ BasicMelee(
|
||||
melee_constructor: (
|
||||
kind: Bash(
|
||||
damage: 8,
|
||||
poise: 5,
|
||||
poise: 10,
|
||||
knockback: 0,
|
||||
energy_regen: 0,
|
||||
),
|
||||
|
@ -7,7 +7,7 @@ BasicMelee(
|
||||
melee_constructor: (
|
||||
kind: SonicWave(
|
||||
damage: 10,
|
||||
poise: 100,
|
||||
poise: 75,
|
||||
knockback: 20,
|
||||
energy_regen: 0,
|
||||
),
|
||||
|
@ -1,20 +1,20 @@
|
||||
Shockwave(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 3.0,
|
||||
buildup_duration: 1.2,
|
||||
swing_duration: 0.12,
|
||||
recover_duration: 2.4,
|
||||
damage: 60.0,
|
||||
recover_duration: 1.5,
|
||||
damage: 22.0,
|
||||
poise_damage: 30,
|
||||
knockback: (strength: 30.0, direction: TowardsUp),
|
||||
knockback: (strength: 20.0, direction: TowardsUp),
|
||||
shockwave_angle: 90.0,
|
||||
shockwave_vertical_angle: 90.0,
|
||||
shockwave_speed: 15.0,
|
||||
shockwave_duration: 2.0,
|
||||
shockwave_duration: 1.9,
|
||||
dodgeable: Jump,
|
||||
move_efficiency: 0.0,
|
||||
damage_kind: Crushing,
|
||||
specifier: Ground,
|
||||
ori_rate: 1.0,
|
||||
ori_rate: 0.9,
|
||||
timing: PostBuildup,
|
||||
emit_outcome: true,
|
||||
)
|
||||
|
@ -1,18 +1,18 @@
|
||||
BasicMelee(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.9,
|
||||
buildup_duration: 1.2,
|
||||
swing_duration: 0.3,
|
||||
hit_timing: 0.6,
|
||||
recover_duration: 0.6,
|
||||
recover_duration: 1.2,
|
||||
melee_constructor: (
|
||||
kind: Bash(
|
||||
damage: 45,
|
||||
damage: 18,
|
||||
poise: 30,
|
||||
knockback: 20,
|
||||
energy_regen: 0,
|
||||
),
|
||||
range: 7.5,
|
||||
range: 5.0,
|
||||
angle: 360,
|
||||
),
|
||||
ori_modifier: 1.0,
|
||||
ori_modifier: 0.75,
|
||||
)
|
||||
|
@ -1,18 +1,18 @@
|
||||
BasicMelee(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 1.6,
|
||||
swing_duration: 0.1,
|
||||
buildup_duration: 1.0,
|
||||
swing_duration: 0.2,
|
||||
hit_timing: 0.6,
|
||||
recover_duration: 1.0,
|
||||
melee_constructor: (
|
||||
kind: Bash(
|
||||
damage: 30.0,
|
||||
damage: 12.0,
|
||||
poise: 25.0,
|
||||
knockback: 15.0,
|
||||
energy_regen: 0.0,
|
||||
),
|
||||
range: 4.0,
|
||||
angle: 45.0,
|
||||
angle: 55.0,
|
||||
),
|
||||
ori_modifier: 0.4,
|
||||
ori_modifier: 0.7,
|
||||
)
|
||||
|
@ -1,28 +1,28 @@
|
||||
BasicRanged(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.825,
|
||||
buildup_duration: 1.0,
|
||||
recover_duration: 0.6,
|
||||
projectile: (
|
||||
kind: Explosive(
|
||||
radius: 2,
|
||||
min_falloff: 0.5,
|
||||
radius: 3,
|
||||
min_falloff: 0.7,
|
||||
reagent: Some(Red),
|
||||
terrain: Some((2, Black))
|
||||
),
|
||||
attack: Some((
|
||||
damage: 13.5,
|
||||
damage: 5.5,
|
||||
energy: 10,
|
||||
buff: Some((
|
||||
kind: Burning,
|
||||
dur_secs: 5,
|
||||
strength: DamageFraction(0.1),
|
||||
dur_secs: 3,
|
||||
strength: DamageFraction(0.3),
|
||||
chance: 0.1,
|
||||
)),
|
||||
)),
|
||||
),
|
||||
projectile_body: Object(BoltFire),
|
||||
projectile_speed: 25,
|
||||
num_projectiles: 8,
|
||||
num_projectiles: 5,
|
||||
projectile_spread: 0.125,
|
||||
move_efficiency: 0.3,
|
||||
)
|
||||
|
@ -1,16 +1,16 @@
|
||||
Shockwave(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.975,
|
||||
buildup_duration: 1.2,
|
||||
swing_duration: 0.1,
|
||||
recover_duration: 0.6,
|
||||
damage: 30,
|
||||
recover_duration: 0.8,
|
||||
damage: 16,
|
||||
poise_damage: 0,
|
||||
knockback: ( strength: 25, direction: Away),
|
||||
knockback: ( strength: 15, direction: Away),
|
||||
shockwave_angle: 360,
|
||||
shockwave_vertical_angle: 90,
|
||||
shockwave_speed: 10,
|
||||
shockwave_duration: 1,
|
||||
dodgeable: Roll,
|
||||
shockwave_speed: 12,
|
||||
shockwave_duration: 1.0,
|
||||
dodgeable: Jump,
|
||||
move_efficiency: 0,
|
||||
damage_kind: Energy,
|
||||
specifier: Fire,
|
||||
|
@ -1,21 +1,21 @@
|
||||
BasicMelee(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.6,
|
||||
swing_duration: 0.1,
|
||||
buildup_duration: 0.75,
|
||||
swing_duration: 0.4,
|
||||
hit_timing: 0.5,
|
||||
recover_duration: 0.45,
|
||||
recover_duration: 0.6,
|
||||
melee_constructor: (
|
||||
kind: Bash(
|
||||
damage: 12,
|
||||
damage: 9,
|
||||
poise: 10,
|
||||
knockback: 0,
|
||||
energy_regen: 0,
|
||||
),
|
||||
range: 7.5,
|
||||
angle: 60.0,
|
||||
range: 3.0,
|
||||
angle: 40.0,
|
||||
damage_effect: Some(Buff((
|
||||
kind: Burning,
|
||||
dur_secs: 10.0,
|
||||
dur_secs: 4.0,
|
||||
strength: DamageFraction(0.5),
|
||||
chance: 0.5,
|
||||
))),
|
||||
|
@ -1,24 +1,24 @@
|
||||
BasicAura(
|
||||
buildup_duration: 0.375,
|
||||
cast_duration: 0.5,
|
||||
recover_duration: 0.375,
|
||||
buildup_duration: 0.2,
|
||||
cast_duration: 0.4,
|
||||
recover_duration: 0.2,
|
||||
targets: InGroup,
|
||||
auras: [
|
||||
(
|
||||
kind: Regeneration,
|
||||
strength: 7.5,
|
||||
duration: Some(5),
|
||||
strength: 1,
|
||||
duration: Some(1),
|
||||
category: Magical,
|
||||
),
|
||||
(
|
||||
kind: ProtectingWard,
|
||||
strength: 0.75,
|
||||
duration: Some(5),
|
||||
strength: 0.5,
|
||||
duration: Some(1),
|
||||
category: Magical,
|
||||
),
|
||||
],
|
||||
aura_duration: Some(2),
|
||||
range: 50,
|
||||
aura_duration: Some(1),
|
||||
range: 15,
|
||||
energy_cost: 0,
|
||||
scales_with_combo: false,
|
||||
)
|
||||
|
@ -1,18 +1,18 @@
|
||||
BasicAura(
|
||||
buildup_duration: 0.375,
|
||||
cast_duration: 0.5,
|
||||
recover_duration: 0.375,
|
||||
buildup_duration: 0.2,
|
||||
cast_duration: 0.4,
|
||||
recover_duration: 0.2,
|
||||
targets: OutOfGroup,
|
||||
auras: [
|
||||
(
|
||||
kind: Burning,
|
||||
strength: 0.75,
|
||||
duration: Some(5),
|
||||
strength: 1.2,
|
||||
duration: Some(1),
|
||||
category: Magical,
|
||||
),
|
||||
],
|
||||
aura_duration: Some(2),
|
||||
range: 50,
|
||||
aura_duration: Some(1),
|
||||
range: 15,
|
||||
energy_cost: 0,
|
||||
scales_with_combo: false,
|
||||
)
|
||||
|
@ -1,18 +1,18 @@
|
||||
BasicAura(
|
||||
buildup_duration: 0.375,
|
||||
cast_duration: 0.5,
|
||||
recover_duration: 0.375,
|
||||
buildup_duration: 0.2,
|
||||
cast_duration: 0.4,
|
||||
recover_duration: 0.2,
|
||||
targets: InGroup,
|
||||
auras: [
|
||||
(
|
||||
kind: Hastened,
|
||||
strength: 0.75,
|
||||
duration: Some(5),
|
||||
strength: 0.4,
|
||||
duration: Some(1),
|
||||
category: Magical,
|
||||
),
|
||||
],
|
||||
aura_duration: Some(2),
|
||||
range: 50,
|
||||
aura_duration: Some(1),
|
||||
range: 15,
|
||||
energy_cost: 0,
|
||||
scales_with_combo: false,
|
||||
)
|
||||
|
@ -2,7 +2,7 @@
|
||||
#![enable(implicit_some)]
|
||||
(
|
||||
name: Name("Gnarling Chieftain"),
|
||||
body: RandomWith("gnarling"),
|
||||
body: RandomWith("gnarling_chieftain"),
|
||||
alignment: Alignment(Enemy),
|
||||
loot: LootTable("common.loot_tables.dungeon.gnarling.chieftain"),
|
||||
inventory: (
|
||||
|
@ -6,10 +6,10 @@ ItemDef(
|
||||
stats: Direct((
|
||||
protection: Some(Normal(3.0)),
|
||||
poise_resilience: Some(Normal(1.0)),
|
||||
energy_max: Some(14.0),
|
||||
energy_max: Some(6.0),
|
||||
)),
|
||||
)),
|
||||
quality: High,
|
||||
quality: Moderate,
|
||||
tags: [
|
||||
Gnarling,
|
||||
SalvageInto(Lifecloth, 1),
|
||||
|
@ -2,6 +2,6 @@ ItemDef(
|
||||
legacy_name: "Green Luna",
|
||||
legacy_description: "The delicate wings flutter faintly.",
|
||||
kind: Glider,
|
||||
quality: High,
|
||||
quality: Moderate,
|
||||
tags: [],
|
||||
)
|
||||
|
@ -8,6 +8,6 @@ ItemDef(
|
||||
flicker_thousandths: 600,
|
||||
),
|
||||
),
|
||||
quality: High,
|
||||
quality: Moderate,
|
||||
tags: [Utility],
|
||||
)
|
||||
|
@ -1,12 +1,14 @@
|
||||
[
|
||||
// Weapons
|
||||
(5.0, LootTable("common.loot_tables.weapons.tier-1")),
|
||||
// Armor
|
||||
(5.0, LootTable("common.loot_tables.armor.tier-1")),
|
||||
// Misc
|
||||
(3.0, Item("common.items.armor.misc.neck.scratched")),
|
||||
(2.0, Item("common.items.armor.misc.head.wanderers_hat")),
|
||||
(3.0, Item("common.items.armor.misc.head.bamboo_twig")),
|
||||
// Chieftain Mask
|
||||
(1, All([
|
||||
All([
|
||||
LootTable("common.loot_tables.armor.tier-1"),
|
||||
LootTable("common.loot_tables.weapons.tier-1"),
|
||||
]),
|
||||
Lottery([
|
||||
(2.0, Nothing),
|
||||
(1.0, Item("common.items.armor.misc.neck.scratched")),
|
||||
(1.0, Item("common.items.armor.misc.head.gnarling_mask")),
|
||||
(1.0, Item("common.items.glider.moth")),
|
||||
]),
|
||||
])),
|
||||
]
|
@ -1,10 +1,13 @@
|
||||
[
|
||||
// Weapons
|
||||
(5.0, LootTable("common.loot_tables.weapons.tier-3")),
|
||||
// Armor
|
||||
(5.0, LootTable("common.loot_tables.armor.tier-3")),
|
||||
// Misc
|
||||
(3.0, Item("common.items.armor.misc.neck.scratched")),
|
||||
(2.0, Item("common.items.lantern.pumpkin")),
|
||||
(1.0, Item("common.items.glider.moth")),
|
||||
(1, All([
|
||||
All([
|
||||
LootTable("common.loot_tables.armor.tier-2"),
|
||||
LootTable("common.loot_tables.weapons.tier-2"),
|
||||
]),
|
||||
Lottery([
|
||||
(3.0, Nothing),
|
||||
(1.0, Item("common.items.lantern.pumpkin")),
|
||||
(1.0, Item("common.items.armor.misc.head.wanderers_hat")),
|
||||
]),
|
||||
])),
|
||||
]
|
@ -1,6 +1,16 @@
|
||||
[
|
||||
// Crafting ingredients
|
||||
(2.4, MultiDrop(Item("common.items.log.wood"), 5, 10)),
|
||||
(0.1, MultiDrop(Item("common.items.log.hardwood"), 1, 2)),
|
||||
(0.5, LootTable("common.loot_tables.weapons.components.secondary.sceptre")),
|
||||
(1, Lottery([
|
||||
(0.7, All([
|
||||
MultiDrop(Item("common.items.log.wood"), 3, 4),
|
||||
MultiDrop(Item("common.items.crafting_ing.twigs"), 1, 2),
|
||||
Lottery([
|
||||
(0.6, MultiDrop(Item("common.items.log.bamboo"), 1, 2)),
|
||||
(0.4, LootTable("common.loot_tables.weapons.components.secondary.sceptre")),
|
||||
]),
|
||||
])),
|
||||
(0.3, All([
|
||||
MultiDrop(Item("common.items.log.wood"), 5, 7),
|
||||
MultiDrop(Item("common.items.crafting_ing.twigs"), 3, 5),
|
||||
])),
|
||||
]))
|
||||
]
|
||||
|
@ -1,5 +1,5 @@
|
||||
[
|
||||
(1.0, LootTable("common.loot_tables.weapons.components.tier-1")),
|
||||
(1.0, LootTable("common.loot_tables.weapons.components.tier-0")),
|
||||
(1.0, LootTable("common.loot_tables.armor.cloth")),
|
||||
(0.5, Item("common.items.recipes.explosives")),
|
||||
]
|
||||
|
@ -1,7 +1,16 @@
|
||||
[
|
||||
(1.0, LootTable("common.loot_tables.weapons.components.tier-0")),
|
||||
(1.0, LootTable("common.loot_tables.weapons.components.tier-1")),
|
||||
(1.0, LootTable("common.loot_tables.armor.cloth")),
|
||||
(0.5, LootTable("common.loot_tables.weapons.components.tier-0")),
|
||||
(0.25, LootTable("common.loot_tables.weapons.tier-0")),
|
||||
(0.25, LootTable("common.loot_tables.armor.tier-0")),
|
||||
(0.25, Item("common.items.armor.misc.head.bamboo_twig")),
|
||||
// Currency
|
||||
(3.0, MultiDrop(Item("common.items.utility.coins"), 25, 50)),
|
||||
// Materials
|
||||
(0.5, MultiDrop(Item("common.items.mineral.ore.veloritefrag"), 5, 10)),
|
||||
// Consumables
|
||||
(2.0, MultiDrop(Item("common.items.consumable.potion_minor"), 2, 5)),
|
||||
// Food
|
||||
(1.0, MultiDrop(LootTable("common.loot_tables.food.prepared"), 1, 2)),
|
||||
(0.2, Item("common.items.recipes.explosives")),
|
||||
(0.5, Item("common.items.recipes.instruments")),
|
||||
(0.2, Item("common.items.recipes.charms")),
|
||||
|
@ -1233,6 +1233,10 @@
|
||||
keyword: "treasure_egg",
|
||||
generic: "Treasure Egg",
|
||||
),
|
||||
gnarling_chieftain: (
|
||||
keyword: "gnarling_chieftain",
|
||||
generic: "Gnarling Chieftain"
|
||||
),
|
||||
)
|
||||
),
|
||||
fish_small: (
|
||||
|
@ -367,6 +367,7 @@ impl<'a> From<&'a Body> for Psyche {
|
||||
},
|
||||
Body::BipedSmall(biped_small) => match biped_small.species {
|
||||
biped_small::Species::Gnarling => 0.2,
|
||||
biped_small::Species::Mandragora => 0.1,
|
||||
biped_small::Species::Adlet => 0.2,
|
||||
biped_small::Species::Haniwa => 0.1,
|
||||
biped_small::Species::Sahagin => 0.1,
|
||||
@ -379,7 +380,8 @@ impl<'a> From<&'a Body> for Psyche {
|
||||
| biped_small::Species::Irrwurz
|
||||
| biped_small::Species::ShamanicSpirit
|
||||
| biped_small::Species::Jiangshi
|
||||
| biped_small::Species::Bushly => 0.0,
|
||||
| biped_small::Species::Bushly
|
||||
| biped_small::Species::GnarlingChieftain => 0.0,
|
||||
|
||||
_ => 0.5,
|
||||
},
|
||||
|
@ -552,6 +552,7 @@ impl Body {
|
||||
biped_small::Species::Jiangshi => Vec3::new(1.3, 1.8, 2.5),
|
||||
biped_small::Species::Flamekeeper => Vec3::new(1.5, 1.5, 2.5),
|
||||
biped_small::Species::TreasureEgg => Vec3::new(1.1, 1.1, 1.4),
|
||||
biped_small::Species::GnarlingChieftain => Vec3::new(1.0, 0.75, 1.4),
|
||||
_ => Vec3::new(1.0, 0.75, 1.4),
|
||||
},
|
||||
Body::BirdLarge(body) => match body.species {
|
||||
@ -948,7 +949,7 @@ impl Body {
|
||||
biped_large::Species::Tidalwarrior => 1600,
|
||||
biped_large::Species::Yeti => 1800,
|
||||
biped_large::Species::Minotaur => 3000,
|
||||
biped_large::Species::Harvester => 1500,
|
||||
biped_large::Species::Harvester => 1300,
|
||||
biped_large::Species::Blueoni => 240,
|
||||
biped_large::Species::Redoni => 240,
|
||||
biped_large::Species::Huskbrute => 800,
|
||||
@ -969,6 +970,8 @@ impl Body {
|
||||
},
|
||||
Body::BipedSmall(biped_small) => match biped_small.species {
|
||||
biped_small::Species::Gnarling => 50,
|
||||
biped_small::Species::GnarlingChieftain => 150,
|
||||
biped_small::Species::Mandragora => 65,
|
||||
biped_small::Species::Adlet => 65,
|
||||
biped_small::Species::Sahagin => 85,
|
||||
biped_small::Species::Haniwa => 100,
|
||||
@ -990,14 +993,14 @@ impl Body {
|
||||
object::Body::BarrelOrgan => 500,
|
||||
object::Body::HaniwaSentry => 60,
|
||||
object::Body::SeaLantern => 100,
|
||||
object::Body::GnarlingTotemGreen => 25,
|
||||
object::Body::TerracottaStatue => 600,
|
||||
object::Body::GnarlingTotemRed | object::Body::GnarlingTotemWhite => 35,
|
||||
object::Body::GnarlingTotemGreen => 15,
|
||||
object::Body::GnarlingTotemRed | object::Body::GnarlingTotemWhite => 15,
|
||||
_ => 1000,
|
||||
},
|
||||
Body::ItemDrop(_) => 1000,
|
||||
Body::Golem(golem) => match golem.species {
|
||||
golem::Species::WoodGolem => 200,
|
||||
golem::Species::WoodGolem => 120,
|
||||
golem::Species::ClayGolem => 350,
|
||||
golem::Species::Gravewarden => 1000,
|
||||
golem::Species::CoralGolem => 550,
|
||||
@ -1221,7 +1224,6 @@ impl Body {
|
||||
biped_large::Species::Minotaur => 4.05,
|
||||
biped_large::Species::Tidalwarrior => 2.75,
|
||||
biped_large::Species::Yeti => 2.25,
|
||||
biped_large::Species::Harvester => 2.1,
|
||||
_ => 1.0,
|
||||
},
|
||||
Body::BipedSmall(b) => match b.species {
|
||||
@ -1271,6 +1273,7 @@ impl Body {
|
||||
_ => 300,
|
||||
},
|
||||
Body::BipedSmall(b) => match b.species {
|
||||
biped_small::Species::GnarlingChieftain => 130,
|
||||
biped_small::Species::IronDwarf | biped_small::Species::Flamekeeper => 300,
|
||||
_ => 100,
|
||||
},
|
||||
|
@ -53,6 +53,7 @@ make_case_elim!(
|
||||
ShamanicSpirit = 16,
|
||||
Jiangshi = 17,
|
||||
TreasureEgg = 18,
|
||||
GnarlingChieftain = 19,
|
||||
}
|
||||
);
|
||||
|
||||
@ -80,6 +81,7 @@ pub struct AllSpecies<SpeciesMeta> {
|
||||
pub shamanic_spirit: SpeciesMeta,
|
||||
pub jiangshi: SpeciesMeta,
|
||||
pub treasure_egg: SpeciesMeta,
|
||||
pub gnarling_chieftain: SpeciesMeta,
|
||||
}
|
||||
|
||||
impl<'a, SpeciesMeta> core::ops::Index<&'a Species> for AllSpecies<SpeciesMeta> {
|
||||
@ -107,11 +109,12 @@ impl<'a, SpeciesMeta> core::ops::Index<&'a Species> for AllSpecies<SpeciesMeta>
|
||||
Species::ShamanicSpirit => &self.shamanic_spirit,
|
||||
Species::Jiangshi => &self.jiangshi,
|
||||
Species::TreasureEgg => &self.treasure_egg,
|
||||
Species::GnarlingChieftain => &self.gnarling_chieftain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const ALL_SPECIES: [Species; 19] = [
|
||||
pub const ALL_SPECIES: [Species; 20] = [
|
||||
Species::Gnome,
|
||||
Species::Sahagin,
|
||||
Species::Adlet,
|
||||
@ -131,6 +134,7 @@ pub const ALL_SPECIES: [Species; 19] = [
|
||||
Species::ShamanicSpirit,
|
||||
Species::Jiangshi,
|
||||
Species::TreasureEgg,
|
||||
Species::GnarlingChieftain,
|
||||
];
|
||||
|
||||
impl<'a, SpeciesMeta: 'a> IntoIterator for &'a AllSpecies<SpeciesMeta> {
|
||||
|
@ -226,11 +226,17 @@ impl Body {
|
||||
Body::Dragon(_) => 1.0,
|
||||
Body::BirdLarge(_) => 7.0,
|
||||
Body::FishSmall(_) => 7.0,
|
||||
Body::BipedLarge(_) => 2.7,
|
||||
Body::BipedLarge(biped_large) => match biped_large.species {
|
||||
biped_large::Species::Harvester => 2.0,
|
||||
_ => 2.7,
|
||||
},
|
||||
Body::BipedSmall(_) => 3.5,
|
||||
Body::Object(_) => 2.0,
|
||||
Body::ItemDrop(_) => 2.0,
|
||||
Body::Golem(_) => 2.0,
|
||||
Body::Golem(golem) => match golem.species {
|
||||
golem::Species::WoodGolem => 1.2,
|
||||
_ => 2.0,
|
||||
},
|
||||
Body::Theropod(theropod) => match theropod.species {
|
||||
theropod::Species::Archaeos => 2.3,
|
||||
theropod::Species::Odonto => 2.3,
|
||||
|
@ -543,7 +543,7 @@ impl SpriteKind {
|
||||
| SpriteKind::Gold => 0.6,
|
||||
SpriteKind::EnsnaringVines
|
||||
| SpriteKind::CavernLillypadBlue
|
||||
| SpriteKind::EnsnaringWeb => 0.1,
|
||||
| SpriteKind::EnsnaringWeb => 0.15,
|
||||
SpriteKind::LillyPads => 0.1,
|
||||
SpriteKind::WindowArabic | SpriteKind::BookshelfArabic => 1.9,
|
||||
SpriteKind::DecorSetArabic => 2.6,
|
||||
|
@ -206,9 +206,27 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
if matches!(
|
||||
physics_state.on_ground.and_then(|b| b.get_sprite()),
|
||||
Some(SpriteKind::EnsnaringVines) | Some(SpriteKind::EnsnaringWeb)
|
||||
Some(SpriteKind::EnsnaringVines)
|
||||
) {
|
||||
// If on ensnaring vines, apply ensnared debuff
|
||||
// If on ensnaring vines, apply partial ensnared debuff
|
||||
emitters.emit(BuffEvent {
|
||||
entity,
|
||||
buff_change: BuffChange::Add(Buff::new(
|
||||
BuffKind::Ensnared,
|
||||
BuffData::new(0.5, Some(Secs(0.1))),
|
||||
Vec::new(),
|
||||
BuffSource::World,
|
||||
*read_data.time,
|
||||
dest_info,
|
||||
None,
|
||||
)),
|
||||
});
|
||||
}
|
||||
if matches!(
|
||||
physics_state.on_ground.and_then(|b| b.get_sprite()),
|
||||
Some(SpriteKind::EnsnaringWeb)
|
||||
) {
|
||||
// If on ensnaring web, apply ensnared debuff
|
||||
emitters.emit(BuffEvent {
|
||||
entity,
|
||||
buff_change: BuffChange::Add(Buff::new(
|
||||
|
@ -1204,9 +1204,15 @@ impl<'a> AgentData<'a> {
|
||||
// Wield the weapon as running towards the target
|
||||
controller.push_action(ControlAction::Wield);
|
||||
|
||||
let min_attack_dist = (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE)
|
||||
* self.scale
|
||||
+ tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
|
||||
// Information for attack checks
|
||||
// 'min_attack_dist' uses DEFAULT_ATTACK_RANGE, while 'body_dist' does not
|
||||
let self_radius = self.body.map_or(0.5, |b| b.max_radius()) * self.scale;
|
||||
let self_attack_range =
|
||||
(self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE) * self.scale;
|
||||
let tgt_radius =
|
||||
tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
|
||||
let min_attack_dist = self_attack_range + tgt_radius;
|
||||
let body_dist = self_radius + tgt_radius;
|
||||
let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0);
|
||||
let angle = self
|
||||
.ori
|
||||
@ -1358,6 +1364,7 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
|
||||
let attack_data = AttackData {
|
||||
body_dist,
|
||||
min_attack_dist,
|
||||
dist_sqrd,
|
||||
angle,
|
||||
@ -1617,9 +1624,14 @@ impl<'a> AgentData<'a> {
|
||||
Tactic::Yeti => {
|
||||
self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
|
||||
},
|
||||
Tactic::Harvester => {
|
||||
self.handle_harvester_attack(agent, controller, &attack_data, tgt_data, read_data)
|
||||
},
|
||||
Tactic::Harvester => self.handle_harvester_attack(
|
||||
agent,
|
||||
controller,
|
||||
&attack_data,
|
||||
tgt_data,
|
||||
read_data,
|
||||
rng,
|
||||
),
|
||||
Tactic::Cardinal => self.handle_cardinal_attack(
|
||||
agent,
|
||||
controller,
|
||||
@ -1673,7 +1685,7 @@ impl<'a> AgentData<'a> {
|
||||
self.handle_mandragora(agent, controller, &attack_data, tgt_data, read_data)
|
||||
},
|
||||
Tactic::WoodGolem => {
|
||||
self.handle_wood_golem(agent, controller, &attack_data, tgt_data, read_data)
|
||||
self.handle_wood_golem(agent, controller, &attack_data, tgt_data, read_data, rng)
|
||||
},
|
||||
Tactic::GnarlingChieftain => self.handle_gnarling_chieftain(
|
||||
agent,
|
||||
|
@ -13,6 +13,7 @@ use common::{
|
||||
Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction,
|
||||
ControlEvent, Controller, Fluid, InputKind,
|
||||
},
|
||||
consts::GRAVITY,
|
||||
path::TraversalConfig,
|
||||
states::{
|
||||
self_buff,
|
||||
@ -27,6 +28,20 @@ use rand::{prelude::SliceRandom, Rng};
|
||||
use std::{f32::consts::PI, time::Duration};
|
||||
use vek::*;
|
||||
|
||||
// ground-level max range from projectile speed and launch height
|
||||
fn projectile_flat_range(speed: f32, height: f32) -> f32 {
|
||||
let w = speed.powi(2);
|
||||
let u = 0.5 * 2_f32.sqrt() * speed;
|
||||
(0.5 * w + u * (0.5 * w + 2.0 * GRAVITY * height).sqrt()) / GRAVITY
|
||||
}
|
||||
|
||||
// multi-projectile spread (in degrees) based on maximum of linear increase
|
||||
fn projectile_multi_angle(projectile_spread: f32, num_projectiles: u32) -> f32 {
|
||||
(180.0 / PI) * projectile_spread * (num_projectiles - 1) as f32
|
||||
}
|
||||
|
||||
fn rng_from_span(rng: &mut impl Rng, span: [f32; 2]) -> f32 { rng.gen_range(span[0]..=span[1]) }
|
||||
|
||||
impl<'a> AgentData<'a> {
|
||||
// Intended for any agent that has one attack, that attack is a melee attack,
|
||||
// and the agent is able to freely walk around
|
||||
@ -125,7 +140,7 @@ impl<'a> AgentData<'a> {
|
||||
// Behaviour parameters
|
||||
const STRAFE_DIST: f32 = 4.5;
|
||||
const STRAFE_SPEED_MULT: f32 = 0.75;
|
||||
const STRATE_SPIRAL_MULT: f32 = 0.8; // how quickly they close gap while strafing
|
||||
const STRAFE_SPIRAL_MULT: f32 = 0.8; // how quickly they close gap while strafing
|
||||
const BACKSTAB_SPEED_MULT: f32 = 0.3;
|
||||
|
||||
// Handle movement of agent
|
||||
@ -166,7 +181,7 @@ impl<'a> AgentData<'a> {
|
||||
.find(|move_dir| target_ori.xy().dot(**move_dir) < 0.0)
|
||||
{
|
||||
controller.inputs.move_dir =
|
||||
STRAFE_SPEED_MULT * (*move_dir - STRATE_SPIRAL_MULT * target_ori.xy());
|
||||
STRAFE_SPEED_MULT * (*move_dir - STRAFE_SPIRAL_MULT * target_ori.xy());
|
||||
}
|
||||
} else {
|
||||
// Aim for a point a given distance behind the target to prevent sideways
|
||||
@ -4756,16 +4771,49 @@ impl<'a> AgentData<'a> {
|
||||
attack_data: &AttackData,
|
||||
tgt_data: &TargetData,
|
||||
read_data: &ReadData,
|
||||
rng: &mut impl Rng,
|
||||
) {
|
||||
const VINE_CREATION_THRESHOLD: f32 = 0.50;
|
||||
const FIRE_BREATH_RANGE: f32 = 20.0;
|
||||
const MAX_PUMPKIN_RANGE: f32 = 50.0;
|
||||
// === reference ===
|
||||
// Inputs:
|
||||
// Primary: scythe
|
||||
// Secondary: firebreath
|
||||
// Auxiliary
|
||||
// 0: explosivepumpkin
|
||||
// 1: ensaringvines_sparse
|
||||
// 2: ensaringvines_dense
|
||||
|
||||
enum ActionStateConditions {
|
||||
ConditionHasSummonedVines = 0,
|
||||
}
|
||||
// === setup ===
|
||||
|
||||
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
|
||||
// --- static ---
|
||||
// behaviour parameters
|
||||
const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
|
||||
const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
|
||||
const PATH_RANGE_FACTOR: f32 = 0.4; // get comfortably in range, but give player room to breathe
|
||||
const SCYTHE_RANGE_FACTOR: f32 = 0.75; // start attack while suitably in range
|
||||
const SCYTHE_AIM_FACTOR: f32 = 0.7;
|
||||
const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
|
||||
const FIREBREATH_AIM_FACTOR: f32 = 0.8;
|
||||
const FIREBREATH_TIME_LIMIT: f32 = 4.0;
|
||||
const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; // cutoff sooner at close range
|
||||
const FIREBREATH_COOLDOWN: f32 = 3.5;
|
||||
const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
|
||||
const CLOSE_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 7.0]; // variation in attacks at close range
|
||||
const MID_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 4.5]; // ^ mid
|
||||
const FAR_PUMPKIN_COOLDOWN_SPAN: [f32; 2] = [3.0, 5.0]; // allows for pathing to player between throws
|
||||
|
||||
// conditions
|
||||
const HAS_SUMMONED_FIRST_VINES: usize = 0;
|
||||
const HAS_SUMMONED_SECOND_VINES: usize = 1;
|
||||
// timers
|
||||
const FIREBREATH: usize = 0;
|
||||
const MIXUP: usize = 1;
|
||||
const FAR_PUMPKIN: usize = 2;
|
||||
//counters
|
||||
const CLOSE_MIXUP_COOLDOWN: usize = 0;
|
||||
const MID_MIXUP_COOLDOWN: usize = 1;
|
||||
const FAR_PUMPKIN_COOLDOWN: usize = 2;
|
||||
|
||||
// line of sight check
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(
|
||||
self.pos,
|
||||
@ -4778,37 +4826,199 @@ impl<'a> AgentData<'a> {
|
||||
)
|
||||
};
|
||||
|
||||
if health_fraction < VINE_CREATION_THRESHOLD
|
||||
&& !agent.combat_state.conditions
|
||||
[ActionStateConditions::ConditionHasSummonedVines as usize]
|
||||
// --- dynamic ---
|
||||
// attack data
|
||||
let (scythe_range, scythe_angle) = {
|
||||
if let Some(AbilityData::BasicMelee { range, angle, .. }) =
|
||||
self.extract_ability(AbilityInput::Primary)
|
||||
{
|
||||
// Summon vines when reach threshold of health
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
|
||||
if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
|
||||
{
|
||||
agent.combat_state.conditions
|
||||
[ActionStateConditions::ConditionHasSummonedVines as usize] = true;
|
||||
(range, angle)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
} else if attack_data.dist_sqrd < FIRE_BREATH_RANGE.powi(2) {
|
||||
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
|
||||
};
|
||||
let (firebreath_range, firebreath_angle) = {
|
||||
if let Some(AbilityData::BasicBeam { range, angle, .. }) =
|
||||
self.extract_ability(AbilityInput::Secondary)
|
||||
{
|
||||
(range, angle)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
};
|
||||
let pumpkin_speed = {
|
||||
if let Some(AbilityData::BasicRanged {
|
||||
projectile_speed, ..
|
||||
}) = self.extract_ability(AbilityInput::Auxiliary(0))
|
||||
{
|
||||
projectile_speed
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
// calculated attack data
|
||||
let pumpkin_max_range =
|
||||
projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
|
||||
|
||||
// character state info
|
||||
let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
|
||||
let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
|
||||
let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
|
||||
let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
|
||||
data.timer
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let is_using_mixup = is_using_firebreath || is_using_pumpkin;
|
||||
|
||||
// initialise randomised cooldowns
|
||||
if !agent.combat_state.initialized {
|
||||
agent.combat_state.initialized = true;
|
||||
agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
|
||||
rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
|
||||
agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
|
||||
rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
|
||||
agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
|
||||
rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
|
||||
}
|
||||
|
||||
// === main ===
|
||||
|
||||
// --- timers ---
|
||||
if is_in_summon_recovery {
|
||||
// reset all timers when done summoning
|
||||
agent.combat_state.timers[FIREBREATH] = 0.0;
|
||||
agent.combat_state.timers[MIXUP] = 0.0;
|
||||
agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
|
||||
} else {
|
||||
// handle state timers
|
||||
if is_using_firebreath {
|
||||
agent.combat_state.timers[FIREBREATH] = 0.0;
|
||||
} else {
|
||||
agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
|
||||
}
|
||||
if is_using_mixup {
|
||||
agent.combat_state.timers[MIXUP] = 0.0;
|
||||
} else {
|
||||
agent.combat_state.timers[MIXUP] += read_data.dt.0;
|
||||
}
|
||||
if is_using_pumpkin {
|
||||
agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
|
||||
} else {
|
||||
agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- attacks ---
|
||||
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
|
||||
// second vine summon
|
||||
if health_fraction < SECOND_VINE_CREATION_THRESHOLD
|
||||
&& !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
|
||||
{
|
||||
// use the dense vine summon
|
||||
controller.push_basic_input(InputKind::Ability(2));
|
||||
// wait till recovery before finishing
|
||||
if is_in_summon_recovery {
|
||||
agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
|
||||
}
|
||||
}
|
||||
// first vine summon
|
||||
else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
|
||||
&& !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
|
||||
{
|
||||
// use the sparse vine summon
|
||||
controller.push_basic_input(InputKind::Ability(1));
|
||||
// wait till recovery before finishing
|
||||
if is_in_summon_recovery {
|
||||
agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
|
||||
}
|
||||
}
|
||||
// close range
|
||||
else if attack_data.dist_sqrd
|
||||
< (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
|
||||
{
|
||||
// if using firebreath, keep going under short time limit
|
||||
if is_using_firebreath
|
||||
&& firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
|
||||
{
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
}
|
||||
// in scythe angle
|
||||
if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
|
||||
// on timer, randomly mixup attacks
|
||||
if agent.combat_state.timers[MIXUP]
|
||||
> agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
|
||||
// for now, no line of sight check for consitency in attacks
|
||||
{
|
||||
// if on firebreath cooldown, throw pumpkin
|
||||
if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
// otherwise, randomise between firebreath and pumpkin
|
||||
else if rng.gen_bool(0.5) {
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
} else {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
// reset mixup cooldown if actually being used
|
||||
if is_using_mixup {
|
||||
agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
|
||||
rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
|
||||
}
|
||||
}
|
||||
// default to using scythe melee
|
||||
else {
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
}
|
||||
}
|
||||
// mid range (line of sight not needed for these 'suppressing' attacks)
|
||||
} else if attack_data.dist_sqrd < firebreath_range.powi(2) {
|
||||
// if using firebreath, keep going under full time limit
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if is_using_firebreath
|
||||
&& firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
|
||||
{
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
}
|
||||
// start using firebreath if close enough, in angle, and off cooldown
|
||||
else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
|
||||
&& attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
|
||||
&& agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
|
||||
{
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
}
|
||||
// on mixup timer, throw a pumpkin
|
||||
else if agent.combat_state.timers[MIXUP]
|
||||
> agent.combat_state.counters[MID_MIXUP_COOLDOWN]
|
||||
{
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
// reset mixup cooldown if pumpkin is actually being used
|
||||
if is_using_pumpkin {
|
||||
agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
|
||||
rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
|
||||
}
|
||||
}
|
||||
}
|
||||
// long range (with line of sight)
|
||||
else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
|
||||
&& agent.combat_state.timers[FAR_PUMPKIN]
|
||||
> agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
|
||||
&& line_of_sight_with_target()
|
||||
{
|
||||
// Keep breathing fire if close enough, can see target, and have not been
|
||||
// breathing for more than 5 seconds
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
} else if attack_data.in_min_range() && attack_data.angle < 60.0 {
|
||||
// Scythe them if they're in range and angle
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
} else if attack_data.angle < 30.0 && line_of_sight_with_target() {
|
||||
// Start breathing fire at them if close enough, in angle, and can see target
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
// throw pumpkin
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
// reset pumpkin cooldown if actually being used
|
||||
if is_using_pumpkin {
|
||||
agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
|
||||
rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
|
||||
}
|
||||
} else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) && line_of_sight_with_target() {
|
||||
// Throw a pumpkin at them if close enough and can see them
|
||||
controller.push_basic_input(InputKind::Ability(1));
|
||||
}
|
||||
// Always attempt to path towards target
|
||||
|
||||
// --- movement ---
|
||||
// closing gap
|
||||
if attack_data.dist_sqrd
|
||||
> (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
|
||||
{
|
||||
self.path_toward_target(
|
||||
agent,
|
||||
controller,
|
||||
@ -4818,6 +5028,16 @@ impl<'a> AgentData<'a> {
|
||||
None,
|
||||
);
|
||||
}
|
||||
// closing angle
|
||||
else if attack_data.angle > 0.0 {
|
||||
// some movement is required to trigger re-orientation
|
||||
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
|
||||
.xy()
|
||||
.try_normalized()
|
||||
.unwrap_or_else(Vec2::zero)
|
||||
* 0.001; // scaled way down to minimise position change and keep close rotation consistent
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_frostgigas_attack(
|
||||
&self,
|
||||
@ -5666,7 +5886,7 @@ impl<'a> AgentData<'a> {
|
||||
tgt_data: &TargetData,
|
||||
read_data: &ReadData,
|
||||
) {
|
||||
const SCREAM_RANGE: f32 = 10.0;
|
||||
const SCREAM_RANGE: f32 = 10.0; // hard-coded from scream.ron
|
||||
|
||||
enum ActionStateFCounters {
|
||||
FCounterHealthThreshold = 0,
|
||||
@ -5737,59 +5957,155 @@ impl<'a> AgentData<'a> {
|
||||
attack_data: &AttackData,
|
||||
tgt_data: &TargetData,
|
||||
read_data: &ReadData,
|
||||
rng: &mut impl Rng,
|
||||
) {
|
||||
const SHOCKWAVE_RANGE: f32 = 25.0;
|
||||
const SHOCKWAVE_WAIT_TIME: f32 = 7.5;
|
||||
const SPIN_WAIT_TIME: f32 = 3.0;
|
||||
// === reference ===
|
||||
|
||||
enum ActionStateTimers {
|
||||
TimerSpinWait = 0,
|
||||
TimerShockwaveWait,
|
||||
// Inputs:
|
||||
// Primary: strike
|
||||
// Secondary: spin
|
||||
// Auxiliary
|
||||
// 0: shockwave
|
||||
|
||||
// === setup ===
|
||||
|
||||
// --- static ---
|
||||
// behaviour parameters
|
||||
const PATH_RANGE_FACTOR: f32 = 0.3; // get comfortably in range, but give player room to breathe
|
||||
const STRIKE_RANGE_FACTOR: f32 = 0.6; // start attack while suitably in range
|
||||
const STRIKE_AIM_FACTOR: f32 = 0.7;
|
||||
const SPIN_RANGE_FACTOR: f32 = 0.6;
|
||||
const SPIN_COOLDOWN: f32 = 1.5;
|
||||
const SPIN_RELAX_FACTOR: f32 = 0.2;
|
||||
const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
|
||||
const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
|
||||
const SHOCKWAVE_COOLDOWN: f32 = 5.0;
|
||||
const MIXUP_COOLDOWN: f32 = 2.5;
|
||||
const MIXUP_RELAX_FACTOR: f32 = 0.3;
|
||||
|
||||
// timers
|
||||
const SPIN: usize = 0;
|
||||
const SHOCKWAVE: usize = 1;
|
||||
const MIXUP: usize = 2;
|
||||
|
||||
// --- dynamic ---
|
||||
// behaviour parameters
|
||||
let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
|
||||
|
||||
// attack data
|
||||
let (strike_range, strike_angle) = {
|
||||
if let Some(AbilityData::BasicMelee { range, angle, .. }) =
|
||||
self.extract_ability(AbilityInput::Primary)
|
||||
{
|
||||
(range, angle)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
};
|
||||
let spin_range = {
|
||||
if let Some(AbilityData::BasicMelee { range, .. }) =
|
||||
self.extract_ability(AbilityInput::Secondary)
|
||||
{
|
||||
range
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let (shockwave_max_range, shockwave_angle) = {
|
||||
if let Some(AbilityData::Shockwave { range, angle, .. }) =
|
||||
self.extract_ability(AbilityInput::Auxiliary(0))
|
||||
{
|
||||
(range, angle)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
};
|
||||
|
||||
// After spinning, reset timer
|
||||
// re-used checks (makes separating timers and attacks easier)
|
||||
let is_in_spin_range = attack_data.dist_sqrd
|
||||
< (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
|
||||
let is_in_strike_range = attack_data.dist_sqrd
|
||||
< (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
|
||||
let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
|
||||
|
||||
// === main ===
|
||||
|
||||
// --- timers ---
|
||||
// spin
|
||||
let current_input = self.char_state.ability_info().map(|ai| ai.input);
|
||||
if matches!(current_input, Some(InputKind::Secondary)) {
|
||||
agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] = 0.0;
|
||||
// reset when spinning
|
||||
agent.combat_state.timers[SPIN] = 0.0;
|
||||
agent.combat_state.timers[MIXUP] = 0.0;
|
||||
} else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
|
||||
// increment within spin range and not in strike range + angle
|
||||
agent.combat_state.timers[SPIN] += read_data.dt.0;
|
||||
} else {
|
||||
// relax towards zero otherwise
|
||||
agent.combat_state.timers[SPIN] =
|
||||
(agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
|
||||
}
|
||||
// shockwave
|
||||
if matches!(self.char_state, CharacterState::Shockwave(_)) {
|
||||
// reset when using shockwave
|
||||
agent.combat_state.timers[SHOCKWAVE] = 0.0;
|
||||
agent.combat_state.timers[MIXUP] = 0.0;
|
||||
} else {
|
||||
// increment otherwise
|
||||
agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
|
||||
}
|
||||
// mixup
|
||||
if is_in_strike_range && is_in_strike_angle {
|
||||
// increment within strike range and angle
|
||||
agent.combat_state.timers[MIXUP] += read_data.dt.0;
|
||||
} else {
|
||||
// relax towards zero otherwise
|
||||
agent.combat_state.timers[MIXUP] =
|
||||
(agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
|
||||
}
|
||||
|
||||
if attack_data.in_min_range() {
|
||||
// If in minimum range
|
||||
if agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] > SPIN_WAIT_TIME
|
||||
{
|
||||
// If it's been too long since able to hit target, spin
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
} else if attack_data.angle < 30.0 {
|
||||
// Else if in angle to strike, strike
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
} else {
|
||||
// Else increment spin timer
|
||||
agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] +=
|
||||
read_data.dt.0;
|
||||
// If not in angle, apply slight movement so golem orients itself correctly
|
||||
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
|
||||
.xy()
|
||||
.try_normalized()
|
||||
.unwrap_or_else(Vec2::zero)
|
||||
* 0.01;
|
||||
// --- attacks ---
|
||||
// strike range and angle
|
||||
if is_in_strike_range && is_in_strike_angle {
|
||||
// on timer, randomly mixup between all attacks
|
||||
if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
|
||||
let randomise: u8 = rng.gen_range(1..=3);
|
||||
match randomise {
|
||||
1 => controller.push_basic_input(InputKind::Ability(0)), // shockwave
|
||||
2 => controller.push_basic_input(InputKind::Primary), // strike
|
||||
_ => controller.push_basic_input(InputKind::Secondary), // spin
|
||||
}
|
||||
} else {
|
||||
// Else if too far for melee
|
||||
if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 45.0 {
|
||||
// Shockwave if close enough and haven't shockwaved too recently
|
||||
if agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize]
|
||||
> SHOCKWAVE_WAIT_TIME
|
||||
}
|
||||
// default to strike
|
||||
else {
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
}
|
||||
}
|
||||
// spin range (or out of angle in strike range)
|
||||
else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
|
||||
// on timer, use spin attack to try and hit evasive target
|
||||
if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
}
|
||||
// otherwise, close angle (no action required)
|
||||
}
|
||||
// shockwave range and angle
|
||||
else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
|
||||
&& attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
|
||||
&& attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
|
||||
{
|
||||
// on timer, use shockwave
|
||||
if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
if matches!(self.char_state, CharacterState::Shockwave(_)) {
|
||||
agent.combat_state.timers[ActionStateTimers::TimerShockwaveWait as usize] = 0.0;
|
||||
} else {
|
||||
agent.combat_state.timers[ActionStateTimers::TimerShockwaveWait as usize] +=
|
||||
read_data.dt.0;
|
||||
// otherwise, close gap and/or angle (no action required)
|
||||
}
|
||||
}
|
||||
// And always try to path towards target
|
||||
|
||||
// --- movement ---
|
||||
// closing gap
|
||||
if attack_data.dist_sqrd
|
||||
> (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
|
||||
{
|
||||
self.path_toward_target(
|
||||
agent,
|
||||
controller,
|
||||
@ -5799,6 +6115,15 @@ impl<'a> AgentData<'a> {
|
||||
None,
|
||||
);
|
||||
}
|
||||
// closing angle
|
||||
else if attack_data.angle > 0.0 {
|
||||
// some movement is required to trigger re-orientation
|
||||
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
|
||||
.xy()
|
||||
.try_normalized()
|
||||
.unwrap_or_else(Vec2::zero)
|
||||
* 0.001; // scaled way down to minimise position change and keep close rotation consistent
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_gnarling_chieftain(
|
||||
@ -5810,60 +6135,42 @@ impl<'a> AgentData<'a> {
|
||||
read_data: &ReadData,
|
||||
rng: &mut impl Rng,
|
||||
) {
|
||||
const TOTEM_TIMER: f32 = 10.0;
|
||||
const HEAVY_ATTACK_WAIT_TIME: f32 = 15.0;
|
||||
// === reference ===
|
||||
// Inputs
|
||||
// Primary: flamestrike
|
||||
// Secondary: firebarrage
|
||||
// Auxiliary
|
||||
// 0: fireshockwave
|
||||
// 1: redtotem
|
||||
// 2: greentotem
|
||||
// 3: whitetotem
|
||||
|
||||
enum ActionStateTimers {
|
||||
TimerSummonTotem = 0,
|
||||
TimerShockwave,
|
||||
}
|
||||
// Handle timers
|
||||
agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] += read_data.dt.0;
|
||||
match self.char_state {
|
||||
CharacterState::BasicSummon(_) => {
|
||||
agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] = 0.0
|
||||
},
|
||||
CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
|
||||
agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] = 0.0
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
// === setup ===
|
||||
|
||||
if !agent.combat_state.initialized {
|
||||
// If not initialized yet, start out by summoning green totem
|
||||
controller.push_basic_input(InputKind::Ability(2));
|
||||
if matches!(self.char_state, CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover)
|
||||
{
|
||||
agent.combat_state.initialized = true;
|
||||
}
|
||||
} else if agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize]
|
||||
> TOTEM_TIMER
|
||||
{
|
||||
// If time to summon a totem, do it
|
||||
let input = rng.gen_range(1..=3);
|
||||
let buff_kind = match input {
|
||||
2 => Some(BuffKind::Regeneration),
|
||||
3 => Some(BuffKind::Hastened),
|
||||
_ => None,
|
||||
};
|
||||
if buff_kind.map_or(true, |b| self.has_buff(read_data, b))
|
||||
&& matches!(self.char_state, CharacterState::Wielding { .. })
|
||||
{
|
||||
// If already under effects of buff from totem that would be summoned, don't
|
||||
// summon totem (doesn't work for red totems since that applies debuff to
|
||||
// enemies instead)
|
||||
agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] = 0.0;
|
||||
} else {
|
||||
controller.push_basic_input(InputKind::Ability(input));
|
||||
}
|
||||
} else if agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize]
|
||||
> HEAVY_ATTACK_WAIT_TIME
|
||||
{
|
||||
// Else if time for a heavy attack
|
||||
if attack_data.in_min_range() {
|
||||
// If in range, shockwave
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
} else if entities_have_line_of_sight(
|
||||
// --- static ---
|
||||
// behaviour parameters
|
||||
const PATH_RANGE_FACTOR: f32 = 0.4;
|
||||
const STRIKE_RANGE_FACTOR: f32 = 0.7;
|
||||
const STRIKE_AIM_FACTOR: f32 = 0.8;
|
||||
const BARRAGE_RANGE_FACTOR: f32 = 0.8;
|
||||
const BARRAGE_AIM_FACTOR: f32 = 0.65;
|
||||
const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
|
||||
const TOTEM_COOLDOWN: f32 = 25.0;
|
||||
const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
|
||||
const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
|
||||
const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
|
||||
|
||||
// conditions
|
||||
const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
|
||||
// timers
|
||||
const SUMMON_TOTEM: usize = 0;
|
||||
const HEAVY_ATTACK: usize = 1;
|
||||
// counters
|
||||
const HEAVY_ATTACK_COOLDOWN: usize = 0;
|
||||
|
||||
// line of sight check
|
||||
let line_of_sight_with_target = || {
|
||||
entities_have_line_of_sight(
|
||||
self.pos,
|
||||
self.body,
|
||||
self.scale,
|
||||
@ -5871,27 +6178,153 @@ impl<'a> AgentData<'a> {
|
||||
tgt_data.body,
|
||||
tgt_data.scale,
|
||||
read_data,
|
||||
) {
|
||||
// Else if in sight, barrage
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
)
|
||||
};
|
||||
|
||||
// --- dynamic ---
|
||||
// attack data
|
||||
let (strike_range, strike_angle) = {
|
||||
if let Some(AbilityData::BasicMelee { range, angle, .. }) =
|
||||
self.extract_ability(AbilityInput::Primary)
|
||||
{
|
||||
(range, angle)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
} else if attack_data.in_min_range() {
|
||||
// Else if not time to use anything fancy, if in range and angle, strike them
|
||||
if attack_data.angle < 20.0 {
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] +=
|
||||
read_data.dt.0;
|
||||
};
|
||||
let (barrage_speed, barrage_spread, barrage_count) = {
|
||||
if let Some(AbilityData::BasicRanged {
|
||||
projectile_speed,
|
||||
projectile_spread,
|
||||
num_projectiles,
|
||||
..
|
||||
}) = self.extract_ability(AbilityInput::Secondary)
|
||||
{
|
||||
(projectile_speed, projectile_spread, num_projectiles)
|
||||
} else {
|
||||
(0.0, 0.0, 0)
|
||||
}
|
||||
};
|
||||
let shockwave_range = {
|
||||
if let Some(AbilityData::Shockwave { range, .. }) =
|
||||
self.extract_ability(AbilityInput::Auxiliary(0))
|
||||
{
|
||||
range
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
|
||||
// calculated attack data
|
||||
let barrage_max_range =
|
||||
projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
|
||||
let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
|
||||
|
||||
// re-used checks
|
||||
let is_in_strike_range = attack_data.dist_sqrd
|
||||
< (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
|
||||
let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
|
||||
|
||||
// initialise randomised cooldowns
|
||||
if !agent.combat_state.initialized {
|
||||
agent.combat_state.initialized = true;
|
||||
agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
|
||||
rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
|
||||
}
|
||||
|
||||
// === main ===
|
||||
|
||||
// --- timers ---
|
||||
// resets
|
||||
match self.char_state {
|
||||
CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
|
||||
// reset when finished summoning
|
||||
agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
|
||||
agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
|
||||
},
|
||||
CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
|
||||
// reset heavy attack on either ability
|
||||
agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
|
||||
agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
|
||||
rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
// totem (always increment)
|
||||
agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
|
||||
// heavy attack (increment at different rates)
|
||||
if is_in_strike_range {
|
||||
// recharge at standard rate in strike range and angle
|
||||
if is_in_strike_angle {
|
||||
agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
|
||||
} else {
|
||||
// If not in angle, charge heavy attack faster
|
||||
agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] +=
|
||||
read_data.dt.0 * 5.0;
|
||||
agent.combat_state.counters[HEAVY_ATTACK] +=
|
||||
read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
|
||||
}
|
||||
} else {
|
||||
// If not in range, charge heavy attack faster
|
||||
agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] +=
|
||||
read_data.dt.0 * 3.3;
|
||||
agent.combat_state.counters[HEAVY_ATTACK] +=
|
||||
read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
|
||||
}
|
||||
|
||||
// --- attacks ---
|
||||
// start by summoning green totem
|
||||
if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
|
||||
controller.push_basic_input(InputKind::Ability(2));
|
||||
}
|
||||
// on timer, summon a new random totem
|
||||
else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
|
||||
controller.push_basic_input(InputKind::Ability(rng.gen_range(1..=3)));
|
||||
}
|
||||
// on timer and in range, use a heavy attack
|
||||
// assumes: barrange_max_range * BARRAGE_RANGE_FACTOR > shockwave_range *
|
||||
// SHOCKWAVE_RANGE_FACTOR
|
||||
else if agent.combat_state.counters[HEAVY_ATTACK]
|
||||
> agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
|
||||
&& attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
|
||||
{
|
||||
// has line of sight
|
||||
if line_of_sight_with_target() {
|
||||
// out of barrage angle, use shockwave
|
||||
if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
// in shockwave range, randomise between barrage and shockwave
|
||||
else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
|
||||
{
|
||||
if rng.gen_bool(0.5) {
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
} else {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
}
|
||||
// in range and angle, use barrage
|
||||
else {
|
||||
controller.push_basic_input(InputKind::Secondary);
|
||||
}
|
||||
// otherwise, close gap and/or angle (no action required)
|
||||
}
|
||||
// no line of sight
|
||||
else {
|
||||
// in range, use shockwave
|
||||
if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
|
||||
controller.push_basic_input(InputKind::Ability(0));
|
||||
}
|
||||
// otherwise, close gap (no action required)
|
||||
}
|
||||
}
|
||||
// if viable, default to flamestrike
|
||||
else if is_in_strike_range && is_in_strike_angle {
|
||||
controller.push_basic_input(InputKind::Primary);
|
||||
}
|
||||
// otherwise, close gap and/or angle (no action required)
|
||||
|
||||
// --- movement ---
|
||||
// closing gap
|
||||
if attack_data.dist_sqrd
|
||||
> (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
|
||||
{
|
||||
self.path_toward_target(
|
||||
agent,
|
||||
controller,
|
||||
@ -5901,6 +6334,16 @@ impl<'a> AgentData<'a> {
|
||||
None,
|
||||
);
|
||||
}
|
||||
// closing angle
|
||||
else if attack_data.angle > 0.0 {
|
||||
// some movement is required to trigger re-orientation
|
||||
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
|
||||
.xy()
|
||||
.try_normalized()
|
||||
.unwrap_or_else(Vec2::zero)
|
||||
* 0.001; // scaled way down to minimise position change and keep close rotation consistent
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_sword_simple_attack(
|
||||
&self,
|
||||
|
@ -145,6 +145,7 @@ impl<'a> TargetData<'a> {
|
||||
}
|
||||
|
||||
pub struct AttackData {
|
||||
pub body_dist: f32,
|
||||
pub min_attack_dist: f32,
|
||||
pub dist_sqrd: f32,
|
||||
pub angle: f32,
|
||||
@ -498,6 +499,8 @@ pub enum AbilityData {
|
||||
BasicRanged {
|
||||
energy: f32,
|
||||
projectile_speed: f32,
|
||||
projectile_spread: f32,
|
||||
num_projectiles: u32,
|
||||
},
|
||||
BasicMelee {
|
||||
energy: f32,
|
||||
@ -666,10 +669,14 @@ impl AbilityData {
|
||||
BasicRanged {
|
||||
energy_cost,
|
||||
projectile_speed,
|
||||
projectile_spread,
|
||||
num_projectiles,
|
||||
..
|
||||
} => Self::BasicRanged {
|
||||
energy: *energy_cost,
|
||||
projectile_speed: *projectile_speed,
|
||||
projectile_spread: *projectile_spread,
|
||||
num_projectiles: *num_projectiles,
|
||||
},
|
||||
BasicMelee {
|
||||
energy_cost,
|
||||
@ -929,6 +936,8 @@ impl AbilityData {
|
||||
BasicRanged {
|
||||
energy,
|
||||
projectile_speed,
|
||||
projectile_spread: _,
|
||||
num_projectiles: _,
|
||||
} => ranged_check(*projectile_speed) && energy_check(*energy),
|
||||
BasicMelee {
|
||||
energy,
|
||||
|
@ -113,7 +113,8 @@ impl Animation for SpriteSummonAnimation {
|
||||
* Quaternion::rotation_y(move2 * -0.1);
|
||||
},
|
||||
Some(ToolKind::Natural) => match ability_id {
|
||||
Some("common.abilities.custom.harvester.ensnaringvines") => {
|
||||
Some("common.abilities.custom.harvester.ensnaringvines_sparse")
|
||||
| Some("common.abilities.custom.harvester.ensnaringvines_dense") => {
|
||||
let (move1, move1pow, move2, move3) = match stage_section {
|
||||
Some(StageSection::Buildup) => (anim_time, anim_time.powf(0.1), 0.0, 0.0),
|
||||
Some(StageSection::Action) => {
|
||||
|
@ -168,6 +168,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (-0.5, 4.5),
|
||||
(Jiangshi, _) => (-1.0, 6.5),
|
||||
(TreasureEgg, _) => (-1.0, 9.0),
|
||||
(GnarlingChieftain, _) => (0.0, 6.0),
|
||||
},
|
||||
chest: match (body.species, body.body_type) {
|
||||
(Gnome, _) => (0.0, 9.0),
|
||||
@ -189,6 +190,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (0.0, 14.5),
|
||||
(Jiangshi, _) => (0.0, 14.0),
|
||||
(TreasureEgg, _) => (0.0, 3.0),
|
||||
(GnarlingChieftain, _) => (0.0, 7.5),
|
||||
},
|
||||
pants: match (body.species, body.body_type) {
|
||||
(Gnome, _) => (0.0, -3.0),
|
||||
@ -210,6 +212,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (0.0, -8.0),
|
||||
(Jiangshi, _) => (0.5, -6.0),
|
||||
(TreasureEgg, _) => (0.0, 1.0),
|
||||
(GnarlingChieftain, _) => (0.0, -3.0),
|
||||
},
|
||||
tail: match (body.species, body.body_type) {
|
||||
(Gnome, _) => (0.0, 0.0),
|
||||
@ -231,6 +234,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (0.0, 0.0),
|
||||
(Jiangshi, _) => (0.0, 0.0),
|
||||
(TreasureEgg, _) => (0.0, 0.0),
|
||||
(GnarlingChieftain, _) => (-2.0, 1.5),
|
||||
},
|
||||
hand: match (body.species, body.body_type) {
|
||||
(Gnome, _) => (4.0, 0.5, -1.0),
|
||||
@ -252,6 +256,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (5.0, 0.0, 1.0),
|
||||
(Jiangshi, _) => (5.0, -1.0, 3.0),
|
||||
(TreasureEgg, _) => (5.0, 2.0, 5.0),
|
||||
(GnarlingChieftain, _) => (4.0, 0.0, 1.5),
|
||||
},
|
||||
foot: match (body.species, body.body_type) {
|
||||
(Gnome, _) => (3.0, 0.0, 4.0),
|
||||
@ -273,6 +278,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (3.5, 3.0, 7.0),
|
||||
(Jiangshi, _) => (3.0, 0.0, 8.0),
|
||||
(TreasureEgg, _) => (2.0, 0.5, 4.0),
|
||||
(GnarlingChieftain, _) => (2.5, 1.0, 5.0),
|
||||
},
|
||||
grip: match (body.species, body.body_type) {
|
||||
(Gnome, _) => (0.0, 0.0, 5.0),
|
||||
@ -294,6 +300,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => (0.0, 0.0, 8.0),
|
||||
(Jiangshi, _) => (0.0, 0.0, 8.0),
|
||||
(TreasureEgg, _) => (0.0, 0.0, 7.0),
|
||||
(GnarlingChieftain, _) => (0.0, 0.0, 7.0),
|
||||
},
|
||||
scaler: match (body.species, body.body_type) {
|
||||
(Gnome, _) => 0.8,
|
||||
@ -315,6 +322,7 @@ impl<'a> From<&'a Body> for SkeletonAttr {
|
||||
(ShamanicSpirit, _) => 1.0,
|
||||
(Jiangshi, _) => 1.0,
|
||||
(TreasureEgg, _) => 1.0,
|
||||
(GnarlingChieftain, _) => 0.8,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1894,11 +1894,11 @@ impl ParticleMgr {
|
||||
kind: buff::BuffKind::Burning,
|
||||
..
|
||||
} => {
|
||||
let num_particles = aura.radius.powi(2) * dt / 250.0;
|
||||
let num_particles = num_particles.floor() as usize
|
||||
+ usize::from(rng.gen_bool(f64::from(num_particles % 1.0)));
|
||||
self.particles
|
||||
.resize_with(self.particles.len() + num_particles, || {
|
||||
let heartbeats = self.scheduler.heartbeats(Duration::from_millis(5));
|
||||
self.particles.resize_with(
|
||||
self.particles.len()
|
||||
+ aura.radius.powi(2) as usize * usize::from(heartbeats) / 300,
|
||||
|| {
|
||||
let rand_pos = {
|
||||
let theta = rng.gen::<f32>() * TAU;
|
||||
let radius = aura.radius * rng.gen::<f32>().sqrt();
|
||||
@ -1918,7 +1918,8 @@ impl ParticleMgr {
|
||||
rand_pos.with_z(pos.z),
|
||||
rand_pos.with_z(pos.z + 1.0),
|
||||
)
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
aura::AuraKind::Buff {
|
||||
kind: buff::BuffKind::Hastened,
|
||||
|
@ -336,6 +336,7 @@ impl GnarlingFortification {
|
||||
max: rpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
|
||||
};
|
||||
|
||||
// tunnel junctions
|
||||
for terminal in &self.tunnels.terminals {
|
||||
if area.contains_point(terminal.xy() - self.origin) {
|
||||
let chance = dynamic_rng.gen_range(0..10);
|
||||
@ -362,6 +363,8 @@ impl GnarlingFortification {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// harvester room
|
||||
if area.contains_point(self.tunnels.end.xy() - self.origin) {
|
||||
let boss_room_offset = (self.tunnels.end.xy() - self.tunnels.start.xy())
|
||||
.map(|e| if e < 0 { -20 } else { 20 });
|
||||
@ -371,41 +374,114 @@ impl GnarlingFortification {
|
||||
));
|
||||
}
|
||||
|
||||
// above-ground structures
|
||||
for (loc, pos, _ori) in &self.structure_locations {
|
||||
let wpos = *pos + self.origin;
|
||||
if area.contains_point(pos.xy()) {
|
||||
match loc {
|
||||
GnarlingStructure::Hut => {
|
||||
let num = dynamic_rng.gen_range(1..=3);
|
||||
for _ in 0..num {
|
||||
const NUM_HUT_GNARLINGS: [i32; 2] = [1, 2];
|
||||
let num =
|
||||
dynamic_rng.gen_range(NUM_HUT_GNARLINGS[0]..=NUM_HUT_GNARLINGS[1]);
|
||||
for _ in 1..=num {
|
||||
supplement.add_entity(random_gnarling(wpos, dynamic_rng));
|
||||
}
|
||||
},
|
||||
GnarlingStructure::VeloriteHut => {
|
||||
let num = dynamic_rng.gen_range(1..=4);
|
||||
for _ in 0..num {
|
||||
const NUM_VELO_GNARLINGS: i32 = 4;
|
||||
const GOLEM_SPAWN_THRESHOLD: i32 = 2;
|
||||
const VELO_HEIGHT: i32 = 12;
|
||||
const GROUND_HEIGHT: i32 = 8;
|
||||
let num = dynamic_rng.gen_range(1..=NUM_VELO_GNARLINGS);
|
||||
for _ in 1..=num {
|
||||
supplement.add_entity(random_gnarling(
|
||||
wpos.xy().with_z(wpos.z + 12),
|
||||
wpos.xy().with_z(wpos.z + VELO_HEIGHT),
|
||||
dynamic_rng,
|
||||
));
|
||||
}
|
||||
if num <= GOLEM_SPAWN_THRESHOLD {
|
||||
// wooden golem (with oriented spawn)
|
||||
let x_offset;
|
||||
let y_offset;
|
||||
match _ori {
|
||||
Dir::X => {
|
||||
x_offset = 8;
|
||||
y_offset = 8;
|
||||
},
|
||||
Dir::NegX => {
|
||||
x_offset = -8;
|
||||
y_offset = 8;
|
||||
},
|
||||
Dir::Y => {
|
||||
x_offset = 8;
|
||||
y_offset = -8;
|
||||
},
|
||||
Dir::NegY => {
|
||||
x_offset = -8;
|
||||
y_offset = -8;
|
||||
},
|
||||
}
|
||||
let pos = Vec3::new(
|
||||
wpos.x + x_offset,
|
||||
wpos.y + y_offset,
|
||||
wpos.z + GROUND_HEIGHT,
|
||||
);
|
||||
supplement.add_entity(wood_golem(pos, dynamic_rng));
|
||||
}
|
||||
},
|
||||
GnarlingStructure::Banner => {},
|
||||
GnarlingStructure::ChieftainHut => {
|
||||
let pos = wpos.xy().with_z(wpos.z + 8);
|
||||
// inside hut
|
||||
const FLOOR_HEIGHT: i32 = 8;
|
||||
let pos = wpos.xy().with_z(wpos.z + FLOOR_HEIGHT);
|
||||
supplement.add_entity(gnarling_chieftain(pos, dynamic_rng));
|
||||
for _ in 0..2 {
|
||||
supplement.add_entity(wood_golem(pos, dynamic_rng));
|
||||
supplement.add_entity(gnarling_logger(pos, dynamic_rng));
|
||||
supplement.add_entity(gnarling_mugger(pos, dynamic_rng));
|
||||
supplement.add_entity(gnarling_stalker(pos, dynamic_rng));
|
||||
// hut corner posts
|
||||
const CORNER_HEIGHT: i32 = 10;
|
||||
const CORNER_OFFSET: i32 = 18;
|
||||
let height = wpos.z + CORNER_HEIGHT;
|
||||
let plus_minus: [i32; 2] = [1, -1];
|
||||
for x in plus_minus {
|
||||
for y in plus_minus {
|
||||
let pos = Vec3::new(
|
||||
wpos.x + x * CORNER_OFFSET,
|
||||
wpos.y + y * CORNER_OFFSET,
|
||||
height,
|
||||
);
|
||||
supplement.add_entity(gnarling_stalker(pos, dynamic_rng));
|
||||
}
|
||||
}
|
||||
// hut sides on ground (using orientation)
|
||||
const NUM_SIDE_GNARLINGS: i32 = 2;
|
||||
const GROUND_HEIGHT: i32 = 4;
|
||||
const GROUND_OFFSET: i32 = 24;
|
||||
let height = wpos.z + GROUND_HEIGHT;
|
||||
let x_or_y = match _ori {
|
||||
Dir::X | Dir::NegX => true,
|
||||
Dir::Y | Dir::NegY => false,
|
||||
};
|
||||
for pm in plus_minus {
|
||||
let mut pos_ori =
|
||||
Vec3::new(wpos.x + pm * GROUND_OFFSET, wpos.y, height);
|
||||
let mut pos_xori =
|
||||
Vec3::new(wpos.x, wpos.y + pm * GROUND_OFFSET, height);
|
||||
if x_or_y {
|
||||
(pos_ori, pos_xori) = (pos_xori, pos_ori);
|
||||
}
|
||||
supplement.add_entity(wood_golem(pos_ori, dynamic_rng));
|
||||
for _ in 1..=NUM_SIDE_GNARLINGS {
|
||||
supplement.add_entity(melee_gnarling(pos_xori, dynamic_rng));
|
||||
}
|
||||
for _ in 0..6 {
|
||||
supplement.add_entity(random_gnarling(pos, dynamic_rng));
|
||||
}
|
||||
},
|
||||
GnarlingStructure::WatchTower => {
|
||||
const NUM_WATCHTOWER_STALKERS: i32 = 2;
|
||||
const FLOOR_HEIGHT: i32 = 27;
|
||||
supplement.add_entity(wood_golem(wpos, dynamic_rng));
|
||||
let spawn_pos = wpos.xy().with_z(wpos.z + 27);
|
||||
let num = dynamic_rng.gen_range(2..=4);
|
||||
for _ in 0..num {
|
||||
let spawn_pos = wpos.xy().with_z(wpos.z + FLOOR_HEIGHT);
|
||||
for _ in 1..=NUM_WATCHTOWER_STALKERS {
|
||||
supplement.add_entity(gnarling_stalker(
|
||||
spawn_pos + Vec2::broadcast(4),
|
||||
dynamic_rng,
|
||||
@ -417,12 +493,19 @@ impl GnarlingFortification {
|
||||
}
|
||||
}
|
||||
|
||||
// wall towers
|
||||
for pos in &self.wall_towers {
|
||||
const NUM_WALLTOWER_STALKERS: [i32; 2] = [1, 3];
|
||||
const FLOOR_HEIGHT: i32 = 27;
|
||||
let wpos = *pos + self.origin;
|
||||
if area.contains_point(pos.xy()) {
|
||||
for _ in 0..4 {
|
||||
supplement
|
||||
.add_entity(gnarling_stalker(wpos.xy().with_z(wpos.z + 21), dynamic_rng))
|
||||
let num =
|
||||
dynamic_rng.gen_range(NUM_WALLTOWER_STALKERS[0]..=NUM_WALLTOWER_STALKERS[1]);
|
||||
for _ in 1..=num {
|
||||
supplement.add_entity(gnarling_stalker(
|
||||
wpos.xy().with_z(wpos.z + FLOOR_HEIGHT),
|
||||
dynamic_rng,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1905,6 +1988,13 @@ fn random_gnarling<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn melee_gnarling<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
|
||||
match rng.gen_range(0..2) {
|
||||
0 => gnarling_mugger(pos, rng),
|
||||
_ => gnarling_logger(pos, rng),
|
||||
}
|
||||
}
|
||||
|
||||
fn gnarling_chieftain<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
|
||||
EntityInfo::at(pos.map(|x| x as f32))
|
||||
.with_asset_expect("common.entity.dungeon.gnarling.chieftain", rng, None)
|
||||
|
Loading…
Reference in New Issue
Block a user