Versus teams, gold for allies, fallen hearts as spectators, multi-mission editor co-op
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
811c955bad
commit
fc9c49d4ca
16 files changed
+1896
-49
No files matched your search
+86
-3
@@ -222,6 +222,53 @@ fn store_progress(p: &campaign::Progress) {
|
||||
platform::storage_set(PROGRESS_KEY, &serde_json::to_string(p).unwrap_or_default());
|
||||
}
|
||||
|
||||
// progress of editor campaigns played in co-op, kept by the host: campaign name -> progress
|
||||
const COOP_PROGRESS_KEY: &str = "dc.coop_editor_progress";
|
||||
|
||||
fn coop_progress_all() -> std::collections::BTreeMap<String, campaign_doc::CampaignProgress> {
|
||||
platform::storage_get(COOP_PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn store_coop_progress(name: &str, p: &campaign_doc::CampaignProgress) {
|
||||
let mut all = coop_progress_all();
|
||||
all.insert(name.to_string(), p.clone());
|
||||
platform::storage_set(COOP_PROGRESS_KEY, &serde_json::to_string(&all).unwrap_or_default());
|
||||
}
|
||||
|
||||
// a co-op run through an editor campaign: the mission being played, the run's seed, what came
|
||||
// along into it, and - once it is over - where the host goes next and what everybody takes along
|
||||
struct CoopRun {
|
||||
doc: campaign_doc::CampaignDoc,
|
||||
mission: usize,
|
||||
run_seed: u32,
|
||||
carry_in: Vec<(u8, campaign::Carry)>,
|
||||
next: Option<usize>,
|
||||
carry_out: Vec<(u8, campaign::Carry)>,
|
||||
sent: bool, // the host asked the relay for the next mission
|
||||
}
|
||||
|
||||
impl CoopRun {
|
||||
// the mission is over: the host decides where the run goes on (the branch of the reached goal,
|
||||
// or the same mission again after a defeat) and stores the progress
|
||||
fn finish(&mut self, g: &Game) {
|
||||
let won = g.state == 1;
|
||||
if won {
|
||||
let (next, choice) = self.doc.next_mission_choice(self.mission, |goal| g.match_goal_done(goal));
|
||||
self.next = next.filter(|n| self.doc.missions.get(*n).map_or(false, campaign::is_editor_coop_mission));
|
||||
self.carry_out = campaign::coop_carry_from(g);
|
||||
if g.local_side == 0 {
|
||||
let mut p = coop_progress_all().remove(&self.doc.name).unwrap_or_else(|| campaign_doc::CampaignProgress::for_doc(&self.doc));
|
||||
p.run_seed = self.run_seed;
|
||||
p.mark_done(self.mission, self.doc.missions.len(), choice);
|
||||
store_coop_progress(&self.doc.name, &p);
|
||||
}
|
||||
} else {
|
||||
self.next = Some(self.mission);
|
||||
self.carry_out = self.carry_in.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// login, account and standalone actions of the menu
|
||||
fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut App, now: f64) {
|
||||
match a {
|
||||
@@ -297,6 +344,7 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
||||
}
|
||||
MenuAction::MatchTakeSide(side) => app.client.take_side(side),
|
||||
MenuAction::MatchSetAi(side, ai) => app.client.set_ai(side, ai),
|
||||
MenuAction::MatchSetTeam(side, team, own) => app.client.set_team(if own { None } else { Some(side) }, team),
|
||||
MenuAction::MatchReady(ready) => app.client.ready(ready),
|
||||
MenuAction::MatchStart => app.client.start(),
|
||||
MenuAction::MatchResume => app.client.resume(),
|
||||
@@ -450,6 +498,7 @@ struct App {
|
||||
match_mode: bool,
|
||||
coop: bool,
|
||||
coop_editor: bool, // the co-op match plays a mission of an editor campaign
|
||||
coop_run: Option<CoopRun>, // ... and this is where the run through that campaign stands
|
||||
fps: f32,
|
||||
renderer: String,
|
||||
frame_ms: f32,
|
||||
@@ -586,6 +635,8 @@ impl App {
|
||||
}
|
||||
game.ai_difficulty = info.difficulty;
|
||||
game.sync_ai_sides(&info.ai, info.difficulty);
|
||||
// what the masters bring from the last mission of an editor campaign run (same on every client)
|
||||
campaign::apply_coop_carry(&mut game, &info.carry);
|
||||
apply_game_settings(&mut game, &self.settings);
|
||||
self.game = game;
|
||||
let (cx, cy) = self.game.map.core;
|
||||
@@ -598,6 +649,7 @@ impl App {
|
||||
|
||||
fn start_match_game(&mut self, info: StartInfo) -> bool {
|
||||
self.coop_editor = false;
|
||||
self.coop_run = None;
|
||||
if !info.sides.contains(&0) {
|
||||
return false;
|
||||
}
|
||||
@@ -614,7 +666,16 @@ impl App {
|
||||
return false;
|
||||
}
|
||||
self.coop_editor = true;
|
||||
self.recorded = false;
|
||||
self.pending_briefing = briefing_of(doc, mission);
|
||||
// the host keeps the progress of the run (a new run starts with the room's seed)
|
||||
if !info.next && info.local_side == Some(0) {
|
||||
let mut p = coop_progress_all().remove(&doc.name).unwrap_or_else(|| campaign_doc::CampaignProgress::for_doc(doc));
|
||||
p.ensure(doc.missions.len());
|
||||
p.run_seed = info.seed;
|
||||
store_coop_progress(&doc.name, &p);
|
||||
}
|
||||
self.coop_run = Some(CoopRun { doc: doc.clone(), mission, run_seed: info.seed, carry_in: info.carry.clone(), next: None, carry_out: Vec::new(), sent: false });
|
||||
return true;
|
||||
}
|
||||
if let Some(mission) = info.mission {
|
||||
@@ -633,6 +694,7 @@ impl App {
|
||||
return false;
|
||||
}
|
||||
self.game.coop = false;
|
||||
self.game.set_teams(&info.teams);
|
||||
self.game.ai_difficulty = info.difficulty;
|
||||
self.game.sync_ai_sides(&info.ai, info.difficulty);
|
||||
apply_game_settings(&mut self.game, &self.settings);
|
||||
@@ -695,7 +757,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, 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 };
|
||||
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, coop_run: None, 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 };
|
||||
@@ -852,6 +914,8 @@ async fn main() {
|
||||
// ---------------- multiplayer relay: receive messages, send queued requests, update the menu state
|
||||
app.poll_match(now);
|
||||
menu.in_match = app.match_mode || app.game.in_match();
|
||||
menu.gold_allies = app.game.gold_allies();
|
||||
ui.coop_end = app.coop_run.as_ref().filter(|_| app.coop_editor).map(|r| (app.game.local_side == 0, r.next.is_some(), r.sent));
|
||||
if matches!(menu.page, Some(Page::Match { .. })) {
|
||||
menu.set_match_ui(app.client.ui());
|
||||
}
|
||||
@@ -1215,6 +1279,7 @@ async fn main() {
|
||||
match menu.click(ux, uy, &mut app.settings, &cloud) {
|
||||
MenuAction::SettingsChanged => settings_changed = true,
|
||||
MenuAction::Resume => {}
|
||||
MenuAction::SendGold(to, amount) => app.game.queue(Act::SendGold { to, amount }),
|
||||
MenuAction::NewGame => {
|
||||
app.seed = next_seed(app.seed);
|
||||
(app.game, app.cam) = new_game(app.seed, &app.settings);
|
||||
@@ -1398,6 +1463,7 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
|
||||
| MenuAction::MatchCoopCreate(..)
|
||||
| MenuAction::MatchTakeSide(..)
|
||||
| MenuAction::MatchSetAi(..)
|
||||
| MenuAction::MatchSetTeam(..)
|
||||
| MenuAction::MatchReady(..)
|
||||
| MenuAction::MatchStart
|
||||
| MenuAction::MatchResume
|
||||
@@ -1447,7 +1513,18 @@ 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
|
||||
// a co-op mission of an editor campaign is over: the host starts the next one (or the
|
||||
// same one again) for everybody; the others wait for it
|
||||
UiAction::EndNext if app.coop_editor && app.game.local_side == 0 && app.coop_run.as_ref().map_or(false, |r| r.next.is_some() && !r.sent) => {
|
||||
if let Some(run) = app.coop_run.as_mut() {
|
||||
if let Some(next) = run.next {
|
||||
app.client.next_mission(next, run.run_seed, &run.carry_out);
|
||||
run.sent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::EndNext if app.coop_editor && app.coop_run.as_ref().map_or(false, |r| r.next.is_some()) => {}
|
||||
// ... or the run is over (or the player leaves): the match ends, back to the lobby
|
||||
UiAction::EndNext | UiAction::EndCampaign if app.coop_editor => {
|
||||
app.leave_match();
|
||||
app.seed = next_seed(app.seed);
|
||||
@@ -1521,6 +1598,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
}
|
||||
}
|
||||
UiAction::OpenMenu => menu.page = Some(Page::Pause),
|
||||
UiAction::SendGold(to, amount) => app.game.queue(Act::SendGold { to, amount }),
|
||||
UiAction::Locked => {
|
||||
let locked = ui.last_locked.unwrap_or(usize::MAX);
|
||||
app.game.msg(if app.game.can_research(locked) { "msg.tool_research" } else { "msg.tool_locked" }, vec![]);
|
||||
@@ -1670,7 +1748,12 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
// 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;
|
||||
if app.game.state != 0 && !app.recorded {
|
||||
app.recorded = true;
|
||||
if let Some(run) = app.coop_run.as_mut() {
|
||||
run.finish(&app.game);
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
|
||||
Reference in new issue
Block a user