Merge branch 'neura/cultist-loot' into 'master'

Rebalance cultist dungeon loot tables and distribution, and improve Mindflayer anticheese

See merge request veloren/veloren!4460
This commit is contained in:
flo 2024-05-23 06:07:21 +00:00
commit 9ea234a17a
30 changed files with 224 additions and 132 deletions

View File

@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Refresh of voxel models for orichalcum armour - Refresh of voxel models for orichalcum armour
- Toned down the health of most wild entities. - Toned down the health of most wild entities.
- Rocksnapper received new abilities and AI - Rocksnapper received new abilities and AI
- Rebalanced cultist dungeon loot tables; among other things, the drops Glowing Remains and Ankh of Life from Mindflayer are now 2.5x and 25x more frequent, respectively.
- Improved Mindflayer anticheese measures.
### Removed ### Removed
@ -40,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- NPC path-finding, especially for merchants and travellers is now less dumb. - NPC path-finding, especially for merchants and travellers is now less dumb.
- Moderate buff to wild large bipeds, to bring in line with other balancing - Moderate buff to wild large bipeds, to bring in line with other balancing
- Loot protection for solo players and NPCs works again - Loot protection for solo players and NPCs works again
- New cultist dungeons are less overly abundant, sahagin dungeons spawn again.
- Cultist dungeons now always have exactly one portal which leads to the boss room.
## [0.16.0] - 2024-03-30 ## [0.16.0] - 2024-03-30

View File

