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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 14:11:23 +01:00
1 parent 52c4d1caf8
commit dd16c684e5
11 files changed
+771 -38

No files matched your search

+43 -2
View File
@@ -489,7 +489,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall
for z in cz * CHUNK..((cz + 1) * CHUNK).min(mh() as i32) {
for x in cx * CHUNK..((cx + 1) * CHUNK).min(mw() as i32) {
let t = map.t[idx(x, z)];
let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var, t.reinforced);
let tex = Gfx::tile_tex(t.kind, t.owner, t.var, t.reinforced);
let (ua, ub) = cell_uv(tex);
let (xf, zf) = (x as f32, z as f32);
if let Some(river) = t.river() {
@@ -901,6 +901,11 @@ impl R3d {
let (gx, gz) = g.gate_center();
push(vec3(gx, 1.2, gz), 7.0, vec3(0.75, 0.85, 1.0), 1.0, 50.0);
}
for rv in g.rivals.iter().filter(|r| r.alive) {
let (rx, rz) = rv.centre();
let c = if rv.side == 2 { vec3(0.3, 1.0, 0.4) } else { vec3(0.35, 0.55, 1.0) };
push(vec3(rx, 1.4, rz), 7.0, c, 1.1, 50.0);
}
// glowing lava near the camera (every third tile)
let r = (cam.zoom * 0.9) as i32;
let (ccx, ccz) = (cam.tx as i32, cam.tz as i32);
@@ -1427,6 +1432,16 @@ fn draw_landmarks_solid(g: &Game, t: f32) {
// claws reaching for the orb
line3(b + vec3(0.0, 1.0, 0.0), c + vec3(dx * 0.45, 1.55 - (k % 2) as f32 * 0.1, dz * 0.45), 0.07, Color::from_rgba(36, 24, 30, 255));
}
// rival masters' cores: obsidian pedestal (the orb glows in the glow pass)
for rv in g.rivals.iter().filter(|r| r.alive) {
let (rx, rz) = rv.centre();
let c = vec3(rx, 0.0, rz);
draw_cube(c + vec3(0.0, 0.08, 0.0), vec3(2.6, 0.16, 2.6), None, Color::from_rgba(24, 22, 30, 255));
draw_cube(c + vec3(0.0, 0.22, 0.0), vec3(1.8, 0.14, 1.8), None, Color::from_rgba(34, 32, 42, 255));
for (dx, dz) in [(-1.1f32, -1.1f32), (1.1, -1.1), (1.1, 1.1), (-1.1, 1.1)] {
draw_cube(c + vec3(dx, 0.5, dz), vec3(0.22, 1.0, 0.22), None, Color::from_rgba(30, 28, 38, 255));
}
}
// portal circles: eight standing stones of different height
for &(px, py) in &g.map.portals {
let pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5);
@@ -1506,6 +1521,20 @@ fn draw_landmarks_glow(g: &Game, t: f32) {
let p = c + vec3(a.cos() * 1.05, (t * 1.1 + k as f32).sin() * 0.25, a.sin() * 1.05);
draw_cube(p, vec3(0.1, 0.16, 0.04), None, Color::new(1.0, 0.7, 0.3, 0.85));
}
// rival masters' cores in their colours
for rv in g.rivals.iter().filter(|r| r.alive) {
let (rx, rz) = rv.centre();
let hp = (rv.hp / crate::rival::RIVAL_CORE_HP).clamp(0.0, 1.0);
let col = if rv.side == 2 { vec3(0.3, 0.95, 0.4) } else { vec3(0.35, 0.55, 1.0) };
let pulse = 1.0 + (t * (2.0 + (1.0 - hp) * 6.0) + rv.side as f32).sin() * 0.06;
let c = vec3(rx, 1.3 + (t * 1.2).sin() * 0.05, rz);
draw_sphere(c, 0.55 * pulse, None, Color::new(col.x * (0.4 + 0.6 * hp), col.y * (0.4 + 0.6 * hp), col.z * (0.4 + 0.6 * hp), 1.0));
draw_sphere(c, 0.85 * pulse, None, Color::new(col.x, col.y, col.z, 0.22));
for k in 0..4 {
let a = t * 0.8 + k as f32 * 1.57;
draw_cube(c + vec3(a.cos() * 0.9, (t + k as f32).sin() * 0.2, a.sin() * 0.9), vec3(0.08, 0.14, 0.04), None, Color::new(col.x, col.y, col.z, 0.8));
}
}
// portals: a circle of standing stones with runes, the vortex, a column of light, sparks
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
let owned = g.map.portal_owned(pk);
@@ -1566,6 +1595,12 @@ fn draw_shadows(g: &Game) {
disc(vec3(e.x, 0.012, e.y), r, Color::new(0.0, 0.0, 0.0, 0.4), 12);
let rc = if e.captive != 0 {
Color::new(0.6, 0.6, 0.6, 0.6)
} else if e.rival() {
if e.side == 2 {
Color::new(0.35, 0.95, 0.4, 0.85)
} else {
Color::new(0.4, 0.6, 1.0, 0.85)
}
} else if e.guard {
Color::new(1.0, 0.85, 0.3, 0.9)
} else if e.hero {
@@ -1744,6 +1779,8 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Floor => match tile.owner {
Owner::Player => [140, 60, 80, 255],
Owner::Hero => [170, 175, 190, 255],
Owner::Rival1 => [70, 150, 80, 255],
Owner::Rival2 => [70, 100, 180, 255],
Owner::None => [70, 64, 60, 255],
},
Kind::Treasury => [200, 160, 60, 255],
@@ -1751,7 +1788,11 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Farm => [70, 140, 60, 255],
Kind::Training => [160, 100, 60, 255],
Kind::Library => [150, 40, 60, 255],
Kind::Core => [255, 60, 60, 255],
Kind::Core => match tile.owner {
Owner::Rival1 => [90, 240, 110, 255],
Owner::Rival2 => [100, 150, 255, 255],
_ => [255, 60, 60, 255],
},
Kind::Portal => [190, 90, 240, 255],
Kind::Gate => [230, 230, 255, 255],
Kind::Torture => [120, 40, 40, 255],