Co-op campaign "League against the Lightwatch": 20 missions, selectable on the Match page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
405f8f60b2
commit
4e49c2fdc9
9 files changed
+240
-13
No files matched your search
@@ -660,6 +660,7 @@ net.coop_mission = Koop-Kampagne: Mission {0} ({1})
|
|||||||
net.coop_create = Koop-Kampagne
|
net.coop_create = Koop-Kampagne
|
||||||
net.coop_editor = Editor-Kampagne
|
net.coop_editor = Editor-Kampagne
|
||||||
net.coop_builtin = Eingebaute Kampagne
|
net.coop_builtin = Eingebaute Kampagne
|
||||||
|
net.coop_league = Bund gegen die Lichtwacht
|
||||||
net.coop_editor_mission = Editor-Kampagne {0}: Mission {1} ({2})
|
net.coop_editor_mission = Editor-Kampagne {0}: Mission {1} ({2})
|
||||||
net.coop_editor_none = Keine Mission der Editor-Kampagne passt für Koop (Karte mindestens 72x72, ein Ziel außer Rivalen-Meistern)
|
net.coop_editor_none = Keine Mission der Editor-Kampagne passt für Koop (Karte mindestens 72x72, ein Ziel außer Rivalen-Meistern)
|
||||||
net.join = Beitreten
|
net.join = Beitreten
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ net.coop_mission = Co-op campaign: mission {0} ({1})
|
|||||||
net.coop_create = Co-op campaign
|
net.coop_create = Co-op campaign
|
||||||
net.coop_editor = Editor campaign
|
net.coop_editor = Editor campaign
|
||||||
net.coop_builtin = Built-in campaign
|
net.coop_builtin = Built-in campaign
|
||||||
|
net.coop_league = League against the Lightwatch
|
||||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||||
net.join = Join
|
net.join = Join
|
||||||
|
|||||||
@@ -1330,6 +1330,7 @@ demo.m2.start = Un mapa aleatorio. Ni siquiera yo lo había visto antes.
|
|||||||
demo.m2.wave = Han pasado dos minutos: llegan los héroes.
|
demo.m2.wave = Han pasado dos minutos: llegan los héroes.
|
||||||
net.coop_editor = Editor campaign
|
net.coop_editor = Editor campaign
|
||||||
net.coop_builtin = Built-in campaign
|
net.coop_builtin = Built-in campaign
|
||||||
|
net.coop_league = Liga contra la Guardia Luminosa
|
||||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||||
blk.until = Repetir hasta
|
blk.until = Repetir hasta
|
||||||
|
|||||||
@@ -1330,6 +1330,7 @@ demo.m2.start = Una mappa casuale. Nemmeno io l'avevo mai vista.
|
|||||||
demo.m2.wave = Sono passati due minuti: arrivano gli eroi.
|
demo.m2.wave = Sono passati due minuti: arrivano gli eroi.
|
||||||
net.coop_editor = Editor campaign
|
net.coop_editor = Editor campaign
|
||||||
net.coop_builtin = Built-in campaign
|
net.coop_builtin = Built-in campaign
|
||||||
|
net.coop_league = Lega contro la Guardia della Luce
|
||||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||||
blk.until = Ripeti finché
|
blk.until = Ripeti finché
|
||||||
|
|||||||
@@ -1330,6 +1330,7 @@ demo.m2.start = Случайная карта. Даже я её раньше н
|
|||||||
demo.m2.wave = Две минуты прошли: идут герои.
|
demo.m2.wave = Две минуты прошли: идут герои.
|
||||||
net.coop_editor = Editor campaign
|
net.coop_editor = Editor campaign
|
||||||
net.coop_builtin = Built-in campaign
|
net.coop_builtin = Built-in campaign
|
||||||
|
net.coop_league = Союз против Светлого Дозора
|
||||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||||
blk.until = Повторять, пока не
|
blk.until = Повторять, пока не
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// The built-in co-op campaign "League against the Lightwatch": 20 missions for two to four masters,
|
||||||
|
// made as an editor campaign (painted maps, goals, branches, triggers). Crookfinger brings young
|
||||||
|
// masters together: the Lightwatch has allied with Mossgrim to smoke out every dungeon. The masters
|
||||||
|
// take fortresses, free creatures, and finally bring down the traitor Mossgrim and the High Marshal.
|
||||||
|
// In co-op the players hold the master seats, so Mossgrim's creatures fight on the heroes' side.
|
||||||
|
//
|
||||||
|
// Texts: coop.* keys in the language files; the campaign is built in the player's language.
|
||||||
|
|
||||||
|
use crate::blocks::{Block, Place, Val};
|
||||||
|
use crate::campaign_doc::*;
|
||||||
|
use crate::game::EK;
|
||||||
|
use crate::i18n::tr;
|
||||||
|
|
||||||
|
struct Spec {
|
||||||
|
size: (usize, usize),
|
||||||
|
seed: u32,
|
||||||
|
gold: f32,
|
||||||
|
gen: (u8, u8, u8, u8), // rivers, lava rivers, forts, difficulty
|
||||||
|
goals: &'static [&'static str],
|
||||||
|
waves: (bool, u32, f32), // hero waves, how many, first after
|
||||||
|
islands: &'static [((f32, f32), bool, bool, i32)], // at, lava, mana spring, treasure
|
||||||
|
caves: &'static [((f32, f32), Option<(EK, u32)>, i32)], // at, chained creature, treasure
|
||||||
|
raids: &'static [(f64, bool, &'static str, u32, u32, usize)], // after/every secs, repeat, kind, count, level, zone
|
||||||
|
boss: Option<(&'static str, u32, &'static str, usize)>, // kind, level, name, zone
|
||||||
|
branch: Option<(&'static str, usize)>, // goal, mission number it leads to
|
||||||
|
}
|
||||||
|
|
||||||
|
// zones where Mossgrim's creatures and the bosses appear (map fractions)
|
||||||
|
const ZONES: [(&str, (f32, f32)); 3] = [("east", (0.86, 0.5)), ("north", (0.7, 0.16)), ("south", (0.7, 0.84))];
|
||||||
|
|
||||||
|
const fn s(size: (usize, usize), seed: u32, gold: f32, gen: (u8, u8, u8, u8), goals: &'static [&'static str]) -> Spec {
|
||||||
|
Spec { size, seed, gold, gen, goals, waves: (false, 0, 0.0), islands: &[], caves: &[], raids: &[], boss: None, branch: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn specs() -> Vec<Spec> {
|
||||||
|
use EK::*;
|
||||||
|
vec![
|
||||||
|
// ---- Act I: the league
|
||||||
|
Spec { raids: &[(150.0, false, "Knight", 3, 1, 0)], ..s((80, 80), 9101, 3000.0, (0, 0, 0, 0), &["KillAll"]) },
|
||||||
|
s((80, 80), 9102, 3000.0, (1, 0, 1, 0), &["Towers"]),
|
||||||
|
Spec { waves: (true, 3, 240.0), raids: &[(120.0, false, "Ogre", 2, 2, 1)], ..s((80, 80), 9103, 3500.0, (1, 0, 0, 0), &["Survive(3)"]) },
|
||||||
|
Spec { islands: &[((0.5, 0.2), false, true, 0), ((0.5, 0.8), false, true, 1500)], branch: Some(("Loot(2000)", 6)), ..s((88, 80), 9104, 3000.0, (1, 0, 1, 0), &["Springs(2)"]) },
|
||||||
|
Spec { caves: &[((0.3, 0.2), Some((Ogre, 5)), 500), ((0.3, 0.8), Some((Mage, 5)), 500)], raids: &[(200.0, false, "Spider", 4, 3, 0)], ..s((88, 80), 9105, 3500.0, (0, 1, 1, 0), &["Freed(2)"]) },
|
||||||
|
// ---- Act II: against the Lightwatch
|
||||||
|
s((88, 88), 9106, 4000.0, (2, 0, 1, 1), &["Towers"]),
|
||||||
|
Spec { raids: &[(180.0, true, "Knight", 2, 3, 0)], ..s((96, 80), 9107, 4000.0, (0, 2, 1, 1), &["Loot(4000)"]) },
|
||||||
|
Spec { raids: &[(60.0, true, "Priest", 1, 3, 0)], ..s((88, 88), 9108, 4000.0, (1, 0, 1, 1), &["KillAll"]) },
|
||||||
|
Spec { boss: Some(("Paladin", 7, "ambassador", 0)), raids: &[(150.0, true, "Ogre", 2, 4, 1)], ..s((96, 88), 9109, 4500.0, (1, 1, 1, 1), &[]) },
|
||||||
|
s((96, 96), 9110, 5000.0, (1, 0, 2, 1), &["Towers"]),
|
||||||
|
Spec { waves: (true, 5, 240.0), raids: &[(300.0, true, "Gargoyle", 2, 4, 2)], ..s((96, 88), 9111, 5000.0, (1, 0, 0, 1), &["Survive(5)"]) },
|
||||||
|
Spec { waves: (true, 0, 300.0), ..s((96, 96), 9112, 5000.0, (2, 0, 1, 1), &["Heart"]) },
|
||||||
|
// ---- Act III: the traitor
|
||||||
|
Spec { caves: &[((0.5, 0.15), None, 2500), ((0.5, 0.85), None, 2500)], raids: &[(120.0, true, "Spider", 3, 4, 0)], ..s((96, 96), 9113, 5000.0, (0, 1, 1, 2), &["Loot(5000)"]) },
|
||||||
|
Spec { raids: &[(90.0, true, "Revenant", 2, 5, 1), (90.0, true, "Revenant", 2, 5, 2)], ..s((104, 96), 9114, 5500.0, (2, 0, 1, 2), &["KillAll"]) },
|
||||||
|
Spec { caves: &[((0.35, 0.2), Some((Chainbreaker, 6)), 1000)], branch: Some(("Loot(5000)", 17)), ..s((104, 96), 9115, 5500.0, (1, 1, 1, 2), &["Freed(1)", "Towers"]) },
|
||||||
|
Spec { islands: &[((0.5, 0.5), true, true, 3000)], raids: &[(150.0, true, "Mage", 2, 5, 0)], ..s((104, 104), 9116, 6000.0, (0, 2, 1, 2), &["Springs(1)", "KillAll"]) },
|
||||||
|
Spec { raids: &[(60.0, true, "Ogre", 2, 6, 0), (120.0, true, "Wyrm", 1, 5, 1)], ..s((104, 104), 9117, 6000.0, (1, 1, 1, 2), &["KillAll"]) },
|
||||||
|
s((112, 104), 9118, 6500.0, (2, 1, 3, 2), &["Towers"]),
|
||||||
|
Spec { boss: Some(("Paladin", 10, "marshal", 0)), raids: &[(120.0, true, "Knight", 3, 6, 1), (120.0, true, "Archer", 3, 6, 2)], ..s((112, 112), 9119, 7000.0, (2, 0, 2, 2), &[]) },
|
||||||
|
Spec { boss: Some(("Wyrm", 10, "mossgrim", 0)), raids: &[(90.0, true, "Ogre", 3, 7, 1), (90.0, true, "Gargoyle", 2, 7, 2)], ..s((112, 112), 9120, 8000.0, (1, 2, 2, 2), &["KillAll"]) },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn n(x: f64) -> Val {
|
||||||
|
Val::num(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(i: usize, part: &str) -> String {
|
||||||
|
format!("coop.m{:02}.{}", i + 1, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn say(k: String) -> Block {
|
||||||
|
Block::Message { text: tr(&k) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// the co-op campaign, built in the player's language
|
||||||
|
pub fn coop_campaign() -> CampaignDoc {
|
||||||
|
let mut doc = CampaignDoc { name: tr("coop.name"), author: "DungeonCraft".into(), language: crate::i18n::language(), ..CampaignDoc::default() };
|
||||||
|
let saved = (crate::map::mw(), crate::map::mh());
|
||||||
|
let specs = specs();
|
||||||
|
for (i, sp) in specs.iter().enumerate() {
|
||||||
|
let mut m = MissionDoc {
|
||||||
|
key: format!("c{:02}", i + 1),
|
||||||
|
title: tr(&key(i, "title")),
|
||||||
|
story: tr(&key(i, "story")).replace("\\n", "\n"),
|
||||||
|
size: sp.size,
|
||||||
|
seed: sp.seed,
|
||||||
|
gold: sp.gold,
|
||||||
|
portals: 2,
|
||||||
|
goals: sp.goals.iter().map(|g| g.to_string()).collect(),
|
||||||
|
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 },
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
if let Some((goal, to)) = sp.branch {
|
||||||
|
m.branches.push(BranchDoc { id: "detour".into(), goal: goal.into(), title: tr(&key(i, "branch")), next: format!("c{:02}", to), priority: Some(1) });
|
||||||
|
}
|
||||||
|
// the map: generated, then fixed with the islands, caves and the places where enemies appear
|
||||||
|
let mut g = crate::campaign::start_editor_mission(&m, i, sp.seed);
|
||||||
|
for (k, &(at, lava, spring, treasure)) in sp.islands.iter().enumerate() {
|
||||||
|
crate::campaign::editor_island(&mut g, at, lava, spring, treasure, sp.seed + k as u32);
|
||||||
|
}
|
||||||
|
for &(at, chained, treasure) in sp.caves.iter() {
|
||||||
|
crate::campaign::editor_cave(&mut g, at, chained, treasure);
|
||||||
|
}
|
||||||
|
let (w, h) = (g.map.w as i32, g.map.h as i32);
|
||||||
|
for (name, (fx, fy)) in ZONES {
|
||||||
|
let (cx, cy) = (((fx * w as f32) as i32).clamp(4, w - 5), ((fy * h as f32) as i32).clamp(4, h - 5));
|
||||||
|
for y in cy - 2..=cy + 2 {
|
||||||
|
for x in cx - 2..=cx + 2 {
|
||||||
|
let t = &mut g.map.t[crate::map::idx(x, y)];
|
||||||
|
if !matches!(t.kind, crate::map::Kind::Core | crate::map::Kind::Portal | crate::map::Kind::Gate | crate::map::Kind::Tower) {
|
||||||
|
t.kind = crate::map::Kind::Floor;
|
||||||
|
t.owner = crate::map::Owner::Hero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.zones.push(ZoneDoc { name: name.into(), shape: "rect".into(), at: None, radius: None, tiles: None, from: Some(((cx - 2) as f32, (cy - 2) as f32)), to: Some(((cx + 2) as f32, (cy + 2) as f32)), width: None });
|
||||||
|
}
|
||||||
|
g.recount();
|
||||||
|
g.place_all_torches();
|
||||||
|
g.init_fog();
|
||||||
|
m.map = MapDoc::Fixed { game: serde_json::to_string(&g).unwrap_or_default() };
|
||||||
|
|
||||||
|
// the scripts: Crookfinger's start, the objective, story lines, raids and the boss
|
||||||
|
let sc = &mut m.scripts;
|
||||||
|
sc.start = vec![say(key(i, "start")), Block::Objective { text: tr(&key(i, "goal")) }];
|
||||||
|
sc.triggers.push(TriggerDoc { name: "story1".into(), kind: TriggerKind::Timer { secs: 90.0, every: false }, once: true, enabled: true, script: vec![Block::Hint { text: tr(&key(i, "t1")) }] });
|
||||||
|
sc.triggers.push(TriggerDoc { name: "story2".into(), kind: TriggerKind::Timer { secs: 360.0, every: false }, once: true, enabled: true, script: vec![Block::Hint { text: tr(&key(i, "t2")) }] });
|
||||||
|
for (k, &(secs, every, kind, count, level, zone)) in sp.raids.iter().enumerate() {
|
||||||
|
sc.triggers.push(TriggerDoc {
|
||||||
|
name: format!("raid{}", k + 1),
|
||||||
|
kind: TriggerKind::Timer { secs, every },
|
||||||
|
once: !every,
|
||||||
|
enabled: true,
|
||||||
|
script: vec![
|
||||||
|
Block::Message { text: tr("coop.raid") },
|
||||||
|
Block::Spawn { side: 1, kind: kind.into(), count: n(count as f64), level: n(level as f64), place: Place::Zone(ZONES[zone].0.into()), name: String::new() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some((kind, level, name, zone)) = sp.boss {
|
||||||
|
sc.start.push(Block::Spawn { side: 1, kind: kind.into(), count: n(1.0), level: n(level as f64), place: Place::Zone(ZONES[zone].0.into()), name: name.into() });
|
||||||
|
// the boss falls: the mission is won once the goals (if any) are done too
|
||||||
|
let win = if sp.goals.is_empty() { vec![say(key(i, "boss")), Block::Win] } else { vec![say(key(i, "boss")), Block::SetVar { name: "boss".into(), value: n(1.0) }] };
|
||||||
|
sc.triggers.push(TriggerDoc { name: "boss".into(), kind: TriggerKind::UnitDies { side: 1, kind: String::new(), name: name.into() }, once: true, enabled: true, script: win });
|
||||||
|
if !sp.goals.is_empty() {
|
||||||
|
// KillAll covers the boss as well: nothing more to do
|
||||||
|
sc.victory = vec![];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc.missions.push(m);
|
||||||
|
}
|
||||||
|
crate::map::set_dims(saved.0, saved.1);
|
||||||
|
doc
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_coop_campaign_is_complete_and_valid() {
|
||||||
|
let doc = coop_campaign();
|
||||||
|
assert_eq!(doc.missions.len(), 20);
|
||||||
|
assert!(doc.validate().is_empty(), "{:?}", doc.validate());
|
||||||
|
for m in &doc.missions {
|
||||||
|
assert!(m.size.0 >= 72 && m.size.1 >= 72, "{} fits the co-op master areas", m.key);
|
||||||
|
assert!(!m.goals.is_empty() || m.scripts.triggers.iter().any(|t| t.name == "boss"), "{} can be won", m.key);
|
||||||
|
assert!(matches!(m.map, MapDoc::Fixed { .. }));
|
||||||
|
}
|
||||||
|
// every mission starts and its scripts run
|
||||||
|
for (i, m) in doc.missions.iter().enumerate().step_by(5) {
|
||||||
|
let mut g = crate::campaign::start_editor_mission(m, i, 3);
|
||||||
|
for _ in 0..(20 * 5) {
|
||||||
|
g.update(0.05);
|
||||||
|
}
|
||||||
|
assert!(g.script.as_ref().unwrap().error.is_none());
|
||||||
|
assert_eq!(g.state, 0, "{} is not decided at once", m.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
// handles what it returns (test play, files, the savegame service).
|
// handles what it returns (test play, files, the savegame service).
|
||||||
|
|
||||||
pub mod blocks_ui;
|
pub mod blocks_ui;
|
||||||
|
pub mod coop_campaign;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod templates;
|
pub mod templates;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|||||||
+3
-2
@@ -336,8 +336,9 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuAction::MatchCoopEditorCreate(mission) => {
|
MenuAction::MatchCoopEditorCreate(mission) => {
|
||||||
if menu.campaign_doc.missions.get(mission).map_or(false, campaign::is_editor_coop_mission) {
|
let doc = menu.coop_doc().clone();
|
||||||
app.client.create_coop_doc(&menu.campaign_doc, mission, &[0, 2, 3, 4], app.settings.difficulty);
|
if doc.missions.get(mission).map_or(false, campaign::is_editor_coop_mission) {
|
||||||
|
app.client.create_coop_doc(&doc, mission, &[0, 2, 3, 4], app.settings.difficulty);
|
||||||
} else {
|
} else {
|
||||||
menu.error = Some(i18n::tr("net.coop_editor_none"));
|
menu.error = Some(i18n::tr("net.coop_editor_none"));
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-11
@@ -421,7 +421,9 @@ pub struct Menu {
|
|||||||
pub error: Option<String>, // message on the login and account pages
|
pub error: Option<String>, // message on the login and account pages
|
||||||
pub match_ui: MatchUi, // copied state of the multiplayer relay, shown on the Match page
|
pub match_ui: MatchUi, // copied state of the multiplayer relay, shown on the Match page
|
||||||
pub coop_mission: usize, // co-op campaign mission selected on the Match page
|
pub coop_mission: usize, // co-op campaign mission selected on the Match page
|
||||||
pub coop_editor: bool, // the Match page offers the editor campaign instead of the built-in one
|
pub coop_editor: bool, // the Match page offers a campaign document instead of the built-in missions
|
||||||
|
pub coop_league: bool, // ... and that document is the co-op campaign "League against the Lightwatch"
|
||||||
|
league_doc: Option<CampaignDoc>, // the co-op campaign, built the first time it is chosen
|
||||||
pub coop_editor_mission: usize, // mission of `campaign_doc` selected for co-op
|
pub coop_editor_mission: usize, // mission of `campaign_doc` selected for co-op
|
||||||
pub in_match: bool, // a match is being played; the pause menu changes its shape
|
pub in_match: bool, // a match is being played; the pause menu changes its shape
|
||||||
pub gold_allies: Vec<u8>, // allies of the local master that can get gold (pause menu)
|
pub gold_allies: Vec<u8>, // allies of the local master that can get gold (pause menu)
|
||||||
@@ -463,6 +465,8 @@ impl Menu {
|
|||||||
match_ui: MatchUi::default(),
|
match_ui: MatchUi::default(),
|
||||||
coop_mission: crate::campaign::coop_mission_indexes().first().copied().unwrap_or(0),
|
coop_mission: crate::campaign::coop_mission_indexes().first().copied().unwrap_or(0),
|
||||||
coop_editor: false,
|
coop_editor: false,
|
||||||
|
coop_league: false,
|
||||||
|
league_doc: None,
|
||||||
coop_editor_mission: 0,
|
coop_editor_mission: 0,
|
||||||
in_match: false,
|
in_match: false,
|
||||||
gold_allies: Vec::new(),
|
gold_allies: Vec::new(),
|
||||||
@@ -764,7 +768,7 @@ impl Menu {
|
|||||||
Some(b @ (Btn::MatchCoopPrev | Btn::MatchCoopNext)) => {
|
Some(b @ (Btn::MatchCoopPrev | Btn::MatchCoopNext)) => {
|
||||||
let dir = if b == Btn::MatchCoopPrev { -1 } else { 1 };
|
let dir = if b == Btn::MatchCoopPrev { -1 } else { 1 };
|
||||||
if self.coop_editor {
|
if self.coop_editor {
|
||||||
self.coop_editor_mission = crate::campaign::next_editor_coop_mission(&self.campaign_doc, self.coop_editor_mission, dir);
|
self.coop_editor_mission = crate::campaign::next_editor_coop_mission(self.coop_doc(), self.coop_editor_mission, dir);
|
||||||
} else {
|
} else {
|
||||||
self.coop_mission = crate::campaign::next_coop_mission(self.coop_mission, dir);
|
self.coop_mission = crate::campaign::next_coop_mission(self.coop_mission, dir);
|
||||||
}
|
}
|
||||||
@@ -772,8 +776,25 @@ impl Menu {
|
|||||||
MenuAction::Consumed
|
MenuAction::Consumed
|
||||||
}
|
}
|
||||||
Some(Btn::MatchCoopSource) => {
|
Some(Btn::MatchCoopSource) => {
|
||||||
self.coop_editor = !self.coop_editor && !self.campaign_doc.missions.is_empty();
|
// built-in missions -> the league -> the editor's campaign (if it has missions) -> ...
|
||||||
self.coop_editor_mission = crate::campaign::next_editor_coop_mission(&self.campaign_doc, self.coop_editor_mission, 0);
|
match (self.coop_editor, self.coop_league) {
|
||||||
|
(false, _) => {
|
||||||
|
if self.league_doc.is_none() {
|
||||||
|
self.league_doc = Some(crate::editor::coop_campaign::coop_campaign());
|
||||||
|
}
|
||||||
|
self.coop_editor = true;
|
||||||
|
self.coop_league = true;
|
||||||
|
}
|
||||||
|
(true, true) if !self.campaign_doc.missions.is_empty() => self.coop_league = false,
|
||||||
|
_ => {
|
||||||
|
self.coop_editor = false;
|
||||||
|
self.coop_league = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.coop_editor_mission = 0;
|
||||||
|
if self.coop_editor {
|
||||||
|
self.coop_editor_mission = crate::campaign::next_editor_coop_mission(self.coop_doc(), 0, 0);
|
||||||
|
}
|
||||||
self.confirm = None;
|
self.confirm = None;
|
||||||
MenuAction::Consumed
|
MenuAction::Consumed
|
||||||
}
|
}
|
||||||
@@ -1717,6 +1738,14 @@ impl Menu {
|
|||||||
self.column(gfx, x + (cols as f32 * CW() - w) * 0.5, y + (rows - 4) as f32 * CH(), w, &[(Btn::Resume, label)]);
|
self.column(gfx, x + (cols as f32 * CW() - w) * 0.5, y + (rows - 4) as f32 * CH(), w, &[(Btn::Resume, label)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the campaign document the co-op create offers: the league or the editor's campaign
|
||||||
|
pub fn coop_doc(&self) -> &CampaignDoc {
|
||||||
|
match (&self.league_doc, self.coop_league) {
|
||||||
|
(Some(d), true) => d,
|
||||||
|
_ => &self.campaign_doc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scrollable(&self) -> bool {
|
pub fn scrollable(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.page,
|
self.page,
|
||||||
@@ -1920,15 +1949,16 @@ impl Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the editor campaign can only be offered while it has missions (it may have been replaced)
|
// the editor campaign can only be offered while it has missions (it may have been replaced)
|
||||||
let editor_coop = self.coop_editor && !self.campaign_doc.missions.is_empty();
|
let doc = self.coop_doc().clone();
|
||||||
let editor_list = if editor_coop { crate::campaign::editor_coop_indexes(&self.campaign_doc) } else { Vec::new() };
|
let editor_coop = self.coop_editor && !doc.missions.is_empty();
|
||||||
|
let editor_list = if editor_coop { crate::campaign::editor_coop_indexes(&doc) } else { Vec::new() };
|
||||||
if editor_coop && !editor_list.contains(&self.coop_editor_mission) {
|
if editor_coop && !editor_list.contains(&self.coop_editor_mission) {
|
||||||
self.coop_editor_mission = editor_list.first().copied().unwrap_or(0);
|
self.coop_editor_mission = editor_list.first().copied().unwrap_or(0);
|
||||||
}
|
}
|
||||||
if !ui.started && !ui.room_open() && editor_coop {
|
if !ui.started && !ui.room_open() && editor_coop {
|
||||||
let line = match self.campaign_doc.missions.get(self.coop_editor_mission).filter(|_| !editor_list.is_empty()) {
|
let line = match doc.missions.get(self.coop_editor_mission).filter(|_| !editor_list.is_empty()) {
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let name = if self.campaign_doc.name.trim().is_empty() { "?".to_string() } else { self.campaign_doc.name.clone() };
|
let name = if doc.name.trim().is_empty() { "?".to_string() } else { doc.name.clone() };
|
||||||
let title = if m.title.trim().is_empty() { m.key.clone() } else { m.title.clone() };
|
let title = if m.title.trim().is_empty() { m.key.clone() } else { m.title.clone() };
|
||||||
trf("net.coop_editor_mission", &[name, (self.coop_editor_mission + 1).to_string(), title])
|
trf("net.coop_editor_mission", &[name, (self.coop_editor_mission + 1).to_string(), title])
|
||||||
}
|
}
|
||||||
@@ -1963,9 +1993,13 @@ impl Menu {
|
|||||||
items.push((Btn::MatchCoopCreate, tr("net.coop_create")));
|
items.push((Btn::MatchCoopCreate, tr("net.coop_create")));
|
||||||
items.push((Btn::MatchCoopNext, ">".to_string()));
|
items.push((Btn::MatchCoopNext, ">".to_string()));
|
||||||
}
|
}
|
||||||
if !self.campaign_doc.missions.is_empty() {
|
// the button names what comes next
|
||||||
items.push((Btn::MatchCoopSource, tr(if editor_coop { "net.coop_builtin" } else { "net.coop_editor" })));
|
let next = match (editor_coop, self.coop_league) {
|
||||||
}
|
(false, _) => "net.coop_league",
|
||||||
|
(true, true) if !self.campaign_doc.missions.is_empty() => "net.coop_editor",
|
||||||
|
_ => "net.coop_builtin",
|
||||||
|
};
|
||||||
|
items.push((Btn::MatchCoopSource, tr(next)));
|
||||||
}
|
}
|
||||||
if ui.room_open() {
|
if ui.room_open() {
|
||||||
items.push((Btn::MatchReady, tr(if ui.ready { "net.unready" } else { "net.ready" })));
|
items.push((Btn::MatchReady, tr(if ui.ready { "net.unready" } else { "net.ready" })));
|
||||||
|
|||||||
Reference in new issue
Block a user