Final missions above ground (M30, co-op M20), surface option for versus matches, docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 22:50:24 +01:00
1 parent 1ae41228ab
commit f27dbea2f1
28 files changed
+111 -49

No files matched your search

+7 -4
View File
@@ -358,16 +358,19 @@ pub fn missions() -> Vec<MissionDef> {
.boss(Paladin, 10)],
..base("m20", (160, 160), 2020, vec![Goal::Heart, Goal::Shards])
});
// the finale above ground: the dungeon in the rock, the eastern half a forest with the High
// Marshal's fortress (the gate of the order) and both rival masters
v.push(MissionDef {
portals: 3,
gold: 6000.0,
gate: true,
waves: true,
first_wave: 420.0,
gate_defenders: true,
rivers: vec![river(false, true, 0.45, 4, 2), river(true, false, 0.5, 4, 2)],
islands: vec![island((0.3, 0.2), false, true, 3000)],
rivals: vec![(0.85, 0.22, 2, 2), (0.85, 0.78, 2, 3)],
rivers: vec![river(false, true, 0.3, 4, 2)],
islands: vec![island((0.2, 0.2), false, true, 3000)],
forts: vec![fort((0.8, 0.5), (27, 25), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Cannon], 3, 14, 4, 4, 3, 3000).heart().boss(Paladin, 10)],
rivals: vec![(0.86, 0.15, 2, 2), (0.86, 0.85, 2, 3)],
surface: 0.48,
..base("m30", (176, 176), 3030, vec![Goal::Rivals, Goal::Heart])
});
v
+1 -1
View File
@@ -91,7 +91,7 @@ pub fn coop_campaign() -> CampaignDoc {
waves: sp.waves.0,
wave_limit: sp.waves.1,
first_wave: if sp.waves.0 { sp.waves.2 } else { 300.0 },
gen: GenDoc { rivers: sp.gen.0, lava: sp.gen.1, forts: sp.gen.2, rivals: 0, difficulty: sp.gen.3, surface: false },
gen: GenDoc { rivers: sp.gen.0, lava: sp.gen.1, forts: sp.gen.2, rivals: 0, difficulty: sp.gen.3, surface: i + 1 == specs.len() },
next: (i + 1 < specs.len()).then(|| format!("c{:02}", i + 2)),
pos: (40.0 + (i % 5) as f32 * 220.0, 40.0 + (i / 5) as f32 * 140.0),
..MissionDoc::default()
+16 -2
View File
@@ -90,6 +90,7 @@ pub struct StartInfo {
pub teams: Vec<(u8, u8)>, // (side, team) of a versus match
pub next: bool, // the next mission of a running editor campaign co-op match
pub carry: Vec<(u8, crate::campaign::Carry)>, // what every side brings into that mission
pub surface: bool, // a free match whose eastern half lies above ground
}
#[derive(Clone, Debug, Default)]
@@ -223,6 +224,18 @@ impl MatchClient {
self.send_create(sides, map_w, map_h, difficulty, None);
}
// a free match; `surface`: its eastern half lies above ground
pub fn create_land(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, surface: bool) {
let n = self.outbox.len();
self.send_create(sides, map_w, map_h, difficulty, None);
if let (true, Some(text)) = (surface, self.outbox.get_mut(n)) {
if let Ok(mut msg) = serde_json::from_str::<Value>(text) {
msg["surface"] = json!(true);
*text = msg.to_string();
}
}
}
pub fn create_coop(&mut self, mission: usize, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8) {
self.send_create(sides, map_w, map_h, difficulty, Some(mission));
}
@@ -550,7 +563,8 @@ impl MatchClient {
// a resumed editor campaign run goes on like the next mission of a running one
let resume = editor && v.get("resume").and_then(|x| x.as_bool()).unwrap_or(false);
let carry: Vec<(u8, crate::campaign::Carry)> = if resume { v.get("carry").cloned().and_then(|c| serde_json::from_value(c).ok()).unwrap_or_default() } else { Vec::new() };
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry });
let surface = v.get("surface").and_then(|x| x.as_bool()).unwrap_or(false);
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry, surface });
self.error = None;
self.status = None;
}
@@ -581,7 +595,7 @@ impl MatchClient {
}
r.ai = ai.clone();
}
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission: Some(mission), ai, doc: Some(doc), editor: true, teams, next: true, carry });
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission: Some(mission), ai, doc: Some(doc), editor: true, teams, next: true, carry, surface: false });
self.error = None;
}
+6 -2
View File
@@ -349,7 +349,7 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
}
}
MenuAction::MatchJoin(code) => app.client.join(&code),
MenuAction::MatchCreate => app.client.create(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty),
MenuAction::MatchCreate => app.client.create_land(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty, app.settings.surface),
MenuAction::MatchCoopCreate(mission) => {
let all = campaign::missions();
if let Some(d) = all.get(mission) {
@@ -692,7 +692,7 @@ impl App {
let next = run_seed.is_some();
let seed = run_seed.unwrap_or_else(|| next_seed(self.seed));
let editor = doc.is_some();
let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry };
let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry, surface: false };
if !self.start_match_game(info) {
return false;
}
@@ -772,6 +772,10 @@ impl App {
if !self.game.start_match(&info.sides, local) {
return false;
}
if info.surface {
// the same land on every client: built from the shared seed
self.game.add_surface(info.seed, 0.5, 2 + (info.map_h / 64) as u32);
}
self.game.coop = false;
self.game.set_teams(&info.teams);
self.game.ai_difficulty = info.difficulty;
+12
View File
@@ -183,4 +183,16 @@ mod tests {
}
}
}
#[test]
fn the_finale_is_fought_above_ground() {
let g = crate::campaign::start_mission(29, None);
assert!(g.surface_share() > 0.3, "share {}", g.surface_share());
assert_eq!(g.rivals.len(), 2);
assert!(g.gate_hp > 0.0 && g.has_gate());
let (gx, gy) = g.map.gate;
assert!(g.map.sky(idx(gx + 1, gy + 1)), "the gate of the order stands in the forest");
// the heroes find their way from the gate to the core
assert!(g.map.hero_path_to_core(idx(gx + 1, gy + 1)).is_some());
}
}