@ -676,6 +676,7 @@
Simple(None, "common.abilities.custom.mindflayer.dimensionaldoor"), Simple(None, "common.abilities.custom.mindflayer.dimensionaldoor"),
Simple(None, "common.abilities.custom.mindflayer.necroticsphere"), Simple(None, "common.abilities.custom.mindflayer.necroticsphere"),
Simple(None, "common.abilities.custom.mindflayer.summonminions"), Simple(None, "common.abilities.custom.mindflayer.summonminions"),
Simple(None, "common.abilities.custom.mindflayer.summonextraminions"),
], ],
), ),
Custom("Flamekeeper"): ( Custom("Flamekeeper"): (

View File

@ -1,10 +1,10 @@
BasicBeam( BasicBeam(
buildup_duration: 0.9, buildup_duration: 0.8,
recover_duration: 1.0, recover_duration: 0.9,
beam_duration: 1.0, beam_duration: 1.0,
damage: 12.0, damage: 9.5,
tick_rate: 5.0, tick_rate: 5.0,
range: 22.0, range: 28.0,
max_angle: 15.0, max_angle: 15.0,
damage_effect: Some(Buff(( damage_effect: Some(Buff((
kind: Cursed, kind: Cursed,
@ -13,7 +13,7 @@ BasicBeam(
chance: 0.33, chance: 0.33,
))), ))),
energy_regen: 0, energy_regen: 0,
energy_drain: 0, energy_drain: 45,
ori_rate: 0.4, ori_rate: 0.45,
specifier: Cultist, specifier: Cultist,
) )

View File

@ -1,11 +1,12 @@
BasicRanged( BasicRanged(
energy_cost: 0, energy_cost: 0,
buildup_duration: 1.125, buildup_duration: 1.00,
recover_duration: 0.8, recover_duration: 0.70,
projectile: NecroticSphere( projectile: NecroticSphere(
damage: 67.5, damage: 50,
radius: 5.0, radius: 7.0,
min_falloff: 0.9, min_falloff: 0.2,
energy_regen: 40,
), ),
projectile_body: Object(FireworkPurple), projectile_body: Object(FireworkPurple),
projectile_speed: 100.0, projectile_speed: 100.0,

View File

@ -1,14 +1,15 @@
RapidMelee( RapidMelee(
buildup_duration: 1.8, buildup_duration: 1.0,
swing_duration: 0.5, swing_duration: 0.45,
recover_duration: 1.2, recover_duration: 1.0,
melee_constructor: ( melee_constructor: (
kind: NecroticVortex( kind: NecroticVortex(
damage: 30, damage: 20,
pull: 7, pull: 6.5,
lifesteal: 2, lifesteal: 3.0,
energy_regen: 30,
), ),
range: 16.0, range: 17.0,
angle: 360.0, angle: 360.0,
multi_target: Some(Normal), multi_target: Some(Normal),
), ),

View File

@ -0,0 +1,19 @@
BasicSummon(
buildup_duration: 0.5,
cast_duration: 1.0,
recover_duration: 0.5,
summon_amount: 2,
summon_distance: (1, 1),
summon_info: (
body: BipedSmall((
species: Husk,
body_type: Male,
)),
use_npc_name: true,
scale: None,
has_health: true,
loadout_config: Some(HuskSummon),
skillset_config: Some(Rank5),
),
duration: None,
)

View File

@ -3,7 +3,7 @@
name: Name("Beastmaster"), name: Name("Beastmaster"),
body: RandomWith("humanoid"), body: RandomWith("humanoid"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.miniboss"), loot: LootTable("common.loot_tables.dungeon.cultist.beastmaster"),
inventory: ( inventory: (
loadout: Inline(( loadout: Inline((
inherit: Asset("common.loadout.dungeon.cultist.beastmaster"), inherit: Asset("common.loadout.dungeon.cultist.beastmaster"),

View File

@ -3,9 +3,9 @@
name: Name("Tamed Darkhound"), name: Name("Tamed Darkhound"),
body: RandomWith("darkhound"), body: RandomWith("darkhound"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.minion"), loot: LootTable("common.loot_tables.dungeon.cultist.hound"),
inventory: ( inventory: (
loadout: FromBody, loadout: FromBody,
), ),
meta: [], meta: [],
) )

View File

@ -3,9 +3,9 @@
name: Name("Cultist Husk"), name: Name("Cultist Husk"),
body: RandomWith("husk"), body: RandomWith("husk"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.minion"), loot: LootTable("common.loot_tables.dungeon.cultist.husk"),
inventory: ( inventory: (
loadout: Asset("common.loadout.dungeon.cultist.husk"), loadout: Asset("common.loadout.dungeon.cultist.husk"),
), ),
meta: [], meta: [],
) )

View File

@ -3,9 +3,9 @@
name: Name("Husk Brute"), name: Name("Husk Brute"),
body: RandomWith("husk_brute"), body: RandomWith("husk_brute"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.miniboss"), loot: LootTable("common.loot_tables.dungeon.cultist.husk_brute"),
inventory: ( inventory: (
loadout: FromBody, loadout: FromBody,
), ),
meta: [], meta: [],
) )

View File

@ -3,7 +3,7 @@
name: Name("Cultist Warlock"), name: Name("Cultist Warlock"),
body: RandomWith("cultist_warlock"), body: RandomWith("cultist_warlock"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.enemy"), loot: LootTable("common.loot_tables.dungeon.cultist.enemy_large"),
inventory: ( inventory: (
loadout: Inline(( loadout: Inline((
inherit: Asset("common.loadout.dungeon.cultist.warlock"), inherit: Asset("common.loadout.dungeon.cultist.warlock"),
@ -14,4 +14,4 @@
)), )),
), ),
meta: [], meta: [],
) )

View File

@ -3,7 +3,7 @@
name: Name("Cultist Warlord"), name: Name("Cultist Warlord"),
body: RandomWith("cultist_warlord"), body: RandomWith("cultist_warlord"),
alignment: Alignment(Enemy), alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.enemy"), loot: LootTable("common.loot_tables.dungeon.cultist.enemy_large"),
inventory: ( inventory: (
loadout: Inline(( loadout: Inline((
inherit: Asset("common.loadout.dungeon.cultist.warlord"), inherit: Asset("common.loadout.dungeon.cultist.warlord"),
@ -14,4 +14,4 @@
)), )),
), ),
meta: [], meta: [],
) )

View File

@ -4,7 +4,7 @@
name: Name("Argo"), name: Name("Argo"),
body: RandomWith("humanoid"), body: RandomWith("humanoid"),
alignment: Alignment(Npc), alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.dungeon.cultist.miniboss"), loot: LootTable("common.loot_tables.dungeon.cultist.beastmaster"),
inventory: ( inventory: (
loadout: Inline(( loadout: Inline((
inherit: Asset("common.loadout.spots.wizard_tower.wizard_boss"), inherit: Asset("common.loadout.spots.wizard_tower.wizard_boss"),
@ -16,4 +16,4 @@
meta: [ meta: [
SkillSetAsset("common.skillset.preset.rank5.fullskill"), SkillSetAsset("common.skillset.preset.rank5.fullskill"),
], ],
) )

View File

@ -4,7 +4,7 @@
name: Name("Haku"), name: Name("Haku"),
body: RandomWith("humanoid"), body: RandomWith("humanoid"),
alignment: Alignment(Npc), alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.dungeon.cultist.miniboss"), loot: LootTable("common.loot_tables.dungeon.cultist.beastmaster"),
inventory: ( inventory: (
loadout: Inline(( loadout: Inline((
inherit: Asset("common.loadout.spots.wizard_tower.wizard_boss"), inherit: Asset("common.loadout.spots.wizard_tower.wizard_boss"),
@ -16,4 +16,4 @@
meta: [ meta: [
SkillSetAsset("common.skillset.preset.rank5.fullskill"), SkillSetAsset("common.skillset.preset.rank5.fullskill"),
], ],
) )

View File

@ -4,7 +4,7 @@
name: Name("Trish"), name: Name("Trish"),
body: RandomWith("humanoid"), body: RandomWith("humanoid"),
alignment: Alignment(Npc), alignment: Alignment(Npc),
loot: LootTable("common.loot_tables.dungeon.cultist.miniboss"), loot: LootTable("common.loot_tables.dungeon.cultist.beastmaster"),
inventory: ( inventory: (
loadout: Inline(( loadout: Inline((
inherit: Asset("common.loadout.spots.wizard_tower.wizard_boss"), inherit: Asset("common.loadout.spots.wizard_tower.wizard_boss"),
@ -16,4 +16,4 @@
meta: [ meta: [
SkillSetAsset("common.skillset.preset.rank5.fullskill"), SkillSetAsset("common.skillset.preset.rank5.fullskill"),
], ],
) )

View File

@ -23,6 +23,7 @@
(2, Item("common.items.weapons.staff.cultist_staff")), (2, Item("common.items.weapons.staff.cultist_staff")),
(2, Item("common.items.weapons.hammer.cultist_purp_2h-0")), (2, Item("common.items.weapons.hammer.cultist_purp_2h-0")),
(2, ModularWeapon(tool: Hammer, material: Orichalcum, hands: None)), (2, ModularWeapon(tool: Hammer, material: Orichalcum, hands: None)),
(2, Item("common.items.weapons.axe.malachite_axe-0")),
(2, Item("common.items.weapons.bow.velorite")), (2, Item("common.items.weapons.bow.velorite")),
(1, Item("common.items.weapons.sceptre.root_evil")), (1, Item("common.items.weapons.sceptre.root_evil")),
]), None)), ]), None)),

View File

@ -0,0 +1,11 @@
[
(1, All([
MultiDrop(Item("common.items.utility.coins"), 500, 1000),
MultiDrop(Item("common.items.consumable.potion_minor"), 2, 4),
Lottery([
(4.0, LootTable("common.loot_tables.armor.cultist")),
(1.0, Item("common.items.glider.blue")),
(15.0, Nothing),
]),
])),
]

View File

@ -4,10 +4,11 @@
// Gear // Gear
(0.25, LootTable("common.loot_tables.weapons.cultist")), (0.25, LootTable("common.loot_tables.weapons.cultist")),
(0.25, LootTable("common.loot_tables.armor.cultist")), (0.25, LootTable("common.loot_tables.armor.cultist")),
(0.25, LootTable("common.loot_tables.weapons.cave")),
// Currency // Currency
(3.0, MultiDrop(Item("common.items.utility.coins"), 1000, 2000)), (3.0, MultiDrop(Item("common.items.utility.coins"), 1000, 2000)),
// Consumables // Consumables
(2.0, MultiDrop(Item("common.items.consumable.potion_minor"), 4, 8)), (2.0, MultiDrop(Item("common.items.consumable.potion_minor"), 2, 4)),
(0.1, MultiDrop(Item("common.items.food.spore_corruption"), 1, 3)), (0.1, MultiDrop(Item("common.items.food.spore_corruption"), 1, 3)),
// Food // Food
(1.0, MultiDrop(LootTable("common.loot_tables.food.prepared"), 3, 6)), (1.0, MultiDrop(LootTable("common.loot_tables.food.prepared"), 3, 6)),

View File

@ -0,0 +1,9 @@
[
// Currency
(10.0, MultiDrop(Item("common.items.utility.coins"), 250, 500)),
// Food
(5.0, LootTable("common.loot_tables.food.prepared")),
// Weapons
(0.5, LootTable("common.loot_tables.weapons.cultist")),
(0.5, LootTable("common.loot_tables.weapons.cave")),
]

View File

@ -0,0 +1,6 @@
[
// Currency
(30.0, MultiDrop(Item("common.items.utility.coins"), 50, 100)),
// Nothing
(30.0, Nothing),
]

View File

@ -0,0 +1,12 @@
[
(1, All([
MultiDrop(Item("common.items.utility.coins"), 500, 1000),
MultiDrop(Item("common.items.consumable.potion_minor"), 2, 4),
Lottery([
(4.0, LootTable("common.loot_tables.armor.cultist")),
(1.0, Item("common.items.glider.blue")),
(1.0, MultiDrop(Item("common.items.food.spore_corruption"), 1, 2)),
(14.0, Nothing),
]),
])),
]

View File

@ -1,15 +1,27 @@
[ [
(5.0, LootTable("common.loot_tables.weapons.cultist")), (1, All([
(5.0, LootTable("common.loot_tables.weapons.cave")), // Gear
(10.0, LootTable("common.loot_tables.armor.cultist")), MultiDrop(Lottery([
// Rare misc items (2.0, LootTable("common.loot_tables.armor.cultist")),
(1.0, Item("common.items.boss_drops.lantern")), (1.0, LootTable("common.loot_tables.weapons.cultist")),
(1.0, Item("common.items.glider.skullgrin")), (1.0, LootTable("common.loot_tables.weapons.cave")),
(0.01, Item("common.items.armor.misc.neck.ankh_of_life")), ]), 1, 2),
// Legendary weapons Lottery([
(1.0, Item("common.items.weapons.staff.laevateinn")), // Rare misc items
// Crafting material // Allow for Ankh to drop till it finds a proper home
// Allow for Eldwood to drop till entity droppers are implemented (1.0, Item("common.items.boss_drops.lantern")),
(1.0, Item("common.items.crafting_ing.mindflayer_bag_damaged")), (1.0, Item("common.items.glider.skullgrin")),
(1.0, MultiDrop(Item("common.items.log.eldwood"), 2, 6)), (0.1, Item("common.items.armor.misc.neck.ankh_of_life")),
// Legendary weapons
(0.5, Item("common.items.weapons.staff.laevateinn")),
// Crafting material
(0.5, Item("common.items.crafting_ing.mindflayer_bag_damaged")),
(6.9, Nothing),
]),
// Allow for Eldwood to drop till entity droppers are implemented
Lottery([
(1.0, MultiDrop(Item("common.items.log.eldwood"), 1, 3)),
(1.0, Nothing),
])
])),
] ]

View File

@ -1,12 +0,0 @@
[
// Currency
(10.0, MultiDrop(Item("common.items.utility.coins"), 500, 1000)),
// Consumables
(5.0, MultiDrop(Item("common.items.consumable.potion_minor"), 4, 8)),
// Back
(1.0, Item("common.items.armor.misc.back.dungeon_purple")),
// Ring
(0.5, LootTable("common.loot_tables.armor.cultist")),
// Glider
(1.0, Item("common.items.glider.blue")),
]

View File

@ -1264,7 +1264,7 @@ impl Body {
match self { match self {
Body::Humanoid(_) => 100, Body::Humanoid(_) => 100,
Body::BipedLarge(biped_large) => match biped_large.species { Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Mindflayer => 390, biped_large::Species::Mindflayer => 777,
biped_large::Species::Minotaur => 340, biped_large::Species::Minotaur => 340,
biped_large::Species::Forgemaster => 300, biped_large::Species::Forgemaster => 300,
biped_large::Species::Gigasfrost => 990, biped_large::Species::Gigasfrost => 990,

View File

@ -291,7 +291,10 @@ impl MeleeConstructor {
damage, damage,
pull, pull,
lifesteal, lifesteal,
energy_regen,
} => { } => {
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy_regen))
.with_requirement(CombatRequirement::AnyDamage);
let lifesteal = CombatEffect::Lifesteal(lifesteal); let lifesteal = CombatEffect::Lifesteal(lifesteal);
let mut damage = AttackDamage::new( let mut damage = AttackDamage::new(
@ -321,6 +324,7 @@ impl MeleeConstructor {
Attack::default() Attack::default()
.with_damage(damage) .with_damage(damage)
.with_precision(precision_mult) .with_precision(precision_mult)
.with_effect(energy)
.with_effect(knockback) .with_effect(knockback)
}, },
SonicWave { SonicWave {
@ -467,16 +471,19 @@ impl MeleeConstructor {
damage: a_damage, damage: a_damage,
pull: a_pull, pull: a_pull,
lifesteal: a_lifesteal, lifesteal: a_lifesteal,
energy_regen: a_energy_regen,
}, },
NecroticVortex { NecroticVortex {
damage: b_damage, damage: b_damage,
pull: b_pull, pull: b_pull,
lifesteal: b_lifesteal, lifesteal: b_lifesteal,
energy_regen: b_energy_regen,
}, },
) => NecroticVortex { ) => NecroticVortex {
damage: scale_values(a_damage, b_damage), damage: scale_values(a_damage, b_damage),
pull: scale_values(a_pull, b_pull), pull: scale_values(a_pull, b_pull),
lifesteal: scale_values(a_lifesteal, b_lifesteal), lifesteal: scale_values(a_lifesteal, b_lifesteal),
energy_regen: scale_values(a_energy_regen, b_energy_regen),
}, },
( (
Hook { Hook {
@ -577,6 +584,7 @@ pub enum MeleeConstructorKind {
damage: f32, damage: f32,
pull: f32, pull: f32,
lifesteal: f32, lifesteal: f32,
energy_regen: f32,
}, },
SonicWave { SonicWave {
damage: f32, damage: f32,
@ -633,6 +641,7 @@ impl MeleeConstructorKind {
ref mut damage, ref mut damage,
ref mut pull, ref mut pull,
ref mut lifesteal, ref mut lifesteal,
energy_regen: _,
} => { } => {
*damage *= stats.power; *damage *= stats.power;
*pull *= stats.effect_power; *pull *= stats.effect_power;

View File

@ -87,6 +87,7 @@ pub enum ProjectileConstructor {
damage: f32, damage: f32,
radius: f32, radius: f32,
min_falloff: f32, min_falloff: f32,
energy_regen: f32,
}, },
Magicball { Magicball {
damage: f32, damage: f32,
@ -461,7 +462,10 @@ impl ProjectileConstructor {
damage, damage,
radius, radius,
min_falloff, min_falloff,
energy_regen,
} => { } => {
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy_regen))
.with_requirement(CombatRequirement::AnyDamage);
let damage = AttackDamage::new( let damage = AttackDamage::new(
Damage { Damage {
source: DamageSource::Explosion, source: DamageSource::Explosion,
@ -474,7 +478,8 @@ impl ProjectileConstructor {
let attack = Attack::default() let attack = Attack::default()
.with_damage(damage) .with_damage(damage)
.with_precision(precision_mult) .with_precision(precision_mult)
.with_combo_increment(); .with_combo_increment()
.with_effect(energy);
let explosion = Explosion { let explosion = Explosion {
effects: vec![RadiusEffect::Attack(attack)], effects: vec![RadiusEffect::Attack(attack)],
radius, radius,

View File

@ -2312,7 +2312,7 @@ impl<'a> AgentData<'a> {
AttackTimer = 0, AttackTimer = 0,
} }
let home = agent.patrol_origin.unwrap_or(self.pos.0.round()); let home = agent.patrol_origin.unwrap_or(self.pos.0);
let attack_select = let attack_select =
if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 { if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
@ -3076,34 +3076,48 @@ impl<'a> AgentData<'a> {
ConditionCounterInit = 0, ConditionCounterInit = 0,
} }
enum Timers {
ExtraSummonTimer = 0,
}
const MINDFLAYER_ATTACK_DIST: f32 = 16.0; const MINDFLAYER_ATTACK_DIST: f32 = 16.0;
const MINION_SUMMON_THRESHOLD: f32 = 0.20; const MINION_SUMMON_THRESHOLD: f32 = 0.20;
const MIN_CURSEDFLAMES_ENERGY: f32 = 180.0;
const MAX_BLINK_DISTANCE: f32 = 150.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());
let home = agent.patrol_origin.unwrap_or(self.pos.0);
// 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.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] { if !agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] {
agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] = agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
1.0 - MINION_SUMMON_THRESHOLD; 1.0 - MINION_SUMMON_THRESHOLD;
agent.combat_state.int_counters[ActionStateICounters::ICounterNumFireballs as usize] =
rand::random::<u8>() % 4;
agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] = agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] =
true; true;
} }
agent.combat_state.timers[Timers::ExtraSummonTimer as usize] += read_data.dt.0;
if (tgt_data.pos.0 - home).xy().magnitude_squared() < (25.0_f32).powi(2) {
agent.combat_state.timers[Timers::ExtraSummonTimer as usize] = 0.0;
}
if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
> health_fraction > health_fraction
&& (matches!(self.char_state, CharacterState::BasicSummon(_))
|| entities_have_line_of_sight(
self.pos,
self.body,
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
))
// TODO: Better check for if there's room to spawn summons
{ {
// Summon minions at particular thresholds of health // teleport to room center for summon, to avoid walls
controller.push_basic_input(InputKind::Ability(2)); if (5.0_f32.powi(2)..=MAX_BLINK_DISTANCE.powi(2))
.contains(&home.distance_squared(self.pos.0))
{
controller.push_action(ControlAction::StartInput {
input: InputKind::Ability(0),
target_entity: None,
select_pos: Some(home),
});
} else {
// Summon minions at particular thresholds of health
controller.push_basic_input(InputKind::Ability(2));
}
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))
{ {
@ -3111,6 +3125,28 @@ impl<'a> AgentData<'a> {
[ActionStateFCounters::FCounterHealthThreshold as usize] -= [ActionStateFCounters::FCounterHealthThreshold as usize] -=
MINION_SUMMON_THRESHOLD; MINION_SUMMON_THRESHOLD;
} }
} else if agent.combat_state.timers[Timers::ExtraSummonTimer as usize] > 12.0 {
// teleport to target for extra summons
if (3.0_f32.powi(2)..=MAX_BLINK_DISTANCE.powi(2))
.contains(&tgt_data.pos.0.distance_squared(self.pos.0))
{
controller.push_action(ControlAction::StartInput {
input: InputKind::Ability(0),
target_entity: agent
.target
.as_ref()
.and_then(|t| read_data.uids.get(t.target))
.copied(),
select_pos: None,
});
} else {
controller.push_basic_input(InputKind::Ability(3));
}
if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
{
agent.combat_state.timers[Timers::ExtraSummonTimer as usize] = 0.0;
}
} else if attack_data.dist_sqrd < MINDFLAYER_ATTACK_DIST.powi(2) { } else if attack_data.dist_sqrd < MINDFLAYER_ATTACK_DIST.powi(2) {
if entities_have_line_of_sight( if entities_have_line_of_sight(
self.pos, self.pos,
@ -3122,16 +3158,18 @@ impl<'a> AgentData<'a> {
read_data, read_data,
) { ) {
// If close to target, use either primary or secondary ability // If close to target, use either primary or secondary ability
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(10) && !matches!(c.stage_section, StageSection::Recover)) if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5) && !matches!(c.stage_section, StageSection::Recover))
{ {
// If already using primary, keep using primary until 10 consecutive seconds // If already using primary, keep using primary until 5 consecutive seconds
controller.push_basic_input(InputKind::Primary); controller.push_basic_input(InputKind::Primary);
} else if matches!(self.char_state, CharacterState::RapidMelee(c) if c.current_strike < 50 && !matches!(c.stage_section, StageSection::Recover)) } else if matches!(self.char_state, CharacterState::RapidMelee(c) if c.current_strike < 50 && !matches!(c.stage_section, StageSection::Recover))
{ {
// If already using secondary, keep using secondary until 10 consecutive // If already using secondary, keep using secondary until 10 consecutive
// seconds // seconds
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
} else if rng.gen_bool(health_fraction.into()) { } else if self.energy.current() > MIN_CURSEDFLAMES_ENERGY
&& rng.gen_bool(health_fraction.into())
{
// Else if at high health, use primary // Else if at high health, use primary
controller.push_basic_input(InputKind::Primary); controller.push_basic_input(InputKind::Primary);
} else { } else {
@ -3280,7 +3318,7 @@ impl<'a> AgentData<'a> {
read_data, read_data,
) )
}; };
let home = agent.patrol_origin.unwrap_or(self.pos.0.round()); let home = agent.patrol_origin.unwrap_or(self.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());
// Teleport back to home position if we're too far from our home position but in // Teleport back to home position if we're too far from our home position but in
// range of the blink ability // range of the blink ability

View File

@ -414,7 +414,7 @@ impl Civs {
)?, )?,
SiteKind::DwarvenMine, SiteKind::DwarvenMine,
), ),
87..=92 => ( 87..=90 => (
find_site_loc( find_site_loc(
&mut ctx, &mut ctx,
&ProximityRequirementsBuilder::new() &ProximityRequirementsBuilder::new()

View File

@ -20,6 +20,7 @@ pub struct Room {
clear_center: Vec2<i32>, clear_center: Vec2<i32>,
mob_room: bool, mob_room: bool,
boss_room: bool, boss_room: bool,
portal_to_boss: bool,
} }
pub struct Cultist { pub struct Cultist {
@ -47,18 +48,25 @@ impl Cultist {
for s in 0..=1 { for s in 0..=1 {
// rooms // rooms
let rooms = [1, 2]; let rooms = [1, 2];
let boss_portal_floor =
(1 + (RandomField::new(0).get(center.with_z(base + 1)) % 2)) as i32;
let portal_to_boss_index =
(RandomField::new(0).get(center.with_z(base)) % 4) as usize;
if rooms.contains(&f) { if rooms.contains(&f) {
for dir in DIAGONALS { for (d, dir) in DIAGONALS.iter().enumerate() {
let room_base = base - (f * (2 * (room_size))) - (s * room_size); let room_base = base - (f * (2 * (room_size))) - (s * room_size);
let room_center = center + (dir * ((room_size * 2) - 5 + (10 * s))); let room_center = center + (dir * ((room_size * 2) - 5 + (10 * s)));
let clear_center = center + (dir * ((room_size * 2) - 6 + (10 * s))); let clear_center = center + (dir * ((room_size * 2) - 6 + (10 * s)));
let mob_room = s < 1; let mob_room = s < 1;
let portal_to_boss =
mob_room && d == portal_to_boss_index && f == boss_portal_floor;
room_data.push(Room { room_data.push(Room {
room_base, room_base,
room_center, room_center,
clear_center, clear_center,
mob_room, mob_room,
boss_room: false, boss_room: false,
portal_to_boss,
}); });
} }
} }
@ -71,6 +79,7 @@ impl Cultist {
clear_center: center, clear_center: center,
mob_room: false, mob_room: false,
boss_room: true, boss_room: true,
portal_to_boss: false,
}); });
Self { Self {
@ -332,12 +341,13 @@ impl Structure for Cultist {
} }
// room clears // room clears
for room in room_data { for room in room_data {
let (room_base, room_center, clear_center, mob_room, boss_room) = ( let (room_base, room_center, clear_center, mob_room, boss_room, portal_to_boss) = (
room.room_base, room.room_base,
room.room_center, room.room_center,
room.clear_center, room.clear_center,
room.mob_room, room.mob_room,
room.boss_room, room.boss_room,
room.portal_to_boss,
); );
painter painter
.cylinder(Aabb { .cylinder(Aabb {
@ -541,7 +551,7 @@ impl Structure for Cultist {
let npc_pos = (room_center + dir * ((spacing / 2) * d)) let npc_pos = (room_center + dir * ((spacing / 2) * d))
.with_z(room_base - room_size + ((room_size / 3) * f)); .with_z(room_base - room_size + ((room_size / 3) * f));
let pos_var = RandomField::new(0).get(npc_pos) % 10; let pos_var = RandomField::new(0).get(npc_pos) % 10;
if pos_var < 1 { if pos_var < 2 {
painter.spawn(EntityInfo::at(npc_pos.as_()).with_asset_expect( painter.spawn(EntityInfo::at(npc_pos.as_()).with_asset_expect(
"common.entity.dungeon.cultist.cultist", "common.entity.dungeon.cultist.cultist",
&mut thread_rng, &mut thread_rng,
@ -596,66 +606,30 @@ impl Structure for Cultist {
let exit_position = (center - 10).with_z(base - (6 * room_size)); let exit_position = (center - 10).with_z(base - (6 * room_size));
let boss_position = (center - 10).with_z(base - (7 * room_size)); let boss_position = (center - 10).with_z(base - (7 * room_size));
let boss_portal = center.with_z(base - (7 * room_size)); let boss_portal = center.with_z(base - (7 * room_size));
let mini_boss_portal_target = if portal_to_boss {
let mob_portal_pos = Vec3::new( boss_position.as_::<f32>()
mob_portal.x as f32, } else {
mob_portal.y as f32, exit_position.as_::<f32>()
mob_portal.z as f32, };
);
let mob_portal_target_pos = Vec3::new(
mob_portal_target.x as f32,
mob_portal_target.y as f32,
mob_portal_target.z as f32,
);
let mini_boss_portal_pos = Vec3::new(
mini_boss_portal.x as f32,
mini_boss_portal.y as f32,
mini_boss_portal.z as f32,
);
let exit_pos = Vec3::new(
exit_position.x as f32,
exit_position.y as f32,
exit_position.z as f32,
);
let boss_pos = Vec3::new(
boss_position.x as f32,
boss_position.y as f32,
boss_position.z as f32,
);
let boss_portal_pos = Vec3::new(
boss_portal.x as f32,
boss_portal.y as f32,
boss_portal.z as f32,
);
let mini_boss_portal_target = [
exit_pos, exit_pos, exit_pos, exit_pos, exit_pos, boss_pos, boss_pos, boss_pos,
];
if mob_room { if mob_room {
painter.spawn(EntityInfo::at(mob_portal_pos).into_special( painter.spawn(EntityInfo::at(mob_portal.as_::<f32>()).into_special(
SpecialEntity::Teleporter(PortalData { SpecialEntity::Teleporter(PortalData {
target: mob_portal_target_pos, target: mob_portal_target.as_::<f32>(),
requires_no_aggro: true, requires_no_aggro: true,
buildup_time: Secs(5.), buildup_time: Secs(5.),
}), }),
)); ));
let mini_boss_portal_target_index = RandomField::new(0).get(mini_boss_portal) painter.spawn(EntityInfo::at(mini_boss_portal.as_::<f32>()).into_special(
as usize
% mini_boss_portal_target.len();
let mini_boss_portal_target_pos =
mini_boss_portal_target[mini_boss_portal_target_index];
painter.spawn(EntityInfo::at(mini_boss_portal_pos).into_special(
SpecialEntity::Teleporter(PortalData { SpecialEntity::Teleporter(PortalData {
target: mini_boss_portal_target_pos, target: mini_boss_portal_target,
requires_no_aggro: true, requires_no_aggro: true,
buildup_time: Secs(5.), buildup_time: Secs(5.),
}), }),
)); ));
} else if boss_room { } else if boss_room {
painter.spawn(EntityInfo::at(boss_portal_pos).into_special( painter.spawn(EntityInfo::at(boss_portal.as_::<f32>()).into_special(
SpecialEntity::Teleporter(PortalData { SpecialEntity::Teleporter(PortalData {
target: exit_pos, target: exit_position.as_::<f32>(),
requires_no_aggro: true, requires_no_aggro: true,
buildup_time: Secs(5.), buildup_time: Secs(5.),
}), }),