More deterministic dungeon fights

- Spawn fixed number of enemies (2-4) in a room with any size. 1-3 enemies for T5 dungeon.
This commit is contained in:
juliancoffee 2021-06-27 18:13:18 +03:00
parent 5b2ef98a7d
commit 63a9ceda29

View File

@ -187,7 +187,7 @@ impl Tile {
pub struct Room { pub struct Room {
seed: u32, seed: u32,
loot_density: f32, loot_density: f32,
enemy_density: Option<f32>, fight: bool,
miniboss: bool, miniboss: bool,
boss: bool, boss: bool,
area: Rect<i32, i32>, area: Rect<i32, i32>,
@ -252,7 +252,7 @@ impl Floor {
let upstair_room = this.create_room(Room { let upstair_room = this.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.0, loot_density: 0.0,
enemy_density: None, fight: false,
miniboss: false, miniboss: false,
boss: false, boss: false,
area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))), area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))),
@ -265,7 +265,8 @@ impl Floor {
this.create_room(Room { this.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.0, loot_density: 0.0,
enemy_density: Some((0.0002 * difficulty as f32).min(0.001)), // Minions! // Our bosses are intelligent enough to summon minions if needed.
fight: false,
miniboss: false, miniboss: false,
boss: true, boss: true,
area: Rect::from(( area: Rect::from((
@ -281,7 +282,7 @@ impl Floor {
let downstair_room = this.create_room(Room { let downstair_room = this.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.0, loot_density: 0.0,
enemy_density: None, fight: false,
miniboss: false, miniboss: false,
boss: false, boss: false,
area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))), area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))),
@ -358,10 +359,11 @@ impl Floor {
let mut dynamic_rng = rand::thread_rng(); let mut dynamic_rng = rand::thread_rng();
match dynamic_rng.gen_range(0..5) { match dynamic_rng.gen_range(0..5) {
// Miniboss room
0 => self.create_room(Room { 0 => self.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.000025 + level as f32 * 0.00015, loot_density: 0.000025 + level as f32 * 0.00015,
enemy_density: None, fight: false,
miniboss: true, miniboss: true,
boss: false, boss: false,
area, area,
@ -369,10 +371,11 @@ impl Floor {
pillars: Some(ctx.rng.gen_range(2..=4)), pillars: Some(ctx.rng.gen_range(2..=4)),
difficulty: self.difficulty, difficulty: self.difficulty,
}), }),
// Fight room with enemies in it
_ => self.create_room(Room { _ => self.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.000025 + level as f32 * 0.00015, loot_density: 0.000025 + level as f32 * 0.00015,
enemy_density: Some(0.001 + level as f32 * 0.00006), fight: true,
miniboss: false, miniboss: false,
boss: false, boss: false,
area, area,
@ -433,7 +436,6 @@ impl Floor {
} }
} }
#[allow(clippy::match_single_binding)] // TODO: Pending review in #587
fn apply_supplement( fn apply_supplement(
&self, &self,
// NOTE: Used only for dynamic elements like chests and entities! // NOTE: Used only for dynamic elements like chests and entities!
@ -477,79 +479,108 @@ impl Floor {
.map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2), .map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2),
); );
let tile_is_pillar = room // Fill regular rooms
.pillars if room.fight {
.map(|pillar_space| { let enemy_spawn_tile = room.area.center();
tile_pos // Don't spawn enemies in a pillar
let enemy_tile_is_pillar = room.pillars.map_or(false, |pillar_space| {
enemy_spawn_tile
.map(|e| e.rem_euclid(pillar_space) == 0) .map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and() .reduce_and()
}) });
.unwrap_or(false); let enemy_spawn_tile =
enemy_spawn_tile + if enemy_tile_is_pillar { 1 } else { 0 };
if room // Toss mobs in the center of the room
.enemy_density if tile_pos == enemy_spawn_tile && wpos2d == tile_wcenter.xy() {
.map(|density| dynamic_rng.gen_range(0..density.recip() as usize) == 0) let entities = match room.difficulty {
.unwrap_or(false) 0 => enemy_0(dynamic_rng, tile_wcenter),
&& !tile_is_pillar 1 => enemy_1(dynamic_rng, tile_wcenter),
&& !room.boss 2 => enemy_2(dynamic_rng, tile_wcenter),
{ 3 => enemy_3(dynamic_rng, tile_wcenter),
// Randomly displace them a little 4 => enemy_4(dynamic_rng, tile_wcenter),
let raw_entity = EntityInfo::at( 5 => enemy_5(dynamic_rng, tile_wcenter),
tile_wcenter.map(|e| e as f32) _ => enemy_fallback(dynamic_rng, tile_wcenter),
+ 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 { for entity in entities {
0 => enemy_0(dynamic_rng, raw_entity), supplement.add_entity(
1 => enemy_1(dynamic_rng, raw_entity), entity
2 => enemy_2(dynamic_rng, raw_entity), .with_level(
3 => enemy_3(dynamic_rng, raw_entity), dynamic_rng
4 => enemy_4(dynamic_rng, raw_entity), .gen_range(
5 => enemy_5(dynamic_rng, raw_entity), (room.difficulty as f32).powf(1.25) + 3.0
_ => enemy_fallback(raw_entity), ..(room.difficulty as f32).powf(1.5) + 4.0,
}; )
supplement.add_entity( .round()
entity.with_alignment(comp::Alignment::Enemy).with_level( as u16,
dynamic_rng )
.gen_range( .with_alignment(comp::Alignment::Enemy),
(room.difficulty as f32).powf(1.25) + 3.0 );
..(room.difficulty as f32).powf(1.5) + 4.0, }
) } else {
.round() as u16, // Turrets
), if dynamic_rng.gen_range(0..5000) == 0 {
); let pos = 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 turret = EntityInfo::at(pos.map(|e| e as f32))
.with_alignment(comp::Alignment::Enemy);
match room.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);
},
_ => {},
};
}
}
} }
if room.boss { // Spawn miniboss (or minibosses)
let boss_spawn_tile = room.area.center(); if room.miniboss {
// Don't spawn the boss in a pillar let miniboss_spawn_tile = room.area.center();
let boss_tile_is_pillar = room // Don't spawn the miniboss in a pillar
.pillars let miniboss_tile_is_pillar = room.pillars.map_or(false, |pillar_space| {
.map(|pillar_space| { miniboss_spawn_tile
boss_spawn_tile .map(|e| e.rem_euclid(pillar_space) == 0)
.map(|e| e.rem_euclid(pillar_space) == 0) .reduce_and()
.reduce_and() });
}) let miniboss_spawn_tile =
.unwrap_or(false); miniboss_spawn_tile + if miniboss_tile_is_pillar { 1 } else { 0 };
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 { if tile_pos == miniboss_spawn_tile && tile_wcenter.xy() == wpos2d {
let entities = match room.difficulty { let entities = match room.difficulty {
0 => boss_0(tile_wcenter), 0 => mini_boss_0(tile_wcenter),
1 => boss_1(tile_wcenter), 1 => mini_boss_1(tile_wcenter),
2 => boss_2(tile_wcenter), 2 => mini_boss_2(tile_wcenter),
3 => boss_3(tile_wcenter), 3 => mini_boss_3(tile_wcenter),
4 => boss_4(tile_wcenter), 4 => mini_boss_4(tile_wcenter),
5 => boss_5(tile_wcenter), 5 => mini_boss_5(dynamic_rng, tile_wcenter),
_ => boss_fallback(tile_wcenter), _ => mini_boss_fallback(tile_wcenter),
}; };
for entity in entities { for entity in entities {
@ -570,29 +601,28 @@ impl Floor {
} }
} }
} }
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 { // Spawn miniboss
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_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 room.difficulty { let entities = match room.difficulty {
0 => mini_boss_0(tile_wcenter), 0 => boss_0(tile_wcenter),
1 => mini_boss_1(tile_wcenter), 1 => boss_1(tile_wcenter),
2 => mini_boss_2(tile_wcenter), 2 => boss_2(tile_wcenter),
3 => mini_boss_3(tile_wcenter), 3 => boss_3(tile_wcenter),
4 => mini_boss_4(tile_wcenter), 4 => boss_4(tile_wcenter),
5 => mini_boss_5(dynamic_rng, tile_wcenter), 5 => boss_5(tile_wcenter),
_ => mini_boss_fallback(tile_wcenter), _ => boss_fallback(tile_wcenter),
}; };
for entity in entities { for entity in entities {
@ -631,61 +661,105 @@ impl Floor {
} }
} }
fn enemy_0(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { fn enemy_0(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
match dynamic_rng.gen_range(0..5) { let number = dynamic_rng.gen_range(2..=4);
0 => entity.with_asset_expect("common.entity.dungeon.tier-0.bow"), let mut entities = Vec::new();
1 => entity.with_asset_expect("common.entity.dungeon.tier-0.staff"), entities.resize_with(number, || {
_ => entity.with_asset_expect("common.entity.dungeon.tier-0.spear"), 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 { fn enemy_1(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
match dynamic_rng.gen_range(0..5) { let number = dynamic_rng.gen_range(2..=4);
0 => entity.with_asset_expect("common.entity.dungeon.tier-1.bow"), let mut entities = Vec::new();
1 => entity.with_asset_expect("common.entity.dungeon.tier-1.staff"), entities.resize_with(number, || {
_ => entity.with_asset_expect("common.entity.dungeon.tier-1.spear"), 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 { fn enemy_2(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
match dynamic_rng.gen_range(0..5) { let number = dynamic_rng.gen_range(2..=4);
0 => entity.with_asset_expect("common.entity.dungeon.tier-2.bow"), let mut entities = Vec::new();
1 => entity.with_asset_expect("common.entity.dungeon.tier-2.staff"), entities.resize_with(number, || {
_ => entity.with_asset_expect("common.entity.dungeon.tier-2.spear"), 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 { fn enemy_3(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
match dynamic_rng.gen_range(0..5) { let number = dynamic_rng.gen_range(2..=4);
0 => entity let mut entities = Vec::new();
.with_body(comp::Body::Object(comp::object::Body::HaniwaSentry)) entities.resize_with(number, || {
.with_asset_expect("common.entity.dungeon.tier-3.sentry"), let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
1 => entity.with_asset_expect("common.entity.dungeon.tier-3.bow"), match dynamic_rng.gen_range(0..=4) {
2 => entity.with_asset_expect("common.entity.dungeon.tier-3.staff"), 0 => entity.with_asset_expect("common.entity.dungeon.tier-3.bow"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-3.spear"), 1 => 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"), entities
1 => entity.with_asset_expect("common.entity.dungeon.tier-4.staff"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-4.spear"),
}
} }
fn enemy_5(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo { fn enemy_4(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
match dynamic_rng.gen_range(0..6) { let number = dynamic_rng.gen_range(2..=4);
0 => entity let mut entities = Vec::new();
.with_body(comp::Body::Object(comp::object::Body::Crossbow)) entities.resize_with(number, || {
.with_asset_expect("common.entity.dungeon.tier-5.turret"), let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
1 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlock"), match dynamic_rng.gen_range(0..=4) {
2 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlord"), 0 => entity.with_asset_expect("common.entity.dungeon.tier-4.bow"),
_ => entity.with_asset_expect("common.entity.dungeon.tier-5.cultist"), 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 { fn enemy_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
entity.with_asset_expect("common.entity.dungeon.fallback.enemy") 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> { fn boss_0(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
@ -782,7 +856,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> { fn mini_boss_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
let mut entities = Vec::new(); let mut entities = Vec::new();
match dynamic_rng.gen_range(0..3) { match dynamic_rng.gen_range(0..=2) {
0 => { 0 => {
entities.push( entities.push(
EntityInfo::at(tile_wcenter.map(|e| e as f32)) EntityInfo::at(tile_wcenter.map(|e| e as f32))
@ -816,51 +890,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>> { 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)); let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE));
@ -1334,3 +1363,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);
}
}