Script blocks for weather and talent experience, groves as ground; the co-op campaign brings rogues and a storm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e3158cc19
commit
eb219f6b01
23 files changed
+233
-8
No files matched your search
+26
-5
@@ -16,7 +16,7 @@ pub const KINDS: [&str; 30] = [
|
||||
];
|
||||
|
||||
// ground a Terrain block can make
|
||||
pub const TERRAIN: [&str; 6] = ["floor", "earth", "rock", "water", "lava", "gold"];
|
||||
pub const TERRAIN: [&str; 7] = ["floor", "earth", "rock", "water", "lava", "gold", "grove"];
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "b")]
|
||||
@@ -42,6 +42,8 @@ pub enum Block {
|
||||
Terrain { zone: String, kind: String },
|
||||
Kill { zone: String, side: i32 },
|
||||
Trigger { name: String, on: bool },
|
||||
Weather { kind: String, secs: Val }, // the weather above ground (secs 0: until changed again)
|
||||
TalentXp { amount: Val }, // experience for the player's talents
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -70,6 +72,7 @@ pub enum Val {
|
||||
Or { a: Box<Val>, b: Box<Val> },
|
||||
Not { a: Box<Val> },
|
||||
EventKind { kind: String }, // the unit that set off the trigger is of this kind
|
||||
WeatherIs { kind: String }, // the weather above ground is ...
|
||||
}
|
||||
|
||||
pub const MATH_OPS: [&str; 4] = ["+", "-", "*", "/"];
|
||||
@@ -89,7 +92,7 @@ impl Val {
|
||||
|
||||
pub fn ty(&self) -> Ty {
|
||||
match self {
|
||||
Val::Bool { .. } | Val::Cmp { .. } | Val::And { .. } | Val::Or { .. } | Val::Not { .. } | Val::EventKind { .. } => Ty::Bool,
|
||||
Val::Bool { .. } | Val::Cmp { .. } | Val::And { .. } | Val::Or { .. } | Val::Not { .. } | Val::EventKind { .. } | Val::WeatherIs { .. } => Ty::Bool,
|
||||
_ => Ty::Num,
|
||||
}
|
||||
}
|
||||
@@ -113,6 +116,7 @@ impl Val {
|
||||
"or" => Val::Or { a: Box::new(Val::Bool { b: true }), b: Box::new(Val::Bool { b: false }) },
|
||||
"not" => Val::Not { a: Box::new(Val::Bool { b: true }) },
|
||||
"eventkind" => Val::EventKind { kind: "Knight".into() },
|
||||
"weatheris" => Val::WeatherIs { kind: "rain".into() },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -135,16 +139,20 @@ impl Val {
|
||||
Val::Or { .. } => "or",
|
||||
Val::Not { .. } => "not",
|
||||
Val::EventKind { .. } => "eventkind",
|
||||
Val::WeatherIs { .. } => "weatheris",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// value kinds by type, for the value picker
|
||||
pub const NUM_VALUES: [&str; 9] = ["num", "var", "gold", "mana", "time", "creatures", "inzone", "random", "math"];
|
||||
pub const BOOL_VALUES: [&str; 6] = ["cmp", "and", "or", "not", "bool", "eventkind"];
|
||||
pub const BOOL_VALUES: [&str; 7] = ["cmp", "and", "or", "not", "bool", "eventkind", "weatheris"];
|
||||
|
||||
// the weather a block can set or ask for (weather.rs: FAIR, RAIN, FOG, STORM)
|
||||
pub const WEATHER: [&str; 4] = ["fair", "rain", "fog", "storm"];
|
||||
|
||||
// statement blocks in the palette: (category, kind)
|
||||
pub const PALETTE: [(&str, &str); 21] = [
|
||||
pub const PALETTE: [(&str, &str); 23] = [
|
||||
("say", "message"),
|
||||
("say", "objective"),
|
||||
("say", "hint"),
|
||||
@@ -156,6 +164,8 @@ pub const PALETTE: [(&str, &str); 21] = [
|
||||
("world", "camera"),
|
||||
("world", "gold"),
|
||||
("world", "mana"),
|
||||
("world", "weather"),
|
||||
("world", "talentxp"),
|
||||
("logic", "if"),
|
||||
("logic", "repeat"),
|
||||
("logic", "until"),
|
||||
@@ -185,6 +195,8 @@ impl Block {
|
||||
"camera" => Block::Camera { zone: String::new() },
|
||||
"gold" => Block::Gold { amount: n(500.0) },
|
||||
"mana" => Block::Mana { amount: n(100.0) },
|
||||
"weather" => Block::Weather { kind: "storm".into(), secs: n(0.0) },
|
||||
"talentxp" => Block::TalentXp { amount: n(300.0) },
|
||||
"if" => Block::If { cond: Val::template("cmp").unwrap(), then: Vec::new(), els: Vec::new() },
|
||||
"repeat" => Block::Repeat { times: n(3.0), body: Vec::new() },
|
||||
"until" => Block::Until { cond: Val::template("cmp").unwrap(), body: Vec::new() },
|
||||
@@ -212,6 +224,8 @@ impl Block {
|
||||
Block::Camera { .. } => "camera",
|
||||
Block::Gold { .. } => "gold",
|
||||
Block::Mana { .. } => "mana",
|
||||
Block::Weather { .. } => "weather",
|
||||
Block::TalentXp { .. } => "talentxp",
|
||||
Block::If { .. } => "if",
|
||||
Block::Repeat { .. } => "repeat",
|
||||
Block::Until { .. } => "until",
|
||||
@@ -258,6 +272,7 @@ fn check_val(v: &Val, want: Ty, n: &Names, path: &[usize], out: &mut Vec<Problem
|
||||
Val::Creatures { kind, .. } if !kind.is_empty() && !KINDS.contains(&kind.as_str()) => out.push(p("blk.err_kind")),
|
||||
Val::InZone { zone, .. } if !n.zones.iter().any(|z| z == zone) => out.push(p("blk.err_zone")),
|
||||
Val::EventKind { kind } if !KINDS.contains(&kind.as_str()) => out.push(p("blk.err_kind")),
|
||||
Val::WeatherIs { kind } if !WEATHER.contains(&kind.as_str()) => out.push(p("blk.err_weather")),
|
||||
Val::Random { a, b } | Val::Math { a, b, .. } => {
|
||||
check_val(a, Ty::Num, n, path, out);
|
||||
check_val(b, Ty::Num, n, path, out);
|
||||
@@ -295,7 +310,13 @@ fn check_list(list: &[Block], n: &Names, path: &mut Vec<usize>, out: &mut Vec<Pr
|
||||
}
|
||||
}
|
||||
Block::Wave { count } => check_val(count, Ty::Num, n, path, out),
|
||||
Block::Gold { amount } | Block::Mana { amount } => check_val(amount, Ty::Num, n, path, out),
|
||||
Block::Gold { amount } | Block::Mana { amount } | Block::TalentXp { amount } => check_val(amount, Ty::Num, n, path, out),
|
||||
Block::Weather { kind, secs } => {
|
||||
if !WEATHER.contains(&kind.as_str()) {
|
||||
out.push(p("blk.err_weather", path));
|
||||
}
|
||||
check_val(secs, Ty::Num, n, path, out);
|
||||
}
|
||||
Block::SetVar { name, value } | Block::AddVar { name, value } => {
|
||||
if !valid_var(name) {
|
||||
out.push(p("blk.err_var", path));
|
||||
|
||||
+21
-1
@@ -543,7 +543,13 @@ impl BlockEdit {
|
||||
let r = lay.take(CW() * 10.0);
|
||||
im.text_field(gfx, fid(path, 6), r, name, 16);
|
||||
}
|
||||
Block::Wave { count } | Block::Gold { amount: count } | Block::Mana { amount: count } => self.value(im, gfx, count, Ty::Num, path, 1, lay, ctx),
|
||||
Block::Wave { count } | Block::Gold { amount: count } | Block::Mana { amount: count } | Block::TalentXp { amount: count } => self.value(im, gfx, count, Ty::Num, path, 1, lay, ctx),
|
||||
Block::Weather { kind, secs } => {
|
||||
weather_dd(im, gfx, kind, fid(path, 2), lay);
|
||||
lay.word(gfx, &tr("blk.w_for"));
|
||||
self.value(im, gfx, secs, Ty::Num, path, 1, lay, ctx);
|
||||
lay.word(gfx, &tr("blk.w_seconds"));
|
||||
}
|
||||
Block::SetVar { name, value } | Block::AddVar { name, value } => {
|
||||
let r = lay.take(CW() * 12.0);
|
||||
im.text_field(gfx, fid(path, 0), r, name, 24);
|
||||
@@ -713,10 +719,24 @@ impl BlockEdit {
|
||||
lay.word(gfx, &tr("blk.v.eventkind"));
|
||||
self.kind_dd(im, gfx, kind, false, path, sub(0), lay);
|
||||
}
|
||||
Val::WeatherIs { kind } => {
|
||||
lay.word(gfx, &tr("blk.v.weatheris"));
|
||||
weather_dd(im, gfx, kind, fid(path, sub(0)), lay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a dropdown with the kinds of weather
|
||||
fn weather_dd(im: &mut Im, gfx: &Gfx, kind: &mut String, id: u64, lay: &mut Lay) {
|
||||
let opts: Vec<String> = WEATHER.iter().map(|w| tr(&format!("weather.{}", w))).collect();
|
||||
let cur = tr(&format!("weather.{}", kind));
|
||||
let r = lay.take(CW() * 12.0);
|
||||
if let Some(c) = im.dropdown(gfx, id, r, &cur, &opts) {
|
||||
*kind = WEATHER[c].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// where the next field of a block goes: left to right, into a new row at the block's edge
|
||||
struct Lay {
|
||||
x: f32,
|
||||
|
||||
@@ -143,6 +143,40 @@ pub fn coop_campaign() -> CampaignDoc {
|
||||
],
|
||||
});
|
||||
}
|
||||
// the rogues of the Order in the later missions: thieves from mission 7, an inquisitor
|
||||
// from mission 15; the last mission (above ground) brews a storm
|
||||
let lv = n((1 + i / 3) as f64);
|
||||
if i >= 6 {
|
||||
sc.triggers.push(TriggerDoc {
|
||||
name: "thieves".into(),
|
||||
kind: TriggerKind::Timer { secs: 420.0, every: false },
|
||||
once: true,
|
||||
enabled: true,
|
||||
script: vec![Block::Message { text: tr("coop.raid") }, Block::Spawn { side: 1, kind: "Thief".into(), count: n(2.0), level: lv.clone(), place: Place::Zone(ZONES[i % 3].0.into()), name: String::new() }],
|
||||
});
|
||||
}
|
||||
if i >= 14 {
|
||||
sc.triggers.push(TriggerDoc {
|
||||
name: "inquisitor".into(),
|
||||
kind: TriggerKind::Timer { secs: 600.0, every: false },
|
||||
once: true,
|
||||
enabled: true,
|
||||
script: vec![
|
||||
Block::Message { text: tr("coop.raid") },
|
||||
Block::Spawn { side: 1, kind: "Inquisitor".into(), count: n(1.0), level: lv.clone(), place: Place::Zone(ZONES[(i + 1) % 3].0.into()), name: String::new() },
|
||||
Block::Spawn { side: 1, kind: "Monk".into(), count: n(2.0), level: lv.clone(), place: Place::Zone(ZONES[(i + 1) % 3].0.into()), name: String::new() },
|
||||
],
|
||||
});
|
||||
}
|
||||
if i + 1 == specs.len() {
|
||||
sc.triggers.push(TriggerDoc {
|
||||
name: "storm".into(),
|
||||
kind: TriggerKind::Timer { secs: 480.0, every: false },
|
||||
once: true,
|
||||
enabled: true,
|
||||
script: vec![Block::Weather { kind: "storm".into(), secs: n(120.0) }],
|
||||
});
|
||||
}
|
||||
if let Some((kind, level, name, zone)) = sp.boss {
|
||||
sc.start.push(Block::Spawn { side: 1, kind: kind.into(), count: n(1.0), level: n(level as f64), place: Place::Zone(ZONES[zone].0.into()), name: name.into() });
|
||||
// the boss falls: the mission is won once the goals (if any) are done too
|
||||
|
||||
@@ -109,6 +109,8 @@ enum Act {
|
||||
Terrain(String, String),
|
||||
Kill(String, i32),
|
||||
Trigger(String, bool),
|
||||
Weather(u8, f64),
|
||||
TalentXp(f64),
|
||||
}
|
||||
|
||||
// the triggering unit (for "where the unit is" and "the unit is a ...")
|
||||
@@ -539,6 +541,12 @@ impl Game {
|
||||
Val::Or { a, b } => (self.cond(s, a, ev) || self.cond(s, b, ev)) as i32 as f64,
|
||||
Val::Not { a } => (!self.cond(s, a, ev)) as i32 as f64,
|
||||
Val::EventKind { kind } => (ev.kind.is_some() && ev.kind == kind_by_name(kind)) as i32 as f64,
|
||||
Val::WeatherIs { kind } => {
|
||||
// the weather felt now (a change counts once it has come in)
|
||||
let w = &self.weather;
|
||||
let now = if w.level > 0.5 { w.shown } else { crate::weather::FAIR };
|
||||
(crate::blocks::WEATHER.iter().position(|k| k == kind) == Some(now as usize)) as i32 as f64
|
||||
}
|
||||
};
|
||||
if r.is_finite() {
|
||||
r
|
||||
@@ -568,6 +576,8 @@ impl Game {
|
||||
Block::Wave { count } => Act::Wave(self.eval(s, count, ev).round() as i64),
|
||||
Block::Gold { amount } => Act::Gold(self.eval(s, amount, ev)),
|
||||
Block::Mana { amount } => Act::Mana(self.eval(s, amount, ev)),
|
||||
Block::Weather { kind, secs } => Act::Weather(crate::blocks::WEATHER.iter().position(|k| k == kind).unwrap_or(0) as u8, self.eval(s, secs, ev)),
|
||||
Block::TalentXp { amount } => Act::TalentXp(self.eval(s, amount, ev)),
|
||||
Block::SetVar { name, value } => {
|
||||
let v = self.eval(s, value, ev);
|
||||
s.vars.insert(name.clone(), v);
|
||||
@@ -824,6 +834,13 @@ impl Game {
|
||||
}
|
||||
}
|
||||
Act::Mana(n) => self.mana = (self.mana + n as f32).clamp(0.0, self.mana_max()),
|
||||
// the weather comes (softly) and stays the given seconds (0: until a block changes it)
|
||||
Act::Weather(k, secs) => {
|
||||
self.weather.kind = k;
|
||||
self.weather.left = if secs > 0.0 { secs as f32 } else { 1.0e9 };
|
||||
self.weather.strike_t = self.weather.strike_t.min(5.0);
|
||||
}
|
||||
Act::TalentXp(n) => self.add_master_xp(n.max(0.0) as f32),
|
||||
Act::Win => {
|
||||
self.state = 1;
|
||||
self.script_msg("msg.mission_won", vec![]);
|
||||
@@ -854,6 +871,7 @@ impl Game {
|
||||
"water" => Kind::Water,
|
||||
"lava" => Kind::Lava,
|
||||
"gold" => Kind::Gold,
|
||||
"grove" => Kind::Grove,
|
||||
_ => Kind::Floor,
|
||||
};
|
||||
for i in zone_tiles(&z) {
|
||||
@@ -960,6 +978,28 @@ mod tests {
|
||||
assert!(g.script.as_ref().unwrap().error.is_none(), "{:?}", g.script.as_ref().unwrap().error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weather_and_talent_blocks_work() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.add_surface(3, 0.5, 2);
|
||||
let scripts = MissionScripts {
|
||||
start: vec![
|
||||
Block::Weather { kind: "storm".into(), secs: Val::num(0.0) },
|
||||
Block::TalentXp { amount: Val::num(400.0) },
|
||||
Block::Wait { secs: Val::num(25.0) },
|
||||
Block::If { cond: Val::WeatherIs { kind: "storm".into() }, then: vec![Block::SetVar { name: "storm".into(), value: Val::num(1.0) }], els: vec![] },
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
g.script = Some(ScriptState::new(scripts, vec![], 7));
|
||||
run(&mut g, 30.0);
|
||||
assert_eq!(g.weather.kind, crate::weather::STORM);
|
||||
assert!(g.weather.left > 1.0e8, "it stays");
|
||||
assert!(g.talents.xp >= 400.0 && g.talents.free() >= 1);
|
||||
assert_eq!(g.script.as_ref().unwrap().vars.get("storm"), Some(&1.0), "the condition sees the storm");
|
||||
assert!(crate::blocks::check(&g.script.as_ref().unwrap().scripts.start, &crate::blocks::Names { zones: &[], triggers: &[], vars: &[] }).iter().all(|p| p.key != "blk.err_weather"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zone_trigger_spawns_and_the_victory_script_wins() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
|
||||
Reference in new issue
Block a user