Co-op campaign matches and command markers: mission relay, allied masters, temporary flags

This commit is contained in:
Richard Zink committed 2026-11-04 20:07:06 +01:00
1 parent dc22e27184
commit 5928acf7f6
12 files changed
+433 -27

No files matched your search

+61 -6
View File
@@ -476,6 +476,15 @@ fn place_cave(g: &mut Game, d: &CaveDef, w: usize, h: usize) {
// Builds the game of mission i (carry: what the last mission handed over)
pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
start_mission_mode(i, carry, false)
}
// A co-op campaign mission: the same mission, but without rival masters on playable sides.
pub fn start_coop_mission(i: usize, carry: Option<&Carry>) -> Game {
start_mission_mode(i, carry, true)
}
fn start_mission_mode(i: usize, carry: Option<&Carry>, coop: bool) -> Game {
let all = missions();
let d = &all[i.min(all.len() - 1)];
let (w, h) = d.size;
@@ -498,10 +507,12 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
for c in &d.caves {
place_cave(&mut g, c, w, h);
}
for &(rx, ry, diff, side) in &d.rivals {
let x = ((rx * w as f32) as i32).clamp(10, w as i32 - 10);
let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10);
g.add_rival(side, x, y, diff);
if !coop {
for &(rx, ry, diff, side) in &d.rivals {
let x = ((rx * w as f32) as i32).clamp(10, w as i32 - 10);
let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10);
g.add_rival(side, x, y, diff);
}
}
g.place_dwarf_caves(d.seed);
g.add_wilds(d.seed, false);
@@ -514,9 +525,14 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
g.gate_on = heart;
g.guard_treasures();
g.place_all_torches();
let goals: Vec<Goal> = if coop {
d.goals.iter().filter(|&g| *g != Goal::Rivals).cloned().collect()
} else {
d.goals.clone()
};
g.mission = Some(MissionState {
index: i,
goals: d.goals.clone(),
goals,
tools: TOOL_UNLOCK.iter().map(|&u| u <= i).collect(),
waves: d.waves,
wave_limit: d.wave_limit,
@@ -548,6 +564,30 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
g
}
// missions that can be played in co-op: they fit the multiplayer master areas and do not
// depend on rival masters as their only goal
pub fn is_coop_mission(i: usize) -> bool {
missions().get(i).map_or(false, |d| d.size.0 >= 72 && d.size.1 >= 72 && d.goals.iter().any(|&g| g != Goal::Rivals))
}
pub fn coop_mission_indexes() -> Vec<usize> {
missions()
.iter()
.enumerate()
.filter(|(_, d)| d.size.0 >= 72 && d.size.1 >= 72 && d.goals.iter().any(|&g| g != Goal::Rivals))
.map(|(i, _)| i)
.collect()
}
pub fn next_coop_mission(i: usize, dir: i32) -> usize {
let list = coop_mission_indexes();
if list.is_empty() {
return 0;
}
let pos = list.iter().position(|m| *m == i).unwrap_or(0);
list[((pos as i32 + dir).rem_euclid(list.len() as i32)) as usize]
}
// the strongest creatures and a fifth of the gold go along into the next mission
pub fn carry_from(g: &Game) -> Carry {
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
@@ -635,6 +675,21 @@ mod tests {
}
}
#[test]
fn coop_missions_drop_rivals_and_are_not_decided_at_once() {
let list = coop_mission_indexes();
assert!(list.len() > 3, "co-op campaign needs selectable missions");
for &i in &list {
assert!(is_coop_mission(i), "mission {} selectable in co-op", i);
let g = start_coop_mission(i, None);
assert!(g.rivals.is_empty(), "mission {} must not spawn rival masters", i);
let m = g.mission.as_ref().unwrap();
assert!(!m.goals.iter().any(|g| *g == Goal::Rivals), "mission {} keeps the rival goal", i);
assert!(!m.goals.is_empty(), "mission {} has a playable goal after dropping rivals", i);
assert!(!m.goals.iter().all(|&goal| g.goal_done(goal)), "mission {} is decided at the start", i);
}
}
// A simple scripted player: digs a corridor to the shard, bridges rivers on the way and
// sends a few strong creatures along with the flag.
fn play_to_shard(i: usize) -> (u8, f32) {
@@ -648,7 +703,7 @@ mod tests {
match t.kind {
Kind::Rock | Kind::Core | Kind::Tower => None,
Kind::Water | Kind::Lava => Some(3),
k if t.solid() => Some(if t.reinforced { 6 } else { 2 }),
_ if t.solid() => Some(if t.reinforced { 6 } else { 2 }),
_ => Some(1),
}
}, |j| j == shard, false)