Merge branch 'juliancoffee/dungeon_rebalance' into 'master'

Make cultist great again

See merge request veloren/veloren!2513
This commit is contained in:
Samuel Keiffer 2021-07-01 02:37:39 +00:00
commit 099ddea165
27 changed files with 645 additions and 420 deletions

View File

@ -7,7 +7,7 @@ RepeaterRanged(
half_speed_at: 1,
projectile: Arrow(
damage: 30.0,
knockback: 5.0,
knockback: 2.0,
energy_regen: 0,
),
projectile_body: Object(Arrow),

View File

@ -2,17 +2,17 @@ ComboMelee(
stage_data: [
(
stage: 1,
base_damage: 160,
base_damage: 200,
damage_increase: 0,
base_poise_damage: 12,
poise_damage_increase: 0,
knockback: 5.0,
range: 3.5,
angle: 60.0,
base_buildup_duration: 0.25,
base_swing_duration: 0.07,
base_buildup_duration: 0.5,
base_swing_duration: 0.2,
hit_timing: 0.5,
base_recover_duration: 0.25,
base_recover_duration: 0.5,
forward_movement: 0.5,
damage_kind: Crushing,
),

View File

@ -2,8 +2,8 @@ BasicBeam(
buildup_duration: 0.40,
recover_duration: 0.50,
beam_duration: 1.0,
damage: 400,
tick_rate: 0.9,
damage: 150,
tick_rate: 5.0,
range: 22.0,
max_angle: 15.0,
damage_effect: Some(Buff((
@ -14,7 +14,7 @@ BasicBeam(
))),
energy_regen: 0,
energy_drain: 0,
orientation_behavior: Normal,
ori_rate: 0.6,
orientation_behavior: FromOri,
ori_rate: 0.2,
specifier: Cultist,
)

View File

@ -3,10 +3,10 @@ ChargedMelee(
energy_drain: 300,
initial_damage: 10,
scaled_damage: 160,
initial_poise_damage: 60,
scaled_poise_damage: 130,
initial_knockback: 10.0,
scaled_knockback: 50.0,
initial_poise_damage: 20,
scaled_poise_damage: 60,
initial_knockback: 5.0,
scaled_knockback: 20.0,
range: 3.5,
max_angle: 30.0,
speed: 1.0,

View File

@ -14,7 +14,7 @@ BasicBeam(
))),
energy_regen: 0,
energy_drain: 350,
orientation_behavior: Normal,
ori_rate: 0.6,
orientation_behavior: FromOri,
ori_rate: 0.3,
specifier: Flamethrower,
)

View File

@ -4,11 +4,11 @@ ItemDef(
kind: Armor((
kind: Belt("Cultist"),
stats: (
protection: Normal(6.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(8.0),
poise_resilience: Normal(1.0),
energy_max: 20,
energy_reward: 0.025,
crit_power: 0.02,
stealth: 0.0,
),
)),
@ -16,4 +16,4 @@ ItemDef(
tags: [
Cultist,
],
)
)

View File

@ -4,11 +4,11 @@ ItemDef(
kind: Armor((
kind: Chest("Cultist"),
stats: (
protection: Normal(30.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(48.0),
poise_resilience: Normal(6.0),
energy_max: 135,
energy_reward: 0.135,
crit_power: 0.125,
stealth: 0.0,
),
)),
@ -16,4 +16,4 @@ ItemDef(
tags: [
Cultist,
],
)
)

View File

@ -4,11 +4,11 @@ ItemDef(
kind: Armor((
kind: Foot("Cultist"),
stats: (
protection: Normal(6.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(16.0),
poise_resilience: Normal(2.0),
energy_max: 45,
energy_reward: 0.045,
crit_power: 0.04,
stealth: 0.0,
),
)),
@ -16,4 +16,4 @@ ItemDef(
tags: [
Cultist,
],
)
)

View File

@ -4,11 +4,11 @@ ItemDef(
kind: Armor((
kind: Hand("Cultist"),
stats: (
protection: Normal(12.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(16.0),
poise_resilience: Normal(2.0),
energy_max: 45,
energy_reward: 0.045,
crit_power: 0.04,
stealth: 0.0,
),
)),
@ -16,4 +16,4 @@ ItemDef(
tags: [
Cultist,
],
)
)

View File

@ -4,11 +4,11 @@ ItemDef(
kind: Armor((
kind: Pants("Cultist"),
stats: (
protection: Normal(24.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(32.0),
poise_resilience: Normal(4.0),
energy_max: 90,
energy_reward: 0.1,
crit_power: 0.08,
stealth: 0.0,
),
)),
@ -16,4 +16,4 @@ ItemDef(
tags: [
Cultist,
],
)
)

View File

@ -4,11 +4,11 @@ ItemDef(
kind: Armor((
kind: Shoulder("Cultist"),
stats: (
protection: Normal(18.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(32.0),
poise_resilience: Normal(5.0),
energy_max: 90,
energy_reward: 0.1,
crit_power: 0.08,
stealth: 0.0,
),
)),
@ -16,4 +16,4 @@ ItemDef(
tags: [
Cultist,
],
)
)

View File

@ -4,14 +4,16 @@ ItemDef(
kind: Armor((
kind: Back("DungeonPurple"),
stats: (
protection: Normal(3.0),
poise_resilience: Normal(0.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
protection: Normal(8.0),
poise_resilience: Normal(1.0),
energy_max: 20,
energy_reward: 0.025,
crit_power: 0.02,
stealth: 0.0,
),
)),
quality: Epic,
tags: [],
)
tags: [
Cultist,
],
)

View File

@ -0,0 +1,17 @@
ItemDef(
name: "Giant Warlock Chest",
description: "Made of darkest silk.",
kind: Armor((
kind: Chest("GiantWarlock"),
stats: (
protection: Normal(250.0),
poise_resilience: Normal(1.0),
energy_max: 1000,
energy_reward: 1.0,
crit_power: 0.0,
stealth: 0.0,
),
)),
quality: Moderate,
tags: [],
)

View File

@ -0,0 +1,17 @@
ItemDef(
name: "Giant Warlord Chest",
description: "Made of darkest steel.",
kind: Armor((
kind: Chest("GiantWarlord"),
stats: (
protection: Normal(300.0),
poise_resilience: Normal(1.0),
energy_max: 0,
energy_reward: 0.0,
crit_power: 0.0,
stealth: 0.0,
),
)),
quality: Moderate,
tags: [],
)

View File

@ -15,4 +15,4 @@ ItemDef(
quality: Low,
tags: [],
ability_spec: Some(Custom("Husk Brute")),
)
)

View File

@ -4,5 +4,4 @@
Tree("common.skillset.dungeon.tier-5.axe"),
Tree("common.skillset.dungeon.tier-5.hammer"),
Tree("common.skillset.dungeon.tier-5.bow"),
Tree("common.skillset.dungeon.tier-5.staff"),
])

View File

@ -1,21 +0,0 @@
([
Group(Weapon(Staff)),
// Fireball
Skill((Staff(BDamage), Some(1))),
Skill((Staff(BRegen), Some(1))),
Skill((Staff(BRadius), Some(1))),
// Flamethrower
Skill((Staff(FRange), Some(1))),
Skill((Staff(FDrain), Some(1))),
Skill((Staff(FDamage), Some(1))),
Skill((Staff(FVelocity), Some(1))),
// Shockwave
Skill((Staff(UnlockShockwave), None)),
Skill((Staff(SDamage), Some(1))),
Skill((Staff(SKnockback), Some(1))),
Skill((Staff(SRange), Some(1))),
Skill((Staff(SCost), Some(1))),
])

View File

@ -13,7 +13,6 @@
// Spin of death
Skill((Sword(UnlockSpin), None)),
Skill((Sword(SDamage), Some(1))),
Skill((Sword(SSpins), Some(2))),
Skill((Sword(SCost), Some(1))),
Skill((Sword(SDamage), Some(2))),
Skill((Sword(SCost), Some(2))),
])

View File

@ -0,0 +1,23 @@
([
Group(Weapon(Axe)),
// DoubleStrike
Skill((Axe(DsCombo), None)),
Skill((Axe(DsDamage), Some(3))),
Skill((Axe(DsRegen), Some(2))),
Skill((Axe(DsSpeed), Some(3))),
// Spin
Skill((Axe(SInfinite), None)),
Skill((Axe(SHelicopter), None)),
Skill((Axe(SDamage), Some(3))),
Skill((Axe(SSpeed), Some(2))),
Skill((Axe(SCost), Some(2))),
// Leap
Skill((Axe(UnlockLeap), None)),
Skill((Axe(LDamage), Some(2))),
Skill((Axe(LKnockback), Some(2))),
Skill((Axe(LCost), Some(2))),
Skill((Axe(LDistance), Some(2))),
])

View File

@ -0,0 +1,25 @@
([
Group(Weapon(Bow)),
// Projectile Speed
Skill((Bow(ProjSpeed), Some(2))),
// Charged
Skill((Bow(CDamage), Some(3))),
Skill((Bow(CKnockback), Some(2))),
Skill((Bow(CSpeed), Some(2))),
Skill((Bow(CRegen), Some(2))),
Skill((Bow(CMove), Some(2))),
// Repeater
Skill((Bow(RDamage), Some(3))),
Skill((Bow(RCost), Some(2))),
Skill((Bow(RSpeed), Some(2))),
// Shotgun
Skill((Bow(UnlockShotgun), None)),
Skill((Bow(SDamage), Some(2))),
Skill((Bow(SCost), Some(2))),
Skill((Bow(SArrows), Some(2))),
Skill((Bow(SSpread), Some(2))),
])

View File

@ -0,0 +1,23 @@
([
Group(Weapon(Hammer)),
// Single Strike, as single as you are
Skill((Hammer(SsKnockback), Some(2))),
Skill((Hammer(SsDamage), Some(3))),
Skill((Hammer(SsRegen), Some(2))),
Skill((Hammer(SsSpeed), Some(3))),
// Charged
Skill((Hammer(CDamage), Some(3))),
Skill((Hammer(CKnockback), Some(3))),
Skill((Hammer(CDrain), Some(2))),
Skill((Hammer(CSpeed), Some(2))),
// Leap
Skill((Hammer(UnlockLeap), None)),
Skill((Hammer(LDamage), Some(2))),
Skill((Hammer(LCost), Some(2))),
Skill((Hammer(LDistance), Some(2))),
Skill((Hammer(LKnockback), Some(2))),
Skill((Hammer(LRange), Some(2))),
])

View File

@ -0,0 +1,21 @@
([
Group(Weapon(Staff)),
// Fireball
Skill((Staff(BDamage), Some(3))),
Skill((Staff(BRegen), Some(2))),
Skill((Staff(BRadius), Some(3))),
// Flamethrower
Skill((Staff(FRange), Some(2))),
Skill((Staff(FDamage), Some(3))),
Skill((Staff(FDrain), Some(2))),
Skill((Staff(FVelocity), Some(2))),
// Shockwave
Skill((Staff(UnlockShockwave), None)),
Skill((Staff(SDamage), Some(2))),
Skill((Staff(SKnockback), Some(2))),
Skill((Staff(SRange), Some(2))),
Skill((Staff(SCost), Some(2))),
])

View File

@ -0,0 +1,26 @@
([
Group(Weapon(Sword)),
Skill((Sword(InterruptingAttacks), None)),
// TripleStrike
Skill((Sword(TsCombo), None)),
Skill((Sword(TsDamage), Some(3))),
Skill((Sword(TsRegen), Some(2))),
Skill((Sword(TsSpeed), Some(3))),
// Dash
Skill((Sword(DCost), Some(2))),
Skill((Sword(DDrain), Some(2))),
Skill((Sword(DDamage), Some(2))),
Skill((Sword(DScaling), Some(3))),
Skill((Sword(DSpeed), None)),
Skill((Sword(DInfinite), None)),
// Spin of death
Skill((Sword(UnlockSpin), None)),
Skill((Sword(SDamage), Some(2))),
Skill((Sword(SSpeed), Some(2))),
Skill((Sword(SSpins), Some(2))),
Skill((Sword(SCost), Some(2))),
])

View File

@ -314,6 +314,15 @@
("common.items.armor.ferocious.back",1),
("common.items.weapons.sword.bloodsteel-1",1),
],
"cultist": [
("common.items.armor.cultist.chest",1),
("common.items.armor.cultist.pants",1),
("common.items.armor.cultist.hand",1),
("common.items.armor.cultist.foot",1),
("common.items.armor.cultist.shoulder",1),
("common.items.armor.cultist.belt",1),
("common.items.armor.misc.back.dungeon_purple",1),
],
"admin_cosmetics": [
("common.items.debug.cultist_belt",1),
("common.items.debug.cultist_boots",1),

View File

@ -625,6 +625,7 @@ impl Body {
biped_large::Species::Dullahan => 120,
biped_large::Species::Huskbrute => 100,
// Boss enemies have their health set, not adjusted by level.
biped_large::Species::Huskbrute => 0,
biped_large::Species::Mindflayer => 0,
biped_large::Species::Minotaur => 0,
biped_large::Species::Tidalwarrior => 0,

View File

@ -161,14 +161,9 @@ fn default_main_tool(body: &Body) -> Item {
_ => None,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Wolf
| quadruped_medium::Species::Grolgar
| quadruped_medium::Species::Lion
| quadruped_medium::Species::Bonerattler
| quadruped_medium::Species::Darkhound
| quadruped_medium::Species::Snowleopard => Some(Item::new_from_asset_expect(
"common.items.npc_weapons.unique.quadmedquick",
)),
quadruped_medium::Species::Wolf | quadruped_medium::Species::Grolgar => Some(
Item::new_from_asset_expect("common.items.npc_weapons.unique.quadmedquick"),
),
quadruped_medium::Species::Donkey
| quadruped_medium::Species::Horse
| quadruped_medium::Species::Zebra
@ -180,7 +175,11 @@ fn default_main_tool(body: &Body) -> Item {
| quadruped_medium::Species::Alpaca => Some(Item::new_from_asset_expect(
"common.items.npc_weapons.unique.quadmedhoof",
)),
quadruped_medium::Species::Saber => Some(Item::new_from_asset_expect(
quadruped_medium::Species::Saber
| quadruped_medium::Species::Bonerattler
| quadruped_medium::Species::Darkhound
| quadruped_medium::Species::Lion
| quadruped_medium::Species::Snowleopard => Some(Item::new_from_asset_expect(
"common.items.npc_weapons.unique.quadmedjump",
)),
quadruped_medium::Species::Tuskram
@ -398,88 +397,73 @@ impl LoadoutBuilder {
#[must_use]
/// Set default equipement based on `body`
pub fn with_default_equipment(mut self, body: &Body) -> Self {
self = match body {
Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Mindflayer,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.biped_large.mindflayer",
))),
Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Minotaur,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.biped_large.minotaur",
))),
Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Tidalwarrior,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.biped_large.tidal_warrior",
))),
Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Yeti,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.biped_large.yeti",
))),
Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Harvester,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.biped_large.harvester",
))),
Body::BipedLarge(biped_large::Body {
species:
biped_large::Species::Ogre
| biped_large::Species::Cyclops
| biped_large::Species::Blueoni
| biped_large::Species::Redoni
| biped_large::Species::Cavetroll
| biped_large::Species::Wendigo,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.biped_large.generic",
))),
Body::Golem(golem::Body {
species: golem::Species::ClayGolem,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.golem.claygolem",
))),
Body::QuadrupedLow(quadruped_low::Body {
species:
quadruped_low::Species::Basilisk
| quadruped_low::Species::Asp
| quadruped_low::Species::Lavadrake
| quadruped_low::Species::Maneater
| quadruped_low::Species::Rocksnapper
| quadruped_low::Species::Sandshark,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.quadruped_low.generic",
))),
Body::QuadrupedLow(quadruped_low::Body {
species: quadruped_low::Species::Tortoise,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.quadruped_low.shell",
))),
Body::Theropod(theropod::Body {
species:
theropod::Species::Archaeos
| theropod::Species::Yale
| theropod::Species::Ntouka
| theropod::Species::Odonto,
..
}) => self.chest(Some(Item::new_from_asset_expect(
"common.items.npc_armor.theropod.rugged",
))),
_ => self,
pub fn with_default_equipment(self, body: &Body) -> Self {
let chest = match body {
Body::BipedLarge(body) => match body.species {
biped_large::Species::Mindflayer => {
Some("common.items.npc_armor.biped_large.mindflayer")
},
biped_large::Species::Minotaur => {
Some("common.items.npc_armor.biped_large.minotaur")
},
biped_large::Species::Tidalwarrior => {
Some("common.items.npc_armor.biped_large.tidal_warrior")
},
biped_large::Species::Yeti => Some("common.items.npc_armor.biped_large.yeti"),
biped_large::Species::Harvester => {
Some("common.items.npc_armor.biped_large.harvester")
},
biped_large::Species::Ogre
| biped_large::Species::Cyclops
| biped_large::Species::Blueoni
| biped_large::Species::Redoni
| biped_large::Species::Cavetroll
| biped_large::Species::Wendigo => {
Some("common.items.npc_armor.biped_large.generic")
},
biped_large::Species::Cultistwarlord => {
Some("common.items.npc_armor.biped_large.warlord")
},
biped_large::Species::Cultistwarlock => {
Some("common.items.npc_armor.biped_large.warlock")
},
_ => None,
},
Body::Golem(body) => match body.species {
golem::Species::ClayGolem => Some("common.items.npc_armor.golem.claygolem"),
_ => None,
},
Body::QuadrupedLow(body) => match body.species {
quadruped_low::Species::Basilisk
| quadruped_low::Species::Asp
| quadruped_low::Species::Lavadrake
| quadruped_low::Species::Maneater
| quadruped_low::Species::Rocksnapper
| quadruped_low::Species::Sandshark => {
Some("common.items.npc_armor.quadruped_low.generic")
},
quadruped_low::Species::Tortoise => {
Some("common.items.npc_armor.quadruped_low.shell")
},
_ => None,
},
Body::Theropod(body) => match body.species {
theropod::Species::Archaeos
| theropod::Species::Yale
| theropod::Species::Ntouka
| theropod::Species::Odonto => Some("common.items.npc_armor.theropod.rugged"),
_ => None,
},
_ => None,
};
self
// closures can't be used here, because it moves value
#[allow(clippy::option_if_let_else)]
if let Some(chest) = chest {
self.chest(Some(Item::new_from_asset_expect(chest)))
} else {
self
}
}
#[must_use]

