feat: difficulty scaling by depth + new items (bow, shield, mana potion)

Difficulty scaling:
- Spawn rates increase with depth (goblins every 30-10 ticks, slimes every 45-15)
- Enemy count caps increase with depth (goblins 3-8, slimes 2-5)
- Enemy health scales with depth (+5 hp/goblin, +3 hp/slime per depth)
- Goblin strength stat scales with depth

New items:
- Bow: ranged weapon, +4 damage bonus
- Shield: armor, +3 armor bonus
- Mana Potion: consumable, heals 20 hp
- Items spawn in world gen: sword, bow, shield, health potion, mana potion, leather armor

All 171 tests pass.
This commit is contained in:
Emil
2026-06-21 16:34:57 +03:00
parent 9778154348
commit 876eb031df
2 changed files with 44 additions and 8 deletions
+27 -4
View File
@@ -2,11 +2,14 @@
pub enum ItemType { pub enum ItemType {
Dagger, Dagger,
Sword, Sword,
Bow,
LeatherArmor, LeatherArmor,
PlateArmor, PlateArmor,
HealthPotion, HealthPotion,
ManaPotion,
Food, Food,
Scroll, Scroll,
Shield,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -25,9 +28,12 @@ impl Item {
match self.typ { match self.typ {
ItemType::Dagger => "Dagger", ItemType::Dagger => "Dagger",
ItemType::Sword => "Sword", ItemType::Sword => "Sword",
ItemType::Bow => "Bow",
ItemType::LeatherArmor => "Leather Armor", ItemType::LeatherArmor => "Leather Armor",
ItemType::PlateArmor => "Plate Armor", ItemType::PlateArmor => "Plate Armor",
ItemType::Shield => "Shield",
ItemType::HealthPotion => "Health Potion", ItemType::HealthPotion => "Health Potion",
ItemType::ManaPotion => "Mana Potion",
ItemType::Food => "Food", ItemType::Food => "Food",
ItemType::Scroll => "Scroll", ItemType::Scroll => "Scroll",
} }
@@ -37,9 +43,12 @@ impl Item {
match self.typ { match self.typ {
ItemType::Dagger => '/', ItemType::Dagger => '/',
ItemType::Sword => '|', ItemType::Sword => '|',
ItemType::Bow => ')',
ItemType::LeatherArmor => '[', ItemType::LeatherArmor => '[',
ItemType::PlateArmor => '{', ItemType::PlateArmor => '{',
ItemType::Shield => 'O',
ItemType::HealthPotion => '!', ItemType::HealthPotion => '!',
ItemType::ManaPotion => '!',
ItemType::Food => '%', ItemType::Food => '%',
ItemType::Scroll => '?', ItemType::Scroll => '?',
} }
@@ -49,9 +58,12 @@ impl Item {
match self.typ { match self.typ {
ItemType::Dagger => [200, 200, 200], ItemType::Dagger => [200, 200, 200],
ItemType::Sword => [240, 240, 240], ItemType::Sword => [240, 240, 240],
ItemType::Bow => [180, 140, 80],
ItemType::LeatherArmor => [140, 90, 50], ItemType::LeatherArmor => [140, 90, 50],
ItemType::PlateArmor => [180, 180, 190], ItemType::PlateArmor => [180, 180, 190],
ItemType::Shield => [160, 140, 60],
ItemType::HealthPotion => [255, 40, 40], ItemType::HealthPotion => [255, 40, 40],
ItemType::ManaPotion => [60, 60, 255],
ItemType::Food => [80, 200, 60], ItemType::Food => [80, 200, 60],
ItemType::Scroll => [255, 220, 120], ItemType::Scroll => [255, 220, 120],
} }
@@ -60,22 +72,30 @@ impl Item {
pub fn is_equipment(&self) -> bool { pub fn is_equipment(&self) -> bool {
matches!( matches!(
self.typ, self.typ,
ItemType::Dagger | ItemType::Sword | ItemType::LeatherArmor | ItemType::PlateArmor ItemType::Dagger
| ItemType::Sword
| ItemType::Bow
| ItemType::LeatherArmor
| ItemType::PlateArmor
| ItemType::Shield
) )
} }
pub fn is_weapon(&self) -> bool { pub fn is_weapon(&self) -> bool {
matches!(self.typ, ItemType::Dagger | ItemType::Sword) matches!(self.typ, ItemType::Dagger | ItemType::Sword | ItemType::Bow)
} }
pub fn is_armor(&self) -> bool { pub fn is_armor(&self) -> bool {
matches!(self.typ, ItemType::LeatherArmor | ItemType::PlateArmor) matches!(
self.typ,
ItemType::LeatherArmor | ItemType::PlateArmor | ItemType::Shield
)
} }
pub fn is_consumable(&self) -> bool { pub fn is_consumable(&self) -> bool {
matches!( matches!(
self.typ, self.typ,
ItemType::HealthPotion | ItemType::Food | ItemType::Scroll ItemType::HealthPotion | ItemType::ManaPotion | ItemType::Food | ItemType::Scroll
) )
} }
@@ -83,6 +103,7 @@ impl Item {
match self.typ { match self.typ {
ItemType::Dagger => 3.0, ItemType::Dagger => 3.0,
ItemType::Sword => 6.0, ItemType::Sword => 6.0,
ItemType::Bow => 4.0,
_ => 0.0, _ => 0.0,
} }
} }
@@ -91,6 +112,7 @@ impl Item {
match self.typ { match self.typ {
ItemType::LeatherArmor => 2.0, ItemType::LeatherArmor => 2.0,
ItemType::PlateArmor => 5.0, ItemType::PlateArmor => 5.0,
ItemType::Shield => 3.0,
_ => 0.0, _ => 0.0,
} }
} }
@@ -98,6 +120,7 @@ impl Item {
pub fn heal_amount(&self) -> f32 { pub fn heal_amount(&self) -> f32 {
match self.typ { match self.typ {
ItemType::HealthPotion => 40.0, ItemType::HealthPotion => 40.0,
ItemType::ManaPotion => 20.0,
ItemType::Food => 15.0, ItemType::Food => 15.0,
_ => 0.0, _ => 0.0,
} }
+17 -4
View File
@@ -245,6 +245,12 @@ impl Game {
.spawn(ItemType::HealthPotion, px as i32 + 6, py as i32 + 1); .spawn(ItemType::HealthPotion, px as i32 + 6, py as i32 + 1);
self.items self.items
.spawn(ItemType::LeatherArmor, px as i32 - 3, py as i32 - 8); .spawn(ItemType::LeatherArmor, px as i32 - 3, py as i32 - 8);
self.items
.spawn(ItemType::Bow, px as i32 + 10, py as i32 + 1);
self.items
.spawn(ItemType::Shield, px as i32 - 10, py as i32 + 1);
self.items
.spawn(ItemType::ManaPotion, px as i32 + 3, py as i32 - 6);
} }
pub fn center_camera_on(&mut self, px: f32, py: f32) { pub fn center_camera_on(&mut self, px: f32, py: f32) {
@@ -516,10 +522,10 @@ impl Game {
self.update_score(); self.update_score();
self.update_item_pickup(); self.update_item_pickup();
if self.tick % 30 == 0 { if self.tick % (30u64 - (self.depth as u64 * 3).min(20)) == 0 {
self.try_spawn_goblin(); self.try_spawn_goblin();
} }
if self.tick % 45 == 0 { if self.tick % (45u64 - (self.depth as u64 * 4).min(30)) == 0 {
self.try_spawn_slime(); self.try_spawn_slime();
} }
@@ -1364,13 +1370,14 @@ impl Game {
} }
fn try_spawn_goblin(&mut self) { fn try_spawn_goblin(&mut self) {
let max_goblins = 3usize + self.depth.min(5) as usize;
let alive_goblins = self let alive_goblins = self
.entities .entities
.all() .all()
.iter() .iter()
.filter(|e| e.alive && e.kind == EntityKind::Goblin) .filter(|e| e.alive && e.kind == EntityKind::Goblin)
.count(); .count();
if alive_goblins >= 3 { if alive_goblins >= max_goblins {
return; return;
} }
@@ -1397,17 +1404,21 @@ impl Game {
let id = self.entities.spawn(EntityKind::Goblin); let id = self.entities.spawn(EntityKind::Goblin);
if let Some(g) = self.entities.get_mut(id) { if let Some(g) = self.entities.get_mut(id) {
g.build_humanoid(spawn_x as f32, spawn_y as f32); g.build_humanoid(spawn_x as f32, spawn_y as f32);
g.health += self.depth as f32 * 5.0;
g.max_health += self.depth as f32 * 5.0;
g.strength += self.depth;
} }
} }
fn try_spawn_slime(&mut self) { fn try_spawn_slime(&mut self) {
let max_slimes = 2usize + self.depth.min(3) as usize;
let alive_slimes = self let alive_slimes = self
.entities .entities
.all() .all()
.iter() .iter()
.filter(|e| e.alive && e.kind == EntityKind::Slime) .filter(|e| e.alive && e.kind == EntityKind::Slime)
.count(); .count();
if alive_slimes >= 2 { if alive_slimes >= max_slimes {
return; return;
} }
@@ -1434,6 +1445,8 @@ impl Game {
let id = self.entities.spawn(EntityKind::Slime); let id = self.entities.spawn(EntityKind::Slime);
if let Some(s) = self.entities.get_mut(id) { if let Some(s) = self.entities.get_mut(id) {
s.build_humanoid(spawn_x as f32, spawn_y as f32); s.build_humanoid(spawn_x as f32, spawn_y as f32);
s.health += self.depth as f32 * 3.0;
s.max_health += self.depth as f32 * 3.0;
} }
} }
} }