Co-op matches of editor campaigns

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 17:39:38 +01:00
1 parent 0966e3318f
commit abe01ff23e
13 files changed
+394 -44

No files matched your search

+66 -19
View File
@@ -286,6 +286,13 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
menu.error = Some(i18n::tr("net.status_closed"));
}
}
MenuAction::MatchCoopEditorCreate(mission) => {
if menu.campaign_doc.missions.get(mission).map_or(false, campaign::is_editor_coop_mission) {
app.client.create_coop_doc(&menu.campaign_doc, mission, &[0, 2, 3, 4], app.settings.difficulty);
} else {
menu.error = Some(i18n::tr("net.coop_editor_none"));
}
}
MenuAction::MatchTakeSide(side) => app.client.take_side(side),
MenuAction::MatchSetAi(side, ai) => app.client.set_ai(side, ai),
MenuAction::MatchReady(ready) => app.client.ready(ready),
@@ -296,12 +303,14 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
app.game.coop = false;
app.match_mode = false;
app.coop = false;
app.coop_editor = false;
}
MenuAction::MatchLeave => {
app.client.leave();
app.game.coop = false;
app.match_mode = false;
app.coop = false;
app.coop_editor = false;
}
MenuAction::MatchChat(text) => app.client.chat(&text, false),
MenuAction::MatchEditField(i) => {
@@ -438,6 +447,7 @@ struct App {
ws_id: i32,
match_mode: bool,
coop: bool,
coop_editor: bool, // the co-op match plays a mission of an editor campaign
fps: f32,
renderer: String,
frame_ms: f32,
@@ -561,33 +571,56 @@ impl App {
self.game.coop = false;
self.match_mode = false;
self.coop = false;
self.coop_editor = false;
self.client.on_disconnect();
}
// the shared part of a co-op start (built-in or editor mission): the masters, AI seats, settings
fn start_coop_game(&mut self, mut game: Game, info: &StartInfo) -> bool {
let local = info.local_side.unwrap_or(0);
game.coop = true;
if !game.start_match(&info.sides, local) {
return false;
}
game.ai_difficulty = info.difficulty;
game.sync_ai_sides(&info.ai, info.difficulty);
apply_game_settings(&mut game, &self.settings);
self.game = game;
let (cx, cy) = self.game.map.core;
self.cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
self.match_mode = true;
self.coop = true;
self.seed = info.seed.wrapping_mul(31) | 1;
true
}
fn start_match_game(&mut self, info: StartInfo) -> bool {
self.coop_editor = false;
if !info.sides.contains(&0) {
return false;
}
if info.editor {
// a co-op mission of an editor campaign: every client builds it from the same file and
// the room's shared seed (random maps and block scripts roll from it)
let (Some(doc), Some(mission)) = (info.doc.as_ref(), info.mission) else { return false };
let Some(mission_doc) = doc.missions.get(mission) else { return false };
if !campaign::is_editor_coop_mission(mission_doc) {
return false;
}
let game = campaign::start_editor_coop_mission(mission_doc, mission, info.seed);
if !self.start_coop_game(game, &info) {
return false;
}
self.coop_editor = true;
self.pending_briefing = briefing_of(doc, mission);
return true;
}
if let Some(mission) = info.mission {
if !campaign::is_coop_mission(mission) {
return false;
}
let local = info.local_side.unwrap_or(0);
let mut game = campaign::start_coop_mission(mission, None);
game.coop = true;
if !game.start_match(&info.sides, local) {
return false;
}
game.ai_difficulty = info.difficulty;
game.sync_ai_sides(&info.ai, info.difficulty);
apply_game_settings(&mut game, &self.settings);
self.game = game;
let (cx, cy) = self.game.map.core;
self.cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
self.match_mode = true;
self.coop = true;
self.seed = info.seed.wrapping_mul(31) | 1;
return true;
let game = campaign::start_coop_mission(mission, None);
return self.start_coop_game(game, &info);
}
if info.map_w < 72 || info.map_h < 72 {
return false;
@@ -660,7 +693,7 @@ async fn main() {
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
// behind the title menu: the endless bridge battle
let (game, cam) = Game::new_demo(seed);
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, fps: 0.0, renderer: platform::renderer(), frame_ms: 0.0, frame_hist: [0.0; FRAME_HIST], frame_hist_pos: 0, custom_campaign: None, custom_progress: None, custom_mission_index: None, custom_next_index: None, editor: None, editor_testing: false, editor_test_only: false, editor_public_pending: false, editor_loading: None, pending_briefing: None };
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, coop_editor: false, fps: 0.0, renderer: platform::renderer(), frame_ms: 0.0, frame_hist: [0.0; FRAME_HIST], frame_hist_pos: 0, custom_campaign: None, custom_progress: None, custom_mission_index: None, custom_next_index: None, editor: None, editor_testing: false, editor_test_only: false, editor_public_pending: false, editor_loading: None, pending_briefing: None };
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
apply_game_settings(&mut app.game, &app.settings);
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
@@ -1368,6 +1401,7 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
| MenuAction::MatchResume
| MenuAction::MatchAbort
| MenuAction::MatchLeave
| MenuAction::MatchCoopEditorCreate(..)
| MenuAction::MatchChat(..)
| MenuAction::MatchEditField(..)) => account_action(a, &mut cloud, &mut menu, &mut app, now),
MenuAction::EditName => {
@@ -1411,6 +1445,16 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
}
// end of a mission: next mission (won) or try again (lost)
UiAction::EndNext | UiAction::EndCampaign if app.editor_testing && app.editor_test_only => back_to_editor(&mut app, &mut menu, &mut r3d),
// a co-op mission of an editor campaign is over: the match ends, back to the lobby
UiAction::EndNext | UiAction::EndCampaign if app.coop_editor => {
app.leave_match();
app.seed = next_seed(app.seed);
(app.game, app.cam) = Game::new_demo(app.seed);
menu.open_match(true);
if !app.connect_match(&cloud) {
menu.error = Some(i18n::tr("net.status_offline"));
}
}
UiAction::EndNext => {
if app.custom_campaign.is_some() {
let campaign = app.custom_campaign.clone();
@@ -1621,8 +1665,11 @@ app.pending_briefing = briefing_of(&campaign, i);
for ev in app.game.events.drain(..) {
sounds.play(ev, vec2(app.cam.rx, app.cam.rz), right, hear, now);
}
// a won mission goes into the campaign progress (with the carry-over)
if app.custom_campaign.is_none() {
// a won mission goes into the campaign progress (with the carry-over); a co-op mission of an
// editor campaign keeps no progress (its index is not a built-in mission)
if app.coop_editor {
app.recorded = app.game.state != 0;
} else if app.custom_campaign.is_none() {
if let (Some(i), false) = (app.game.mission.as_ref().map(|m| m.index), app.recorded) {
if app.game.state != 0 {
app.recorded = true;