Free game against rival dungeon masters (green, blue) and/or the humans
- One opponent on maps up to 128x128, two on bigger maps; with a chance of one in two one of them is the humans' gate instead of a rival master - Simplified rival masters (rival.rs): own soul core and dungeon (lair, treasury to loot) that grows ring by ring, creatures appear and get stronger over time, guard the dungeon and attack in groups - the player, the other master or the humans' gate; a fallen core puts the master out and frees its ground - The humans attack everybody: waves now and then march on a rival master - Sides for units: all other sides are enemies; towers and contested ground follow them; the hand cannot pick up rival creatures; the flag next to a rival core attacks it - Difficulty (easy / medium / hard) in the New Game dialog - Colours: green and blue floors, cores, unit rings, minimap, HUD bars of the rival cores - Victory in free games: every rival core and the gate (if any) destroyed - Tests for setups, growth and attacks, elimination and victory, humans against rivals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52c4d1caf8
commit
dd16c684e5
11 files changed
+771
-38
No files matched your search
+491
@@ -0,0 +1,491 @@
|
||||
// Rival dungeon masters (simplified): a dungeon around their own soul core that grows over
|
||||
// time, creatures that appear there, guard it and attack in groups - the player, the other
|
||||
// master or the humans. A master whose core falls is out; its ground becomes free.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const RIVAL_CORE_HP: f32 = 2000.0;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Rival {
|
||||
pub side: u8, // 2 (green) or 3 (blue)
|
||||
pub core: (i32, i32), // top-left corner of the 3x3 core
|
||||
pub hp: f32,
|
||||
pub alive: bool,
|
||||
pub radius: i32, // size of the dungeon around the core
|
||||
pub grow_t: f32,
|
||||
pub spawn_t: f32,
|
||||
pub attack_t: f32,
|
||||
pub difficulty: u8, // 0 easy, 1 medium, 2 hard
|
||||
pub power: f32, // grows with the time: level of the new creatures
|
||||
}
|
||||
|
||||
struct Pace {
|
||||
spawn: f32, // seconds between new creatures
|
||||
grow: f32, // seconds between growth steps
|
||||
attack: f32, // seconds between attacks
|
||||
cap: usize, // most creatures at a time
|
||||
max_radius: i32, // largest dungeon
|
||||
power: f32, // levels per minute
|
||||
}
|
||||
|
||||
fn pace(d: u8) -> Pace {
|
||||
match d {
|
||||
0 => Pace { spawn: 55.0, grow: 45.0, attack: 330.0, cap: 6, max_radius: 8, power: 0.25 },
|
||||
2 => Pace { spawn: 26.0, grow: 22.0, attack: 170.0, cap: 13, max_radius: 13, power: 0.6 },
|
||||
_ => Pace { spawn: 38.0, grow: 30.0, attack: 240.0, cap: 9, max_radius: 11, power: 0.4 },
|
||||
}
|
||||
}
|
||||
|
||||
impl Rival {
|
||||
pub fn centre(&self) -> (f32, f32) {
|
||||
(self.core.0 as f32 + 1.5, self.core.1 as f32 + 1.5)
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// Free game: one opponent per map up to 128 x 128 tiles, two on bigger maps; with a chance of
|
||||
// one in two one of them is the humans' gate instead of a rival master
|
||||
pub fn new_free(seed: u32, w: usize, h: usize, difficulty: u8) -> Game {
|
||||
let opponents = if w * h > 128 * 128 { 2 } else { 1 };
|
||||
let mut rng = Rng((seed ^ 0x9e37_79b9) | 1);
|
||||
for _ in 0..5 {
|
||||
rng.next();
|
||||
}
|
||||
let humans = rng.f() < 0.5;
|
||||
let rivals = if humans { opponents - 1 } else { opponents };
|
||||
let opts = GenOpts { gate: humans, ..GenOpts::default() };
|
||||
let map = Map::generate_with(seed, w, h, &opts);
|
||||
let mut g = Game::from_map(map, seed);
|
||||
g.gate_on = humans;
|
||||
if humans {
|
||||
let (cx, cy) = g.map.core;
|
||||
let (gx, gy) = g.map.gate;
|
||||
let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32;
|
||||
g.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0);
|
||||
} else {
|
||||
g.gate_hp = 0.0;
|
||||
}
|
||||
g.guard_treasures();
|
||||
// the rival dungeons on the far side, away from the gate
|
||||
let (wi, hi) = (g.map.w as i32, g.map.h as i32);
|
||||
let x = wi - 14;
|
||||
let spots: Vec<(i32, i32)> = match (rivals, humans) {
|
||||
(1, false) => vec![(x, hi / 2)],
|
||||
(1, true) => vec![(x, if g.map.gate.1 < hi / 2 { hi * 3 / 4 } else { hi / 4 })],
|
||||
(2, _) => vec![(x, hi / 4), (x, hi * 3 / 4)],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (k, &(rx, ry)) in spots.iter().enumerate() {
|
||||
g.add_rival(2 + k as u8, rx, ry.clamp(8, hi - 9), difficulty);
|
||||
}
|
||||
g.place_all_torches();
|
||||
g.msg("msg.welcome1", vec![]);
|
||||
g.msg("msg.welcome2", vec![]);
|
||||
g.msg(if rivals == 0 { "msg.welcome_humans" } else if humans { "msg.welcome_both" } else { "msg.welcome_rivals" }, vec![rivals.to_string()]);
|
||||
g
|
||||
}
|
||||
|
||||
pub fn rival_alive(&self, side: u8) -> bool {
|
||||
self.rivals.iter().any(|r| r.side == side && r.alive)
|
||||
}
|
||||
|
||||
// sets up a rival dungeon around (cx, cy)
|
||||
pub fn add_rival(&mut self, side: u8, cx: i32, cy: i32, difficulty: u8) {
|
||||
let owner = Owner::of_side(side);
|
||||
let core = (cx - 1, cy - 1);
|
||||
let r = 4;
|
||||
for y in cy - r..=cy + r {
|
||||
for x in cx - r..=cx + r {
|
||||
if !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
continue;
|
||||
}
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
if matches!(t.kind, Kind::Portal | Kind::Gate | Kind::Core) {
|
||||
continue; // portals and other cores stay
|
||||
}
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = owner;
|
||||
t.gold = 0;
|
||||
t.marked = false;
|
||||
t.reinforced = false;
|
||||
}
|
||||
}
|
||||
for y in 0..3 {
|
||||
for x in 0..3 {
|
||||
let t = &mut self.map.t[idx(core.0 + x, core.1 + y)];
|
||||
t.kind = Kind::Core;
|
||||
t.owner = owner;
|
||||
}
|
||||
}
|
||||
// a lair and a treasury (the player can take it)
|
||||
for y in cy - 3..=cy + 3 {
|
||||
let t = &mut self.map.t[idx(cx - 3, y)];
|
||||
t.kind = Kind::Lair;
|
||||
}
|
||||
for y in cy - 1..=cy + 1 {
|
||||
let t = &mut self.map.t[idx(cx + 3, y)];
|
||||
t.kind = Kind::Treasury;
|
||||
t.gold = 300;
|
||||
t.res = 0;
|
||||
}
|
||||
self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: r, grow_t: 20.0, spawn_t: 5.0, attack_t: pace(difficulty).attack, difficulty, power: 0.0 });
|
||||
for k in 0..3 {
|
||||
self.spawn_rival_unit(side, EK::Kobold, 1, k as f32);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_rival_unit(&mut self, side: u8, kind: EK, level: u32, k: f32) {
|
||||
let Some(rv) = self.rivals.iter().find(|r| r.side == side) else { return };
|
||||
let (cx, cy) = rv.centre();
|
||||
let a = k * 1.7 + self.rng.f() * 2.0;
|
||||
let (x, y) = (cx + a.cos() * 2.4, cy + a.sin() * 2.4);
|
||||
let id = self.spawn(kind, false, x, y, level);
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.side = side;
|
||||
e.home = Some((cx, cy));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_rivals(&mut self, dt: f32) {
|
||||
for k in 0..self.rivals.len() {
|
||||
if !self.rivals[k].alive {
|
||||
continue;
|
||||
}
|
||||
let p = pace(self.rivals[k].difficulty);
|
||||
let side = self.rivals[k].side;
|
||||
{
|
||||
let r = &mut self.rivals[k];
|
||||
r.power += dt / 60.0 * p.power;
|
||||
r.grow_t -= dt;
|
||||
r.spawn_t -= dt;
|
||||
r.attack_t -= dt;
|
||||
}
|
||||
// growth: one more ring of ground (no imps needed)
|
||||
if self.rivals[k].grow_t <= 0.0 {
|
||||
self.rivals[k].grow_t = p.grow * (0.8 + self.rng.f() * 0.4);
|
||||
if self.rivals[k].radius < p.max_radius {
|
||||
self.rivals[k].radius += 1;
|
||||
self.rival_grow(k);
|
||||
}
|
||||
}
|
||||
// new creatures
|
||||
let units = self.ents.iter().filter(|e| e.alive && e.side == side && !e.hero).count();
|
||||
if self.rivals[k].spawn_t <= 0.0 {
|
||||
self.rivals[k].spawn_t = p.spawn * (0.8 + self.rng.f() * 0.4);
|
||||
if units < p.cap {
|
||||
let power = self.rivals[k].power;
|
||||
let mut pool = vec![EK::Kobold, EK::Spider];
|
||||
if power >= 1.5 {
|
||||
pool.extend([EK::Mage, EK::Ogre]);
|
||||
}
|
||||
if power >= 3.0 {
|
||||
pool.push(EK::Gargoyle);
|
||||
}
|
||||
let kind = pool[(self.rng.next() as usize) % pool.len()];
|
||||
let level = (1.0 + power) as u32;
|
||||
self.spawn_rival_unit(side, kind, level.clamp(1, 10), units as f32);
|
||||
}
|
||||
}
|
||||
// attack: about two thirds of the creatures march on an enemy
|
||||
if self.rivals[k].attack_t <= 0.0 {
|
||||
self.rivals[k].attack_t = p.attack * (0.8 + self.rng.f() * 0.4);
|
||||
let mut targets: Vec<(u8, u32)> = vec![(0, 3)];
|
||||
for o in &self.rivals {
|
||||
if o.alive && o.side != side {
|
||||
targets.push((o.side, 1));
|
||||
}
|
||||
}
|
||||
if self.has_gate() && self.gate_hp > 0.0 {
|
||||
targets.push((1, 1));
|
||||
}
|
||||
let total: u32 = targets.iter().map(|t| t.1).sum();
|
||||
let mut r = self.rng.next() % total;
|
||||
let mut target = 0;
|
||||
for (s, w) in targets {
|
||||
if r < w {
|
||||
target = s;
|
||||
break;
|
||||
}
|
||||
r -= w;
|
||||
}
|
||||
let mut n = 0;
|
||||
for e in self.ents.iter_mut() {
|
||||
if e.alive && !e.hero && e.side == side && e.target.is_none() && n * 3 < units * 2 {
|
||||
e.target = Some(target);
|
||||
e.path.clear();
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if n > 0 && target == 0 {
|
||||
self.msg(if side == 2 { "msg.rival1_attack" } else { "msg.rival2_attack" }, vec![n.to_string()]);
|
||||
self.sfx_ui(Sfx::WaveAlarm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a ring of earth around the rival dungeon becomes its ground
|
||||
fn rival_grow(&mut self, k: usize) {
|
||||
let rv = self.rivals[k].clone();
|
||||
let owner = Owner::of_side(rv.side);
|
||||
let (cx, cy) = (rv.core.0 + 1, rv.core.1 + 1);
|
||||
let r = rv.radius;
|
||||
for y in cy - r..=cy + r {
|
||||
for x in cx - r..=cx + r {
|
||||
if (x - cx).abs() != r && (y - cy).abs() != r {
|
||||
continue;
|
||||
}
|
||||
if !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
continue;
|
||||
}
|
||||
let t = self.map.t[idx(x, y)];
|
||||
let free = matches!(t.kind, Kind::Earth | Kind::Floor) && !t.reinforced && t.owner == Owner::None;
|
||||
let touches = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == owner && n.walkable()));
|
||||
if free && touches {
|
||||
let tl = &mut self.map.t[idx(x, y)];
|
||||
tl.kind = Kind::Floor;
|
||||
tl.owner = owner;
|
||||
tl.gold = 0;
|
||||
tl.marked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// damage to the core of a rival master
|
||||
pub fn rival_core_hit(&mut self, side: u8, dmg: f32) {
|
||||
let Some(k) = self.rivals.iter().position(|r| r.side == side && r.alive) else { return };
|
||||
self.rivals[k].hp -= dmg;
|
||||
let (x, y) = self.rivals[k].centre();
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.3, 0xff4040ff);
|
||||
self.sfx(Sfx::CoreHit, x, y);
|
||||
if self.rivals[k].hp <= 0.0 {
|
||||
self.eliminate_rival(k);
|
||||
}
|
||||
}
|
||||
|
||||
fn eliminate_rival(&mut self, k: usize) {
|
||||
let side = self.rivals[k].side;
|
||||
let owner = Owner::of_side(side);
|
||||
self.rivals[k].alive = false;
|
||||
self.rivals[k].hp = 0.0;
|
||||
let (x, y) = self.rivals[k].centre();
|
||||
self.fx(1, x, y, 0.0, 0.0, 1.0, 0xffffffff);
|
||||
self.sfx(Sfx::Victory, x, y);
|
||||
self.msg(if side == 2 { "msg.rival1_out" } else { "msg.rival2_out" }, vec![]);
|
||||
// its creatures scatter and vanish, its ground is free
|
||||
for e in self.ents.iter_mut() {
|
||||
if e.alive && !e.hero && e.side == side {
|
||||
e.alive = false;
|
||||
}
|
||||
}
|
||||
for t in self.map.t.iter_mut() {
|
||||
if t.owner == owner {
|
||||
if t.kind == Kind::Core {
|
||||
t.kind = Kind::Floor;
|
||||
}
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// where an attacker of `side`'s enemy is heading: the core (or gate) of the target side
|
||||
fn target_core_adjacent(&self, i: usize, target: u8) -> bool {
|
||||
match target {
|
||||
1 => {
|
||||
let (x, y) = xy(i);
|
||||
DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |t| t.kind == Kind::Gate))
|
||||
}
|
||||
s => self.map.adj_core_of(i, Owner::of_side(s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn target_alive(&self, target: u8) -> bool {
|
||||
match target {
|
||||
0 => self.core_hp > 0.0,
|
||||
1 => self.has_gate() && self.gate_hp > 0.0,
|
||||
s => self.rival_alive(s),
|
||||
}
|
||||
}
|
||||
|
||||
// hit the core (or gate) of the target side
|
||||
pub fn hit_target_core(&mut self, e: &mut Ent, target: u8) {
|
||||
let dmg = e.atk();
|
||||
e.cd = st(e.kind).interval;
|
||||
match target {
|
||||
0 => {
|
||||
self.core_hp -= dmg;
|
||||
let (cx, cy) = self.map.core;
|
||||
self.fx(2, (e.x + cx as f32 + 1.5) * 0.5, (e.y + cy as f32 + 1.5) * 0.5, 0.0, 0.0, 0.3, 0xff4040ff);
|
||||
self.sfx(Sfx::CoreHit, e.x, e.y);
|
||||
self.hint(6, "hint.core_attacked");
|
||||
}
|
||||
1 => {
|
||||
self.gate_hp -= dmg;
|
||||
self.gate_hit_t = self.time;
|
||||
let (gx, gy) = self.gate_center();
|
||||
self.sfx(Sfx::GateHit, gx, gy);
|
||||
}
|
||||
s => self.rival_core_hit(s, dmg),
|
||||
}
|
||||
}
|
||||
|
||||
// the march of an attacker on a core; false = no target (any more)
|
||||
pub fn march_on(&mut self, e: &mut Ent, dt: f32) -> bool {
|
||||
let Some(target) = e.target else { return false };
|
||||
if !self.target_alive(target) || target == e.side() {
|
||||
e.target = None;
|
||||
return false;
|
||||
}
|
||||
let cur = e.tile();
|
||||
if e.path.is_empty() && self.target_core_adjacent(cur, target) {
|
||||
e.working = true;
|
||||
if e.cd <= 0.0 {
|
||||
self.hit_target_core(e, target);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 1.5;
|
||||
// attackers dig their way like heroes; the player's creatures only walk
|
||||
let goal = |i: usize| self.target_core_adjacent(i, target);
|
||||
let found = if e.side() == 0 { self.map.search(cur, self.map.walk_cost(), goal, true) } else { self.map.search(cur, self.map.hero_cost(), goal, true) };
|
||||
match found {
|
||||
Some(p) => e.path = p,
|
||||
None => {
|
||||
e.target = None;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.step_path(e, dt);
|
||||
true
|
||||
}
|
||||
|
||||
// units of a rival master: fight enemies nearby, march when sent, otherwise guard home
|
||||
pub fn rival_ai(&mut self, e: &mut Ent, dt: f32) {
|
||||
if e.think <= 0.0 {
|
||||
e.think = 0.5;
|
||||
if !matches!(e.job, Job::Attack(_)) {
|
||||
let radius = if e.target.is_some() { 6.0 } else { 8.0 };
|
||||
if let Some(id) = self.nearest_enemy(e, radius) {
|
||||
e.job = Job::Attack(id);
|
||||
e.path.clear();
|
||||
e.repath = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Job::Attack(id) = e.job {
|
||||
let (hx, hy) = e.home.unwrap_or((e.x, e.y));
|
||||
let far = e.target.is_none() && (e.x - hx).powi(2) + (e.y - hy).powi(2) > 15.0 * 15.0;
|
||||
if !far && self.attack_job(e, id, dt) {
|
||||
return;
|
||||
}
|
||||
e.job = Job::None;
|
||||
e.path.clear();
|
||||
}
|
||||
if self.march_on(e, dt) {
|
||||
return;
|
||||
}
|
||||
// guard the own dungeon
|
||||
let (hx, hy) = e.home.unwrap_or((e.x, e.y));
|
||||
let d2 = (e.x - hx).powi(2) + (e.y - hy).powi(2);
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 3.0 + self.rng.f() * 3.0;
|
||||
let (tx, ty) = if d2 > 36.0 { (hx, hy) } else { (hx + self.rng.f() * 8.0 - 4.0, hy + self.rng.f() * 8.0 - 4.0) };
|
||||
if self.map.walk(tx as i32, ty as i32) {
|
||||
if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) {
|
||||
e.path = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.step_path(e, dt);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn free_games_have_one_or_two_opponents() {
|
||||
for seed in 1..=12u32 {
|
||||
for &(w, h) in &[(64usize, 64usize), (160, 160)] {
|
||||
let g = Game::new_free(seed, w, h, 1);
|
||||
let opponents = g.rivals.len() + g.gate_on as usize;
|
||||
assert_eq!(opponents, if w * h > 128 * 128 { 2 } else { 1 }, "seed {} {}x{}", seed, w, h);
|
||||
for r in &g.rivals {
|
||||
let (cx, cy) = (r.core.0 + 1, r.core.1 + 1);
|
||||
assert_eq!(g.map.t[idx(cx, cy)].kind, Kind::Core);
|
||||
assert_eq!(g.map.t[idx(cx, cy)].owner, Owner::of_side(r.side));
|
||||
}
|
||||
// the player's core is untouched
|
||||
let (px, py) = g.map.core;
|
||||
assert_eq!(g.map.t[idx(px + 1, py + 1)].owner, Owner::Player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a free game with exactly one rival master and no gate
|
||||
fn rival_game(difficulty: u8) -> Game {
|
||||
(1..200u32).map(|s| Game::new_free(s, 64, 64, difficulty)).find(|g| g.rivals.len() == 1 && !g.gate_on).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rival_grows_spawns_and_attacks() {
|
||||
let mut g = rival_game(2);
|
||||
let r0 = g.rivals[0].radius;
|
||||
let core0 = g.core_hp;
|
||||
let mut attacked = false;
|
||||
for _ in 0..(12 * 60 * 20) {
|
||||
g.update(0.05);
|
||||
attacked |= g.ents.iter().any(|e| e.rival() && e.target == Some(0));
|
||||
if g.core_hp < core0 - 50.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(g.rivals[0].radius > r0, "the rival dungeon grows");
|
||||
assert!(attacked, "the rival sends creatures");
|
||||
assert!(g.core_hp < core0, "they reach the player's core");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beating_the_rival_wins_the_free_game() {
|
||||
let mut g = rival_game(1);
|
||||
let side = g.rivals[0].side;
|
||||
g.update(0.05);
|
||||
assert!(g.ents.iter().any(|e| e.side == side && e.alive));
|
||||
g.rival_core_hit(side, RIVAL_CORE_HP + 1.0);
|
||||
assert!(!g.rivals[0].alive);
|
||||
assert!(!g.ents.iter().any(|e| e.side == side && e.alive));
|
||||
assert!(!g.map.t.iter().any(|t| t.owner == Owner::of_side(side)));
|
||||
g.update(0.05);
|
||||
assert_eq!(g.state, 1, "all opponents down: victory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn humans_attack_rival_masters_too() {
|
||||
let mut g = (1..200u32).map(|s| Game::new_free(s, 160, 160, 1)).find(|g| g.rivals.len() == 1 && g.gate_on).unwrap();
|
||||
let side = g.rivals[0].side;
|
||||
let (rx, ry) = g.rivals[0].centre();
|
||||
for k in 0..4 {
|
||||
let id = g.spawn(EK::Knight, true, rx + 6.0 + k as f32 * 0.3, ry, 4);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.target = Some(side);
|
||||
}
|
||||
}
|
||||
let hp0 = g.rivals[0].hp;
|
||||
let mut fought = false;
|
||||
for _ in 0..(120 * 20) {
|
||||
g.update(0.05);
|
||||
fought |= g.rivals[0].hp < hp0 || g.ents.iter().any(|e| e.rival() && e.hp < e.max_hp);
|
||||
if fought {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(fought, "heroes fight the rival master");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user