Campaign menu and story: mission list, briefings, goals panel, mission end flow

- Title screen: Campaign with the list of the 20 missions (done / open / locked)
- Briefing per mission: story told by old Crookfinger (EN/DE), goals, new tools,
  creatures and gold carried over; start button
- In the game: goals panel with progress (click/tap opens the briefing again), the pause menu
  has a Briefing entry to read the whole briefing again
- Mission end: next mission / try again and back to the campaign; won missions and the
  carry-over are stored in the browser; epilogue after the last mission

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 11:23:44 +01:00
1 parent 46755d68ac
commit 09e22b009e
5 files changed
+523 -39

No files matched your search

+64 -5
View File
@@ -122,6 +122,20 @@ fn new_game(seed: u32, s: &Settings) -> (Game, Cam) {
(g, cam)
}
// a campaign mission with what the last mission handed over
fn mission_game(i: usize, progress: &campaign::Progress) -> (Game, Cam) {
let g = campaign::start_mission(i, progress.carry_into(i));
let (cx, cy) = g.map.core;
let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
(g, cam)
}
const PROGRESS_KEY: &str = "dc.campaign";
fn store_progress(p: &campaign::Progress) {
platform::storage_set(PROGRESS_KEY, &serde_json::to_string(p).unwrap_or_default());
}
fn next_seed(seed: u32) -> u32 {
seed.wrapping_mul(1103515245).wrapping_add(12345) | 1
}
@@ -223,6 +237,7 @@ struct App {
settings: Settings,
seed: u32,
status: Option<(String, f64)>,
recorded: bool, // the end of the current mission has been recorded in the campaign progress
}
impl App {
@@ -286,7 +301,8 @@ async fn main() {
let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
let (game, cam) = new_game(seed, &settings);
let mut app = App { game, cam, settings, seed, status: None };
let mut app = App { game, cam, settings, seed, status: None, recorded: false };
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
app.game.hints = app.settings.hints;
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 };
let mut touch = TouchInput::new();
@@ -361,10 +377,10 @@ async fn main() {
}
}
}
if matches!(menu.page, Some(Page::Saves { .. })) {
if menu.scrollable() {
let (_, wheel) = mouse_wheel();
if wheel != 0.0 {
menu.scroll_by(if wheel > 0.0 { -1 } else { 1 }, cloud.list.len());
menu.scroll_lines(if wheel > 0.0 { -1 } else { 1 });
}
}
@@ -633,6 +649,15 @@ async fn main() {
MenuAction::CloudDelete(id) => {
cloud.delete(&id);
}
MenuAction::StartMission(i) => {
(app.game, app.cam) = mission_game(i, &menu.progress);
app.game.hints = app.settings.hints;
app.recorded = false;
menu.page = None;
ui.log.clear();
ui.show_help = false;
autosave_t = 0.0;
}
MenuAction::EditName => {
if let Some(n) = platform::prompt(&i18n::tr("saves.name"), &menu.save_name) {
menu.save_name = n.chars().filter(|c| !c.is_control()).take(40).collect();
@@ -664,6 +689,25 @@ async fn main() {
(app.game, app.cam) = new_game(app.seed, &app.settings);
app.game.hints = app.settings.hints;
}
// end of a mission: next mission (won) or try again (lost)
UiAction::EndNext => {
if let Some(i) = app.game.mission.as_ref().map(|m| m.index) {
if app.game.state == 1 {
menu.page = Some(if i + 1 < campaign::missions().len() { Page::Briefing { mission: i + 1, ingame: false } } else { Page::Campaign });
} else {
(app.game, app.cam) = mission_game(i, &menu.progress);
app.game.hints = app.settings.hints;
app.recorded = false;
ui.log.clear();
}
}
}
UiAction::EndCampaign => menu.page = Some(Page::Campaign),
UiAction::Briefing => {
if let Some(i) = app.game.mission.as_ref().map(|m| m.index) {
menu.open_briefing(i);
}
}
UiAction::OpenMenu => menu.page = Some(Page::Pause),
UiAction::Locked => {
app.game.msg("msg.tool_locked", vec![]);
@@ -785,7 +829,22 @@ async fn main() {
for ev in app.game.events.drain(..) {
sounds.play(ev, vec2(app.cam.tx, app.cam.tz), right, hear, now);
}
let on_title = menu.page == Some(Page::Title) || matches!(menu.page, Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }) | Some(Page::NewGame));
// a won mission goes into the campaign progress (with the carry-over)
if let (Some(i), false) = (app.game.mission.as_ref().map(|m| m.index), app.recorded) {
if app.game.state != 0 {
app.recorded = true;
if app.game.state == 1 {
menu.progress.set_done(i, campaign::carry_from(&app.game));
store_progress(&menu.progress);
}
}
}
menu.mission = app.game.mission.as_ref().map(|m| m.index);
let on_title = menu.page == Some(Page::Title)
|| matches!(
menu.page,
Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }) | Some(Page::NewGame) | Some(Page::Campaign) | Some(Page::Briefing { ingame: false, .. })
);
music.update(&app.game, on_title, (app.cam.tx, app.cam.tz));
for m in app.game.msgs.drain(..) {
ui.log.push((m, now));
@@ -832,7 +891,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
r3d.draw(&app.game, gfx, &app.cam, now, &ov);
let title_screen = matches!(
menu.page,
Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. })
Some(Page::Title) | Some(Page::Settings { from_title: true, .. }) | Some(Page::KeyCapture { from_title: true, .. }) | Some(Page::Campaign) | Some(Page::Briefing { ingame: false, .. })
);
if !title_screen {
let info = if over_ui || (touch && inp.inspect.is_none()) {