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
- Toned down the health of most wild entities.
- 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
@ -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.
- Moderate buff to wild large bipeds, to bring in line with other balancing
- 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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,15 @@
RapidMelee(
buildup_duration: 1.8,
swing_duration: 0.5,
recover_duration: 1.2,
buildup_duration: 1.0,
swing_duration: 0.45,
recover_duration: 1.0,
melee_constructor: (
kind: NecroticVortex(
damage: 30,
pull: 7,
lifesteal: 2,
damage: 20,
pull: 6.5,
lifesteal: 3.0,
energy_regen: 30,
),
range: 16.0,
range: 17.0,
angle: 360.0,
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"),
body: RandomWith("humanoid"),
alignment: Alignment(Enemy),
loot: LootTable("common.loot_tables.dungeon.cultist.miniboss"),
loot: LootTable("common.loot_tables.dungeon.cultist.beastmaster"),
inventory: (
loadout: Inline((
inherit: Asset("common.loadout.dungeon.cultist.beastmaster"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@
(2, Item("common.items.weapons.staff.cultist_staff")),
(2, Item("common.items.weapons.hammer.cultist_purp_2h-0")),
(2, ModularWeapon(tool: Hammer, material: Orichalcum, hands: None)),
(2, Item("common.items.weapons.axe.malachite_axe-0")),
(2, Item("common.items.weapons.bow.velorite")),
(1, Item("common.items.weapons.sceptre.root_evil")),
]), 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
(0.25, LootTable("common.loot_tables.weapons.cultist")),
(0.25, LootTable("common.loot_tables.armor.cultist")),
(0.25, LootTable("common.loot_tables.weapons.cave")),
// Currency
(3.0, MultiDrop(Item("common.items.utility.coins"), 1000, 2000)),
// 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)),
// Food
(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")),
(5.0, LootTable("common.loot_tables.weapons.cave")),
(10.0, LootTable("common.loot_tables.armor.cultist")),
(1, All([
// Gear
MultiDrop(Lottery([
(2.0, LootTable("common.loot_tables.armor.cultist")),
(1.0, LootTable("common.loot_tables.weapons.cultist")),
(1.0, LootTable("common.loot_tables.weapons.cave")),
]), 1, 2),
Lottery([
// Rare misc items
// Allow for Ankh to drop till it finds a proper home
(1.0, Item("common.items.boss_drops.lantern")),
(1.0, Item("common.items.glider.skullgrin")),
(0.01, Item("common.items.armor.misc.neck.ankh_of_life")),
(0.1, Item("common.items.armor.misc.neck.ankh_of_life")),
// Legendary weapons
(1.0, Item("common.items.weapons.staff.laevateinn")),
(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
(1.0, Item("common.items.crafting_ing.mindflayer_bag_damaged")),
(1.0, MultiDrop(Item("common.items.log.eldwood"), 2, 6)),
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 {
Body::Humanoid(_) => 100,
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::Forgemaster => 300,
biped_large::Species::Gigasfrost => 990,

View File

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

View File

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

View File

@ -2312,7 +2312,7 @@ impl<'a> AgentData<'a> {
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 =
if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
@ -3076,34 +3076,48 @@ impl<'a> AgentData<'a> {
ConditionCounterInit = 0,
}
enum Timers {
ExtraSummonTimer = 0,
}
const MINDFLAYER_ATTACK_DIST: f32 = 16.0;
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 home = agent.patrol_origin.unwrap_or(self.pos.0);
// Sets counter at start of combat, using `condition` to keep track of whether
// it was already initialized
if !agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] {
agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
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] =
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]
> 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
{
// teleport to room center for summon, to avoid walls
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))
{
@ -3111,6 +3125,28 @@ impl<'a> AgentData<'a> {
[ActionStateFCounters::FCounterHealthThreshold as usize] -=
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) {
if entities_have_line_of_sight(
self.pos,
@ -3122,16 +3158,18 @@ impl<'a> AgentData<'a> {
read_data,
) {
// 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);
} 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
// seconds
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
controller.push_basic_input(InputKind::Primary);
} else {
@ -3280,7 +3318,7 @@ impl<'a> AgentData<'a> {
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());
// Teleport back to home position if we're too far from our home position but in
// range of the blink ability

View File

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

View File

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