Campaign mission system: 20 mission definitions, goals, unlocks, carry-over
- campaign.rs: missions as key data (size, rivers, fortresses, goals, waves, time limit, gifts); start_mission builds the map with the generator; tools unlock step by step; the strongest 5 creatures and 20% of the gold are carried into the next mission; campaign progress type - Map generator options (portals, treasure caves, hero gate) and meandering rivers with knight bridges - Mission goals: shards, gate of the order, all knights, vault loot, knight towers, waves; time limits; waves and gate defenders only where the mission wants them - Locked tools are shown dark and cannot be chosen; HUD shows gate/waves/time only when used - Fix: patrol routes no longer start on tower spots - Tests: every mission builds, its goals are possible, wave heroes reach the core Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e15b16cc60
commit
46755d68ac
8 files changed
+758
-85
No files matched your search
+110
-15
@@ -59,6 +59,31 @@ pub enum Kind {
|
||||
Diamond, // diamond vein: very rare, very valuable
|
||||
}
|
||||
|
||||
// options for the map generator (campaign missions)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenOpts {
|
||||
pub portals: Option<usize>, // None: one per 64 x 64 tiles
|
||||
pub treasures: Option<usize>, // guarded treasure caves; None: portals - 1
|
||||
pub gate: bool, // hero gate at the far side
|
||||
pub core_y: Option<i32>, // row of the core (None: about the middle)
|
||||
}
|
||||
|
||||
impl Default for GenOpts {
|
||||
fn default() -> GenOpts {
|
||||
GenOpts { portals: None, treasures: None, gate: true, core_y: None }
|
||||
}
|
||||
}
|
||||
|
||||
// An underground river across the map
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct River {
|
||||
pub lava: bool,
|
||||
pub vertical: bool, // runs from top to bottom (else from left to right)
|
||||
pub pos: f32, // position across the map (0..1)
|
||||
pub width: i32,
|
||||
pub bridges: u32, // fixed bridges of the knights
|
||||
}
|
||||
|
||||
// order matters for the packed savegame codec: only append new kinds
|
||||
const KINDS: [Kind; 23] = [
|
||||
Kind::Rock,
|
||||
@@ -274,21 +299,25 @@ impl Map {
|
||||
}
|
||||
|
||||
pub fn generate(seed: u32, w: usize, h: usize) -> Map {
|
||||
Map::generate_with(seed, w, h, &GenOpts::default())
|
||||
}
|
||||
|
||||
pub fn generate_with(seed: u32, w: usize, h: usize, opts: &GenOpts) -> Map {
|
||||
let w = w.clamp(MIN_SIZE, MAX_SIZE);
|
||||
let h = h.clamp(MIN_SIZE, MAX_SIZE);
|
||||
set_dims(w, h);
|
||||
let mut s = seed;
|
||||
loop {
|
||||
let m = Map::try_generate(s, w, h);
|
||||
let m = Map::try_generate(s, w, h, opts);
|
||||
// make sure heroes can reach the core
|
||||
if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
|
||||
if !opts.gate || m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() {
|
||||
return m;
|
||||
}
|
||||
s = s.wrapping_mul(747796405).wrapping_add(2891336453) | 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_generate(seed: u32, w: usize, h: usize) -> Map {
|
||||
fn try_generate(seed: u32, w: usize, h: usize, opts: &GenOpts) -> Map {
|
||||
let (wi, hi) = (w as i32, h as i32);
|
||||
let mut rng = Rng(seed | 1);
|
||||
for _ in 0..10 {
|
||||
@@ -297,8 +326,8 @@ impl Map {
|
||||
// content scales with the area; one portal per 64x64 tiles
|
||||
let area = (w * h) as f32 / 4096.0;
|
||||
let scale = |n: f32| (n * area).round() as i32;
|
||||
let n_portals = (area.round() as usize).max(1);
|
||||
let n_treasures = n_portals - 1;
|
||||
let n_portals = opts.portals.unwrap_or((area.round() as usize).max(1));
|
||||
let n_treasures = opts.treasures.unwrap_or(n_portals.saturating_sub(1));
|
||||
|
||||
let mut t = vec![Tile::new(Kind::Earth); w * h];
|
||||
for (i, tile) in t.iter_mut().enumerate() {
|
||||
@@ -310,7 +339,7 @@ impl Map {
|
||||
}
|
||||
}
|
||||
let inner = |x: i32, y: i32| x >= 2 && y >= 2 && x < wi - 2 && y < hi - 2;
|
||||
let core = (11, hi / 2 + rng.range(-5, 3));
|
||||
let core = (11, opts.core_y.unwrap_or(hi / 2 + rng.range(-5, 3)));
|
||||
let gate = (wi - 13 + rng.range(-2, 2), rng.range(10, hi - 14));
|
||||
|
||||
// places features far apart: best of several random candidates
|
||||
@@ -332,7 +361,7 @@ impl Map {
|
||||
// the first portal is close to the start
|
||||
let first = (core.0 + rng.range(13, 19), (core.1 + if rng.f() < 0.5 { rng.range(-14, -10) } else { rng.range(10, 14) }).clamp(5, hi - 9));
|
||||
taken.push(first);
|
||||
let mut portals = vec![first];
|
||||
let mut portals = if n_portals > 0 { vec![first] } else { Vec::new() };
|
||||
for _ in 1..n_portals {
|
||||
portals.push(place(&mut rng, &mut taken, 6));
|
||||
}
|
||||
@@ -448,15 +477,20 @@ impl Map {
|
||||
}
|
||||
}
|
||||
}
|
||||
// hero gate
|
||||
room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero);
|
||||
for y in 0..3 {
|
||||
for x in 0..3 {
|
||||
let tl = &mut t[(gate.1 + y) as usize * w + (gate.0 + x) as usize];
|
||||
tl.kind = Kind::Gate;
|
||||
tl.owner = Owner::Hero;
|
||||
// hero gate (campaign maps place the gate of the order in a fortress instead)
|
||||
let gate = if opts.gate {
|
||||
room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero);
|
||||
for y in 0..3 {
|
||||
for x in 0..3 {
|
||||
let tl = &mut t[(gate.1 + y) as usize * w + (gate.0 + x) as usize];
|
||||
tl.kind = Kind::Gate;
|
||||
tl.owner = Owner::Hero;
|
||||
}
|
||||
}
|
||||
}
|
||||
gate
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
for tl in t.iter_mut() {
|
||||
if tl.vein().is_some() {
|
||||
tl.hp = 6.0;
|
||||
@@ -465,6 +499,67 @@ impl Map {
|
||||
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures }
|
||||
}
|
||||
|
||||
// Carves a meandering river; the player's area, the core and portals stay dry.
|
||||
// Returns the tiles of the knight bridges.
|
||||
pub fn carve_river(&mut self, r: &River, seed: u32) -> Vec<usize> {
|
||||
let mut rng = Rng(seed | 1);
|
||||
let (w, h) = (self.w as i32, self.h as i32);
|
||||
let len = if r.vertical { h } else { w };
|
||||
let across = if r.vertical { w } else { h };
|
||||
let base = (r.pos * across as f32) as i32;
|
||||
let (f1, f2) = (0.05 + rng.f() * 0.06, 0.13 + rng.f() * 0.08);
|
||||
let (p1, p2) = (rng.f() * 6.0, rng.f() * 6.0);
|
||||
let amp = (across as f32 * 0.06).min(6.0);
|
||||
let mut centre = Vec::with_capacity(len as usize);
|
||||
for s in 0..len {
|
||||
let c = base as f32 + (s as f32 * f1 + p1).sin() * amp + (s as f32 * f2 + p2).sin() * amp * 0.4;
|
||||
centre.push(c.round() as i32);
|
||||
}
|
||||
let at = |s: i32, o: i32| if r.vertical { (o, s) } else { (s, o) };
|
||||
let kind = if r.lava { Kind::Lava } else { Kind::Water };
|
||||
for s in 1..len - 1 {
|
||||
for k in 0..r.width {
|
||||
let o = centre[s as usize] - r.width / 2 + k;
|
||||
let (x, y) = at(s, o);
|
||||
if x < 1 || y < 1 || x >= w - 1 || y >= h - 1 {
|
||||
continue;
|
||||
}
|
||||
let t = &mut self.t[idx(x, y)];
|
||||
if t.owner == Owner::Player || matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate) {
|
||||
continue;
|
||||
}
|
||||
t.kind = kind;
|
||||
t.owner = Owner::None;
|
||||
t.gold = 0;
|
||||
t.marked = false;
|
||||
t.reinforced = false;
|
||||
}
|
||||
}
|
||||
// knight bridges, evenly spread, with a little landing at both ends
|
||||
let mut out = Vec::new();
|
||||
for b in 0..r.bridges {
|
||||
let s = ((b as f32 + 0.5) / r.bridges as f32 * (len - 6) as f32) as i32 + 3;
|
||||
for k in -1..=r.width {
|
||||
let o = centre[s as usize] - r.width / 2 + k;
|
||||
let (x, y) = at(s, o);
|
||||
if !inb(x, y) || x < 1 || y < 1 || x >= w - 1 || y >= h - 1 {
|
||||
continue;
|
||||
}
|
||||
let t = &mut self.t[idx(x, y)];
|
||||
if t.liquid() {
|
||||
t.kind = if r.lava { Kind::LavaBridge } else { Kind::Bridge };
|
||||
t.owner = Owner::Hero;
|
||||
out.push(idx(x, y));
|
||||
} else if t.solid() && t.kind != Kind::Rock {
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::None;
|
||||
t.gold = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Generic Dijkstra search.
|
||||
// cost(i) -> None = impassable, Some(c) = cost of entering.
|
||||
// goal(i) -> true = goal reached.
|
||||
|
||||
Reference in new issue
Block a user