Tutorial: Crookfinger leads a new master through the first steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 00:37:25 +01:00
1 parent 1fb6a6ab58
commit 160935bf40
23 files changed
+501 -3

No files matched your search

+101
View File
@@ -238,6 +238,16 @@ pub fn demo_campaign() -> CampaignDoc {
#[cfg(test)]
mod tests {
#[test]
fn the_tutorial_is_a_valid_campaign() {
let doc = super::tutorial_campaign();
assert!(doc.validate().is_empty(), "{:?}", doc.validate());
assert_eq!(doc.missions.len(), 1);
let g = crate::campaign::start_editor_mission(&doc.missions[0], 0, 1);
assert!(g.script.is_some());
assert!(!g.ents.iter().any(|e| e.alive && e.active_hero()), "no knight before the last step");
}
use super::*;
#[test]
@@ -278,3 +288,94 @@ mod tests {
assert!(g.mission.as_ref().unwrap().goals.contains(&Goal::Shards), "the shard of the outpost is still there");
}
}
// The tutorial: Crookfinger leads a new master step by step - dig the tunnel, claim the hall,
// build a lair and a mushroom garden, wait for the first creatures, slap one, and beat the first
// two knights. Each step switches on the trigger of the next one. Played like an editor campaign
// (title menu: Tutorial); texts come from the language files (tut.*).
pub fn tutorial_campaign() -> CampaignDoc {
let mut doc = CampaignDoc { name: tr("tut.name"), author: "DungeonCraft".into(), language: crate::i18n::language(), ..CampaignDoc::default() };
let mut m = MissionDoc {
key: "tutorial".into(),
title: tr("tut.title"),
story: tr("tut.story").replace("\\n", "\n"),
size: (64, 64),
seed: 777,
gold: 3000.0,
goals: Vec::new(),
gen: GenDoc { rivers: 0, lava: 0, forts: 0, rivals: 0, difficulty: 0, surface: false },
pos: (40.0, 60.0),
..MissionDoc::default()
};
let saved = (crate::map::mw(), crate::map::mh());
let mut g = crate::campaign::start_editor_mission(&m, 0, 777);
let (cx, cy) = g.map.core;
let (w, h) = (g.map.w as i32, g.map.h as i32);
let cl = |x: i32, y: i32| (x.clamp(3, w - 4), y.clamp(3, h - 4));
// east of the heart: a tunnel of earth to dig, then a hall of open ground behind it
let tunnel: Vec<(i32, i32)> = (cx + 4..cx + 8).flat_map(|x| [(x, cy + 1), (x, cy + 2)]).map(|(x, y)| cl(x, y)).collect();
let (hx0, hy0) = cl(cx + 8, cy - 2);
let (hx1, hy1) = cl(cx + 15, cy + 5);
for y in hy0 - 1..=hy1 + 1 {
for x in hx0..=hx1 + 1 {
let t = &mut g.map.t[crate::map::idx(x, y)];
let edge = y == hy0 - 1 || y == hy1 + 1 || x == hx1 + 1;
t.kind = if edge { crate::map::Kind::Earth } else { crate::map::Kind::Floor };
t.owner = crate::map::Owner::None;
t.gold = 0;
t.reinforced = false;
}
}
for &(x, y) in &tunnel {
let t = &mut g.map.t[crate::map::idx(x, y)];
t.kind = crate::map::Kind::Earth;
t.owner = crate::map::Owner::None;
t.hp = 3.0;
}
// the knights come from a cave further east
let (kx, ky) = cl(cx + 22, cy + 1);
for y in ky - 2..=ky + 2 {
for x in kx - 2..=kx + 2 {
let t = &mut g.map.t[crate::map::idx(x, y)];
t.kind = crate::map::Kind::Floor;
t.owner = crate::map::Owner::None;
}
}
for x in hx1 + 1..kx - 2 {
let t = &mut g.map.t[crate::map::idx(x, cy + 1)];
t.kind = crate::map::Kind::Earth;
t.owner = crate::map::Owner::None;
}
g.ents.retain(|e| !e.hero);
g.recount();
g.init_fog();
m.zones = vec![
ZoneDoc { name: "tunnel".into(), shape: "tiles".into(), at: None, radius: None, tiles: Some(tunnel), from: None, to: None, width: None },
zone_rect("hall", hx0, hy0, hx1, hy1),
ZoneDoc { name: "knights".into(), shape: "circle".into(), at: Some((kx as f32, ky as f32)), radius: Some(1.5), tiles: None, from: None, to: None, width: None },
];
m.map = MapDoc::Fixed { game: serde_json::to_string(&g).unwrap_or_default() };
crate::map::set_dims(saved.0, saved.1);
let s = &mut m.scripts;
let obj = |k: &str| Block::Objective { text: tr(k) };
let on = |t: &str| Block::Trigger { name: t.into(), on: true };
s.start = vec![say("tut.start"), obj("tut.obj1"), Block::SetVar { name: "knights".into(), value: n(0.0) }, Block::Camera { zone: "tunnel".into() }];
s.victory = vec![Block::If { cond: cmp(">=", var("knights"), n(2.0)), then: vec![say("tut.win"), Block::Win], els: vec![] }];
let off = |t: TriggerDoc| TriggerDoc { enabled: false, ..t };
s.triggers = vec![
trig("dug", TriggerKind::ZoneDug { zone: "tunnel".into() }, true, vec![say("tut.dug"), obj("tut.obj2"), on("lair")]),
off(trig("lair", TriggerKind::RoomBuilt { room: "Lair".into() }, true, vec![say("tut.lair"), obj("tut.obj3"), on("farm")])),
off(trig("farm", TriggerKind::RoomBuilt { room: "Farm".into() }, true, vec![say("tut.farm"), obj("tut.obj4"), on("creatures")])),
off(trig("creatures", TriggerKind::Value { what: "creatures".into(), var: String::new(), op: ">=".into(), n: 2.0 }, true, vec![say("tut.creatures"), obj("tut.obj5"), on("slap")])),
off(trig(
"slap",
TriggerKind::Slap,
true,
vec![say("tut.slap"), obj("tut.obj6"), Block::Wait { secs: n(20.0) }, Block::Spawn { side: 1, kind: "Knight".into(), count: n(2.0), level: n(1.0), place: Place::Zone("knights".into()), name: String::new() }, say("tut.knights"), Block::Camera { zone: "knights".into() }, on("knight")],
)),
off(trig("knight", TriggerKind::UnitDies { side: 1, kind: String::new(), name: String::new() }, false, vec![Block::AddVar { name: "knights".into(), value: n(1.0) }])),
];
doc.missions.push(m);
doc
}