Block scripts and triggers: data model, Rhai translation and runtime

Editor missions keep a painted map or roll a random map per campaign run,
leave out goals they cannot have, and run their triggers in the fixed step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 16:51:43 +01:00
1 parent 5315709776
commit 5701124c4f
13 files changed
+1835 -483

No files matched your search

+92
View File
@@ -589,6 +589,98 @@ fn start_mission_def(d: &MissionDef, i: usize, carry: Option<&Carry>, coop: bool
g
}
// ------------------------------------------------------------------ editor missions
// a mission definition from editor generator settings (random maps and new painted maps)
pub fn generated_def(d: &crate::campaign_doc::MissionDoc, seed: u32) -> MissionDef {
let mut out = mission_def_from_doc(d);
out.seed = seed;
let g = &d.gen;
let mut rng = Rng(seed | 1);
for k in 0..g.rivers.min(3) {
out.rivers.push(river(false, k % 2 == 0, 0.3 + 0.2 * k as f32 + rng.f() * 0.08, 4, 1 + (rng.next() % 2)));
}
for k in 0..g.lava.min(2) {
out.rivers.push(river(true, k % 2 == 1, 0.62 - 0.25 * k as f32, 4, 1));
}
let lvl = 1 + g.difficulty.min(2) as u32;
for k in 0..g.forts.min(3) {
let at = [(0.8, 0.5), (0.75, 0.2), (0.75, 0.8)][k as usize];
out.forts.push(fort(at, (17, 15), &[3], &[TowerKind::Arrow, TowerKind::Arrow], lvl, 3 + 2 * lvl, lvl, lvl, 1, 1000 * lvl as i32));
}
for k in 0..g.rivals.min(2) {
let (x, y) = [(0.82, 0.22), (0.82, 0.78)][k as usize];
out.rivals.push((x, y, g.difficulty.min(2), 2 + k));
}
out
}
// the seed of a random map in one run of a campaign
pub fn run_map_seed(run_seed: u32, mission: usize) -> u32 {
run_seed.wrapping_mul(2654435761).wrapping_add(mission as u32 * 40503) | 1
}
// a mission of an editor campaign: its painted map or a random map (rolled from the run's
// seed), the goals it can actually have, and its block scripts
pub fn start_editor_mission(doc: &crate::campaign_doc::MissionDoc, i: usize, run_seed: u32) -> Game {
use crate::campaign_doc::MapDoc;
let mut g = match &doc.map {
MapDoc::Fixed { game } => match load_fixed_map(game) {
Some(g) => g,
None => start_mission_def(&generated_def(doc, run_map_seed(run_seed, i)), i, None, false),
},
MapDoc::Random => start_mission_def(&generated_def(doc, run_map_seed(run_seed, i)), i, None, false),
};
g.msgs.clear();
g.state = 0;
g.time = 0.0;
// goals the map cannot have would be won at once: they are left out
let shards = g.map.t.iter().filter(|t| t.kind == Kind::Shard).count() as u32;
let heart = g.gate_hp > 0.0 && g.gate_on;
let knight_towers = g.towers.iter().any(|t| t.hero);
let heroes = g.ents.iter().any(|e| e.alive && e.active_hero()) || doc.waves;
let goals: Vec<Goal> = crate::campaign_doc::parse_goals(&doc.goals)
.into_iter()
.filter(|goal| match goal {
Goal::Shards => shards > 0,
Goal::Heart => heart,
Goal::Towers => knight_towers,
Goal::KillAll => heroes,
Goal::Rivals => !g.rivals.is_empty(),
Goal::Survive(_) => doc.waves,
_ => true,
})
.collect();
g.mission = Some(MissionState {
index: i,
goals,
tools: vec![true; crate::ui::TOOLS.len()],
waves: doc.waves,
wave_limit: doc.wave_limit,
gate_defenders: doc.waves,
heart,
time_limit: doc.time_limit,
shards,
});
g.wave_timer = doc.first_wave;
g.gold = doc.gold;
if !doc.scripts.is_empty() {
g.script = Some(crate::mscript::ScriptState::new(doc.scripts.clone(), doc.zones.clone(), run_map_seed(run_seed, i)));
}
if !doc.title.is_empty() {
g.msg("msg.mission_start_custom", vec![doc.title.clone()]);
}
g
}
// a painted map: the game state saved by the map editor
pub fn load_fixed_map(json: &str) -> Option<Game> {
let mut g: Game = serde_json::from_str(json).ok()?;
g.map.activate();
g.recount();
Some(g)
}
// missions that can be played in co-op: they fit the multiplayer master areas and do not
// depend on rival masters as their only goal
pub fn is_coop_mission(i: usize) -> bool {