View File

@ -184,18 +184,195 @@ impl Tile {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum RoomKind {
Peaceful,
Fight,
Boss,
Miniboss,
}
pub struct Room {
seed: u32,
loot_density: f32,
enemy_density: Option<f32>,
miniboss: bool,
boss: bool,
kind: RoomKind,
area: Rect<i32, i32>,
height: i32,
pillars: Option<i32>, // Pillars with the given separation
difficulty: u32,
}
impl Room {
fn fill_fight_cell(
&self,
supplement: &mut ChunkSupplement,
dynamic_rng: &mut impl Rng,
tile_wcenter: Vec3<i32>,
wpos2d: Vec2<i32>,
tile_pos: Vec2<i32>,
) {
let enemy_spawn_tile = self.area.center();
// Don't spawn enemies in a pillar
let enemy_tile_is_pillar = self.pillars.map_or(false, |pillar_space| {
enemy_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
});
let enemy_spawn_tile = enemy_spawn_tile + if enemy_tile_is_pillar { 1 } else { 0 };
// Toss mobs in the center of the room
if tile_pos == enemy_spawn_tile && wpos2d == tile_wcenter.xy() {
let entities = match self.difficulty {
0 => enemy_0(dynamic_rng, tile_wcenter),
1 => enemy_1(dynamic_rng, tile_wcenter),
2 => enemy_2(dynamic_rng, tile_wcenter),
3 => enemy_3(dynamic_rng, tile_wcenter),
4 => enemy_4(dynamic_rng, tile_wcenter),
5 => enemy_5(dynamic_rng, tile_wcenter),
_ => enemy_fallback(dynamic_rng, tile_wcenter),
};
for entity in entities {
supplement.add_entity(
entity
.with_level(
dynamic_rng
.gen_range(
(self.difficulty as f32).powf(1.25) + 3.0
..(self.difficulty as f32).powf(1.5) + 4.0,
)
.round() as u16,
)
.with_alignment(comp::Alignment::Enemy),
);
}
} else {
// Turrets
// Turret has 1/5000 chance to spawn per voxel in fight room
if dynamic_rng.gen_range(0..5000) == 0 {
let pos = tile_wcenter.map(|e| e as f32)
+ Vec3::<u32>::iota()
.map(|e| {
(RandomField::new(self.seed.wrapping_add(10 + e))
.get(Vec3::from(tile_pos))
% 32) as i32
- 16
})
.map(|e| e as f32 / 16.0);
let turret =
EntityInfo::at(pos.map(|e| e as f32)).with_alignment(comp::Alignment::Enemy);
match self.difficulty {
3 => {
let turret = turret
.with_body(comp::Body::Object(comp::object::Body::Crossbow))
.with_asset_expect("common.entity.dungeon.tier-3.sentry");
supplement.add_entity(turret);
},
5 => {
let turret = turret
.with_body(comp::Body::Object(comp::object::Body::Crossbow))
.with_asset_expect("common.entity.dungeon.tier-5.turret");
supplement.add_entity(turret);
},
_ => {},
};
}
}
}
fn fill_miniboss_cell(
&self,
supplement: &mut ChunkSupplement,
dynamic_rng: &mut impl Rng,
tile_wcenter: Vec3<i32>,
wpos2d: Vec2<i32>,
tile_pos: Vec2<i32>,
) {
let miniboss_spawn_tile = self.area.center();
// Don't spawn the miniboss in a pillar
let miniboss_tile_is_pillar = self.pillars.map_or(false, |pillar_space| {
miniboss_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
});
let miniboss_spawn_tile = miniboss_spawn_tile + if miniboss_tile_is_pillar { 1 } else { 0 };
if tile_pos == miniboss_spawn_tile && tile_wcenter.xy() == wpos2d {
let entities = match self.difficulty {
0 => mini_boss_0(tile_wcenter),
1 => mini_boss_1(tile_wcenter),
2 => mini_boss_2(tile_wcenter),
3 => mini_boss_3(tile_wcenter),
4 => mini_boss_4(tile_wcenter),
5 => mini_boss_5(dynamic_rng, tile_wcenter),
_ => mini_boss_fallback(tile_wcenter),
};
for entity in entities {
supplement.add_entity(
entity
.with_level(
dynamic_rng
.gen_range(
(self.difficulty as f32).powf(1.25) + 3.0
..(self.difficulty as f32).powf(1.5) + 4.0,
)
.round() as u16
* 5,
)
.with_alignment(comp::Alignment::Enemy),
);
}
}
}
fn fill_boss_cell(
&self,
supplement: &mut ChunkSupplement,
dynamic_rng: &mut impl Rng,
tile_wcenter: Vec3<i32>,
wpos2d: Vec2<i32>,
tile_pos: Vec2<i32>,
) {
let boss_spawn_tile = self.area.center();
// Don't spawn the boss in a pillar
let boss_tile_is_pillar = self.pillars.map_or(false, |pillar_space| {
boss_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
});
let boss_spawn_tile = boss_spawn_tile + if boss_tile_is_pillar { 1 } else { 0 };
if tile_pos == boss_spawn_tile && wpos2d == tile_wcenter.xy() {
let entities = match self.difficulty {
0 => boss_0(tile_wcenter),
1 => boss_1(tile_wcenter),
2 => boss_2(tile_wcenter),
3 => boss_3(tile_wcenter),
4 => boss_4(tile_wcenter),
5 => boss_5(tile_wcenter),
_ => boss_fallback(tile_wcenter),
};
for entity in entities {
supplement.add_entity(
entity
.with_level(
dynamic_rng
.gen_range(
(self.difficulty as f32).powf(1.25) + 3.0
..(self.difficulty as f32).powf(1.5) + 4.0,
)
.round() as u16
* 5,
)
.with_alignment(comp::Alignment::Enemy),
);
}
}
}
}
struct Floor {
tile_offset: Vec2<i32>,
tiles: Grid<Tile>,
@ -252,9 +429,7 @@ impl Floor {
let upstair_room = this.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: None,
miniboss: false,
boss: false,
kind: RoomKind::Peaceful,
area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))),
height: STAIR_ROOM_HEIGHT,
pillars: None,
@ -265,9 +440,7 @@ impl Floor {
this.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: Some((0.0002 * difficulty as f32).min(0.001)), // Minions!
miniboss: false,
boss: true,
kind: RoomKind::Boss,
area: Rect::from((
new_stair_tile - tile_offset - MAX_WIDTH as i32 - 1,
Extent2::broadcast(width as i32 * 2 + 1),
@ -281,9 +454,7 @@ impl Floor {
let downstair_room = this.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: None,
miniboss: false,
boss: false,
kind: RoomKind::Peaceful,
area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))),
height: STAIR_ROOM_HEIGHT,
pillars: None,
@ -358,23 +529,21 @@ impl Floor {
let mut dynamic_rng = rand::thread_rng();
match dynamic_rng.gen_range(0..5) {
// Miniboss room
0 => self.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.000025 + level as f32 * 0.00015,
enemy_density: None,
miniboss: true,
boss: false,
kind: RoomKind::Miniboss,
area,
height: ctx.rng.gen_range(15..20),
pillars: Some(ctx.rng.gen_range(2..=4)),
difficulty: self.difficulty,
}),
// Fight room with enemies in it
_ => self.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.000025 + level as f32 * 0.00015,
enemy_density: Some(0.001 + level as f32 * 0.00006),
miniboss: false,
boss: false,
kind: RoomKind::Fight,
area,
height: ctx.rng.gen_range(10..15),
pillars: if ctx.rng.gen_range(0..4) == 0 {
@ -433,7 +602,6 @@ impl Floor {
}
}
#[allow(clippy::match_single_binding)] // TODO: Pending review in #587
fn apply_supplement(
&self,
// NOTE: Used only for dynamic elements like chests and entities!
@ -477,141 +645,29 @@ impl Floor {
.map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2),
);
let tile_is_pillar = room
.pillars
.map(|pillar_space| {
tile_pos
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
})
.unwrap_or(false);
if room
.enemy_density
.map(|density| dynamic_rng.gen_range(0..density.recip() as usize) == 0)
.unwrap_or(false)
&& !tile_is_pillar
&& !room.boss
{
// Randomly displace them a little
let raw_entity = EntityInfo::at(
tile_wcenter.map(|e| e as f32)
+ Vec3::<u32>::iota()
.map(|e| {
(RandomField::new(room.seed.wrapping_add(10 + e))
.get(Vec3::from(tile_pos))
% 32) as i32
- 16
})
.map(|e| e as f32 / 16.0),
);
let entity = match room.difficulty {
0 => enemy_0(dynamic_rng, raw_entity),
1 => enemy_1(dynamic_rng, raw_entity),
2 => enemy_2(dynamic_rng, raw_entity),
3 => enemy_3(dynamic_rng, raw_entity),
4 => enemy_4(dynamic_rng, raw_entity),
5 => enemy_5(dynamic_rng, raw_entity),
_ => enemy_fallback(raw_entity),
};
supplement.add_entity(
entity.with_alignment(comp::Alignment::Enemy).with_level(
dynamic_rng
.gen_range(
(room.difficulty as f32).powf(1.25) + 3.0
..(room.difficulty as f32).powf(1.5) + 4.0,
)
.round() as u16,
),
);
}
if room.boss {
let boss_spawn_tile = room.area.center();
// Don't spawn the boss in a pillar
let boss_tile_is_pillar = room
.pillars
.map(|pillar_space| {
boss_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
})
.unwrap_or(false);
let boss_spawn_tile =
boss_spawn_tile + if boss_tile_is_pillar { 1 } else { 0 };
if tile_pos == boss_spawn_tile && tile_wcenter.xy() == wpos2d {
let entities = match room.difficulty {
0 => boss_0(tile_wcenter),
1 => boss_1(tile_wcenter),
2 => boss_2(tile_wcenter),
3 => boss_3(tile_wcenter),
4 => boss_4(tile_wcenter),
5 => boss_5(tile_wcenter),
_ => boss_fallback(tile_wcenter),
};
for entity in entities {
supplement.add_entity(
entity
.with_level(
dynamic_rng
.gen_range(
(room.difficulty as f32).powf(1.25) + 3.0
..(room.difficulty as f32).powf(1.5) + 4.0,
)
.round()
as u16
* 5,
)
.with_alignment(comp::Alignment::Enemy),
);
}
}
}
if room.miniboss {
let miniboss_spawn_tile = room.area.center();
// Don't spawn the miniboss in a pillar
let miniboss_tile_is_pillar = room
.pillars
.map(|pillar_space| {
miniboss_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
})
.unwrap_or(false);
let miniboss_spawn_tile =
miniboss_spawn_tile + if miniboss_tile_is_pillar { 1 } else { 0 };
if tile_pos == miniboss_spawn_tile && tile_wcenter.xy() == wpos2d {
let entities = match room.difficulty {
0 => mini_boss_0(tile_wcenter),
1 => mini_boss_1(tile_wcenter),
2 => mini_boss_2(tile_wcenter),
3 => mini_boss_3(tile_wcenter),
4 => mini_boss_4(tile_wcenter),
5 => mini_boss_5(dynamic_rng, tile_wcenter),
_ => mini_boss_fallback(tile_wcenter),
};
for entity in entities {
supplement.add_entity(
entity
.with_level(
dynamic_rng
.gen_range(
(room.difficulty as f32).powf(1.25) + 3.0
..(room.difficulty as f32).powf(1.5) + 4.0,
)
.round()
as u16
* 5,
)
.with_alignment(comp::Alignment::Enemy),
);
}
}
match room.kind {
RoomKind::Fight => room.fill_fight_cell(
supplement,
dynamic_rng,
tile_wcenter,
wpos2d,
tile_pos,
),
RoomKind::Miniboss => room.fill_miniboss_cell(
supplement,
dynamic_rng,
tile_wcenter,
wpos2d,
tile_pos,
),
RoomKind::Boss => room.fill_boss_cell(
supplement,
dynamic_rng,
tile_wcenter,
wpos2d,
tile_pos,
),
RoomKind::Peaceful => {},
}
}
}
@ -631,61 +687,105 @@ impl Floor {
}
}
fn enemy_0(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {
match dynamic_rng.gen_range(0..5) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-0.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-0.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-0.spear"),
}
fn enemy_0(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(2..=4);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
match dynamic_rng.gen_range(0..=4) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-0.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-0.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-0.spear"),
}
});
entities
}
fn enemy_1(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {
match dynamic_rng.gen_range(0..5) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-1.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-1.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-1.spear"),
}
fn enemy_1(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(2..=4);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
match dynamic_rng.gen_range(0..=4) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-1.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-1.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-1.spear"),
}
});
entities
}
fn enemy_2(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {
match dynamic_rng.gen_range(0..5) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-2.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-2.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-2.spear"),
}
fn enemy_2(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(2..=4);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
match dynamic_rng.gen_range(0..=4) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-2.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-2.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-2.spear"),
}
});
entities
}
fn enemy_3(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {
match dynamic_rng.gen_range(0..5) {
0 => entity
.with_body(comp::Body::Object(comp::object::Body::HaniwaSentry))
.with_asset_expect("common.entity.dungeon.tier-3.sentry"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-3.bow"),
2 => entity.with_asset_expect("common.entity.dungeon.tier-3.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-3.spear"),
}
}
fn enemy_4(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {
match dynamic_rng.gen_range(0..5) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-4.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-4.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-4.spear"),
}
fn enemy_3(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(2..=4);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
match dynamic_rng.gen_range(0..=4) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-3.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-3.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-3.spear"),
}
});
entities
}
fn enemy_5(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {
match dynamic_rng.gen_range(0..6) {
0 => entity
.with_body(comp::Body::Object(comp::object::Body::Crossbow))
.with_asset_expect("common.entity.dungeon.tier-5.turret"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlock"),
2 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlord"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-5.cultist"),
}
fn enemy_4(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(2..=4);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
match dynamic_rng.gen_range(0..=4) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-4.bow"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-4.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-4.spear"),
}
});
entities
}
fn enemy_fallback(entity: EntityInfo) -> EntityInfo {
entity.with_asset_expect("common.entity.dungeon.fallback.enemy")
fn enemy_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(1..=3);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
match dynamic_rng.gen_range(0..=4) {
0 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlock"),
1 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlord"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-5.cultist"),
}
});
entities
}
fn enemy_fallback(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let number = dynamic_rng.gen_range(2..=4);
let mut entities = Vec::new();
entities.resize_with(number, || {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
entity.with_asset_expect("common.entity.dungeon.fallback.enemy")
});
entities
}
fn boss_0(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
@ -782,7 +882,7 @@ fn mini_boss_4(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
fn mini_boss_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let mut entities = Vec::new();
match dynamic_rng.gen_range(0..3) {
match dynamic_rng.gen_range(0..=2) {
0 => {
entities.push(
EntityInfo::at(tile_wcenter.map(|e| e as f32))
@ -816,51 +916,6 @@ fn mini_boss_fallback(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_creating_bosses() {
let tile_wcenter = Vec3::new(0, 0, 0);
boss_0(tile_wcenter);
boss_1(tile_wcenter);
boss_2(tile_wcenter);
boss_3(tile_wcenter);
boss_4(tile_wcenter);
boss_5(tile_wcenter);
boss_fallback(tile_wcenter);
}
#[test]
// FIXME: Uses random, test may be not great
fn test_creating_enemies() {
let mut dynamic_rng = rand::thread_rng();
let raw_entity = EntityInfo::at(Vec3::new(0.0, 0.0, 0.0));
enemy_0(&mut dynamic_rng, raw_entity.clone());
enemy_1(&mut dynamic_rng, raw_entity.clone());
enemy_2(&mut dynamic_rng, raw_entity.clone());
enemy_3(&mut dynamic_rng, raw_entity.clone());
enemy_4(&mut dynamic_rng, raw_entity.clone());
enemy_5(&mut dynamic_rng, raw_entity.clone());
enemy_fallback(raw_entity);
}
#[test]
// FIXME: Uses random, test may be not great
fn test_creating_minibosses() {
let mut dynamic_rng = rand::thread_rng();
let tile_wcenter = Vec3::new(0, 0, 0);
mini_boss_0(tile_wcenter);
mini_boss_1(tile_wcenter);
mini_boss_2(tile_wcenter);
mini_boss_3(tile_wcenter);
mini_boss_4(tile_wcenter);
mini_boss_5(&mut dynamic_rng, tile_wcenter);
mini_boss_fallback(tile_wcenter);
}
}
pub fn tilegrid_nearest_wall(tiles: &Grid<Tile>, rpos: Vec2<i32>) -> Option<Vec2<i32>> {
let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE));
@ -1222,7 +1277,7 @@ impl Floor {
prim(Primitive::Scale(pillar, Vec2::broadcast(scale).with_z(1.0)));
lights = prim(Primitive::And(lighting_plane, lights));
// Only add the base (and shift the lights up) for boss-room pillars
if room.boss {
if room.kind == RoomKind::Boss {
lights = prim(Primitive::Translate(lights, 3 * Vec3::unit_z()));
pillar = prim(Primitive::Or(pillar, base));
}
@ -1231,7 +1286,7 @@ impl Floor {
}
// Keep track of the boss room to be able to add decorations later
if room.boss {
if room.kind == RoomKind::Boss {
boss_room_center =
Some(floor_corner + TILE_SIZE * room.area.center() + TILE_SIZE / 2);
}
@ -1334,3 +1389,48 @@ impl SiteStructure for Dungeon {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_creating_bosses() {
let tile_wcenter = Vec3::new(0, 0, 0);
boss_0(tile_wcenter);
boss_1(tile_wcenter);
boss_2(tile_wcenter);
boss_3(tile_wcenter);
boss_4(tile_wcenter);
boss_5(tile_wcenter);
boss_fallback(tile_wcenter);
}
#[test]
// FIXME: Uses random, test may be not great
fn test_creating_enemies() {
let mut dynamic_rng = rand::thread_rng();
let random_position = Vec3::new(0, 0, 0);
enemy_0(&mut dynamic_rng, random_position);
enemy_1(&mut dynamic_rng, random_position);
enemy_2(&mut dynamic_rng, random_position);
enemy_3(&mut dynamic_rng, random_position);
enemy_4(&mut dynamic_rng, random_position);
enemy_5(&mut dynamic_rng, random_position);
enemy_fallback(&mut dynamic_rng, random_position);
}
#[test]
// FIXME: Uses random, test may be not great
fn test_creating_minibosses() {
let mut dynamic_rng = rand::thread_rng();
let tile_wcenter = Vec3::new(0, 0, 0);
mini_boss_0(tile_wcenter);
mini_boss_1(tile_wcenter);
mini_boss_2(tile_wcenter);
mini_boss_3(tile_wcenter);
mini_boss_4(tile_wcenter);
mini_boss_5(&mut dynamic_rng, tile_wcenter);
mini_boss_fallback(tile_wcenter);
}
}