Random events: earthquakes, overflowing rivers, new gold veins (free games and versus, can be switched off)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5239e4b5a3
commit
c6d82027dd
26 files changed
+313
-10
No files matched your search
+190
@@ -0,0 +1,190 @@
|
||||
// Random events in free games and versus matches (rare; can be switched off): an earthquake
|
||||
// brings down tunnels near the dungeon, lava or water overflows its banks, or the imps find a
|
||||
// new gold vein. Shared state and the shared random numbers: every client of a match sees the
|
||||
// same. Campaigns have none (their stories are told by the missions).
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// seconds between two events: EVENT_GAP plus up to EVENT_SPREAD
|
||||
pub const EVENT_GAP: f32 = 480.0;
|
||||
pub const EVENT_SPREAD: f32 = 240.0;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Events {
|
||||
pub t: f32, // seconds until the next one
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// switches random events on (new free game or match) or off
|
||||
pub fn set_events(&mut self, on: bool) {
|
||||
self.happenings = if on && self.mission.is_none() { Some(Events { t: EVENT_GAP + self.rng.f() * EVENT_SPREAD }) } else { None };
|
||||
}
|
||||
|
||||
pub(crate) fn update_events(&mut self, dt: f32) {
|
||||
let Some(ev) = self.happenings.as_mut() else { return };
|
||||
ev.t -= dt;
|
||||
if ev.t > 0.0 {
|
||||
return;
|
||||
}
|
||||
ev.t = EVENT_GAP + self.rng.f() * EVENT_SPREAD;
|
||||
let kind = self.rng.next() % 4;
|
||||
let happened = match kind {
|
||||
0 => self.event_quake(),
|
||||
1 => self.event_overflow(Kind::Lava),
|
||||
2 => self.event_overflow(Kind::Water),
|
||||
_ => self.event_vein(),
|
||||
};
|
||||
// nothing of that kind possible here: a gold vein instead (there is always earth)
|
||||
if !happened && kind != 3 {
|
||||
self.event_vein();
|
||||
}
|
||||
}
|
||||
|
||||
// own walkable ground within `r` of tile i?
|
||||
fn near_own(&self, i: usize, r: i32) -> bool {
|
||||
let (x, y) = xy(i);
|
||||
(-r..=r).any(|dy| (-r..=r).any(|dx| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner != Owner::None && t.owner != Owner::Hero && t.walkable())))
|
||||
}
|
||||
|
||||
fn announce(&mut self, key: &'static str, i: usize) {
|
||||
let (x, y) = xy(i);
|
||||
let dir = format!("@{}", self.direction_key((x, y)));
|
||||
// every master hears of it
|
||||
let me = self.me_side;
|
||||
for s in self.master_sides() {
|
||||
self.switch_to(s);
|
||||
self.msg(key, vec![dir.clone()]);
|
||||
self.sfx_ui(Sfx::CoreHit);
|
||||
}
|
||||
self.switch_to(me);
|
||||
self.add_mark(x as f32 + 0.5, y as f32 + 0.5, crate::game::MARK_BRIDGE, 255, 8.0, true);
|
||||
}
|
||||
|
||||
// an earthquake: plain tunnels (unowned, not rooms) near a dungeon cave in
|
||||
fn event_quake(&mut self) -> bool {
|
||||
let n = self.map.t.len();
|
||||
let cands: Vec<usize> = (0..n).filter(|&i| {
|
||||
let t = &self.map.t[i];
|
||||
let (x, y) = xy(i);
|
||||
t.kind == Kind::Floor && t.owner == Owner::None && x > 2 && y > 2 && x < mw() as i32 - 3 && y < mh() as i32 - 3 && !self.map.sky(i) && self.near_own(i, 6)
|
||||
}).collect();
|
||||
if cands.len() < 4 {
|
||||
return false;
|
||||
}
|
||||
let centre = cands[(self.rng.next() as usize) % cands.len()];
|
||||
let (cx, cy) = xy(centre);
|
||||
let mut fell = 0;
|
||||
for &i in &cands {
|
||||
let (x, y) = xy(i);
|
||||
if (x - cx).abs() + (y - cy).abs() > 3 || self.rng.f() < 0.35 {
|
||||
continue;
|
||||
}
|
||||
if self.ents.iter().any(|e| e.alive && e.tile() == i) {
|
||||
continue; // nobody is buried alive (the quake is not that mean)
|
||||
}
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = Kind::Earth;
|
||||
t.hp = 6.0;
|
||||
t.marked = false;
|
||||
fell += 1;
|
||||
}
|
||||
if fell == 0 {
|
||||
return false;
|
||||
}
|
||||
self.announce("msg.event_quake", centre);
|
||||
true
|
||||
}
|
||||
|
||||
// a river overflows its banks: unowned open ground next to it fills
|
||||
fn event_overflow(&mut self, river: Kind) -> bool {
|
||||
let n = self.map.t.len();
|
||||
let bank: Vec<usize> = (0..n).filter(|&i| {
|
||||
let t = &self.map.t[i];
|
||||
let (x, y) = xy(i);
|
||||
// no lava above ground
|
||||
t.kind == Kind::Floor && t.owner == Owner::None && !(river == Kind::Lava && self.map.sky(i))
|
||||
&& DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |o| o.kind == river))
|
||||
}).collect();
|
||||
if bank.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let start = bank[(self.rng.next() as usize) % bank.len()];
|
||||
let (sx, sy) = xy(start);
|
||||
let mut done = 0;
|
||||
for &i in &bank {
|
||||
let (x, y) = xy(i);
|
||||
if (x - sx).abs() + (y - sy).abs() > 4 || done >= 8 {
|
||||
continue;
|
||||
}
|
||||
self.map.t[i].kind = river;
|
||||
self.ground_lost(i);
|
||||
done += 1;
|
||||
}
|
||||
self.announce(if river == Kind::Lava { "msg.event_lava" } else { "msg.event_flood" }, start);
|
||||
true
|
||||
}
|
||||
|
||||
// the imps find a gold vein: earth near a dungeon turns to gold
|
||||
fn event_vein(&mut self) -> bool {
|
||||
let n = self.map.t.len();
|
||||
let cands: Vec<usize> = (0..n).filter(|&i| self.map.t[i].kind == Kind::Earth && !self.map.t[i].reinforced && self.near_own(i, 5)).collect();
|
||||
if cands.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let centre = cands[(self.rng.next() as usize) % cands.len()];
|
||||
let (cx, cy) = xy(centre);
|
||||
for &i in &cands {
|
||||
let (x, y) = xy(i);
|
||||
if (x - cx).abs() + (y - cy).abs() <= 2 {
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = Kind::Gold;
|
||||
t.gold = 900;
|
||||
}
|
||||
}
|
||||
self.announce("msg.event_vein", centre);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn free_games_have_rare_events() {
|
||||
let mut g = Game::new(12, 80, 64);
|
||||
g.set_events(true);
|
||||
assert!(g.happenings.is_some());
|
||||
let golds = |g: &Game| g.map.t.iter().filter(|t| t.kind == Kind::Gold).count();
|
||||
let before = golds(&g);
|
||||
g.happenings.as_mut().unwrap().t = 0.0;
|
||||
g.rng = Rng(3); // a gold vein (or another event) happens now
|
||||
g.update_events(0.1);
|
||||
assert!(g.happenings.as_ref().unwrap().t >= EVENT_GAP);
|
||||
assert!(g.msgs.iter().any(|m| m.key.starts_with("msg.event_")), "an event was told");
|
||||
let _ = before;
|
||||
// campaigns have none
|
||||
let mut m = crate::campaign::start_mission(3, None);
|
||||
m.set_events(true);
|
||||
assert!(m.happenings.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_event_can_happen() {
|
||||
let mut g = Game::new(12, 80, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
// a little lava and water next to unowned ground near the dungeon
|
||||
for (x, k) in [(cx + 6, Kind::Lava), (cx + 9, Kind::Water)] {
|
||||
for y in cy - 2..cy + 3 {
|
||||
g.map.t[idx(x, y)].kind = k;
|
||||
g.map.t[idx(x + 1, y)].kind = Kind::Floor;
|
||||
g.map.t[idx(x + 1, y)].owner = Owner::None;
|
||||
}
|
||||
}
|
||||
assert!(g.event_vein());
|
||||
assert!(g.event_overflow(Kind::Lava));
|
||||
assert!(g.event_overflow(Kind::Water));
|
||||
}
|
||||
}
|
||||
@@ -604,6 +604,8 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
|
||||
#[serde(default)]
|
||||
pub happenings: Option<crate::events::Events>, // random events (free games and versus), see events.rs
|
||||
#[serde(default)]
|
||||
pub stats: crate::stats::GameStats, // statistics of the master at this computer (see stats.rs)
|
||||
#[serde(default)]
|
||||
pub wall_plans: Vec<(usize, u8)>, // walls to raise: tile, master side (see walls.rs)
|
||||
@@ -943,6 +945,7 @@ impl Game {
|
||||
vrng: crate::particles::vrng(),
|
||||
dwarf_caves: Vec::new(),
|
||||
bridgework: Default::default(),
|
||||
happenings: None,
|
||||
stats: Default::default(),
|
||||
wall_plans: Vec::new(),
|
||||
wood: crate::wood::WOOD_START,
|
||||
@@ -1715,6 +1718,7 @@ impl Game {
|
||||
self.update_dwarves(dt);
|
||||
self.update_bridges(dt);
|
||||
self.update_wood(dt);
|
||||
self.update_events(dt);
|
||||
// fallen hearts of a match are shared state: every client marks them in the same step
|
||||
self.update_fallen_masters();
|
||||
// the end of the game and the fog are about the master at this computer
|
||||
|
||||
+8
-5
@@ -91,6 +91,7 @@ pub struct StartInfo {
|
||||
pub next: bool, // the next mission of a running editor campaign co-op match
|
||||
pub carry: Vec<(u8, crate::campaign::Carry)>, // what every side brings into that mission
|
||||
pub surface: bool, // a free match whose eastern half lies above ground
|
||||
pub events: bool, // random events in a free match
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -225,12 +226,13 @@ impl MatchClient {
|
||||
}
|
||||
|
||||
// a free match; `surface`: its eastern half lies above ground
|
||||
pub fn create_land(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, surface: bool) {
|
||||
pub fn create_land(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, surface: bool, events: bool) {
|
||||
let n = self.outbox.len();
|
||||
self.send_create(sides, map_w, map_h, difficulty, None);
|
||||
if let (true, Some(text)) = (surface, self.outbox.get_mut(n)) {
|
||||
if let (true, Some(text)) = (surface || events, self.outbox.get_mut(n)) {
|
||||
if let Ok(mut msg) = serde_json::from_str::<Value>(text) {
|
||||
msg["surface"] = json!(true);
|
||||
msg["surface"] = json!(surface);
|
||||
msg["events"] = json!(events);
|
||||
*text = msg.to_string();
|
||||
}
|
||||
}
|
||||
@@ -564,7 +566,8 @@ impl MatchClient {
|
||||
let resume = editor && v.get("resume").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
let carry: Vec<(u8, crate::campaign::Carry)> = if resume { v.get("carry").cloned().and_then(|c| serde_json::from_value(c).ok()).unwrap_or_default() } else { Vec::new() };
|
||||
let surface = v.get("surface").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry, surface });
|
||||
let events = v.get("events").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry, surface, events });
|
||||
self.error = None;
|
||||
self.status = None;
|
||||
}
|
||||
@@ -595,7 +598,7 @@ impl MatchClient {
|
||||
}
|
||||
r.ai = ai.clone();
|
||||
}
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission: Some(mission), ai, doc: Some(doc), editor: true, teams, next: true, carry, surface: false });
|
||||
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission: Some(mission), ai, doc: Some(doc), editor: true, teams, next: true, carry, surface: false, events: false });
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -8,6 +8,7 @@ mod cloud;
|
||||
mod commands;
|
||||
mod demo;
|
||||
mod editor;
|
||||
mod events;
|
||||
mod dwarves;
|
||||
mod fog;
|
||||
mod fortress;
|
||||
@@ -185,6 +186,7 @@ fn new_game(seed: u32, s: &Settings) -> (Game, Cam) {
|
||||
g.add_surface(seed, 0.5, 2 + (s.map_h / 64) as u32);
|
||||
g.init_fog();
|
||||
}
|
||||
g.set_events(s.events);
|
||||
let (cx, cy) = g.map.core;
|
||||
let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
|
||||
(g, cam)
|
||||
@@ -352,7 +354,7 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
||||
}
|
||||
}
|
||||
MenuAction::MatchJoin(code) => app.client.join(&code),
|
||||
MenuAction::MatchCreate => app.client.create_land(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty, app.settings.surface),
|
||||
MenuAction::MatchCreate => app.client.create_land(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty, app.settings.surface, app.settings.events),
|
||||
MenuAction::MatchCoopCreate(mission) => {
|
||||
let all = campaign::missions();
|
||||
if let Some(d) = all.get(mission) {
|
||||
@@ -721,7 +723,7 @@ impl App {
|
||||
let next = run_seed.is_some();
|
||||
let seed = run_seed.unwrap_or_else(|| next_seed(self.seed));
|
||||
let editor = doc.is_some();
|
||||
let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry, surface: false };
|
||||
let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry, surface: false, events: false };
|
||||
if !self.start_match_game(info) {
|
||||
return false;
|
||||
}
|
||||
@@ -805,6 +807,7 @@ impl App {
|
||||
// the same land on every client: built from the shared seed
|
||||
self.game.add_surface(info.seed, 0.5, 2 + (info.map_h / 64) as u32);
|
||||
}
|
||||
self.game.set_events(info.events);
|
||||
self.game.coop = false;
|
||||
self.game.set_teams(&info.teams);
|
||||
self.game.ai_difficulty = info.difficulty;
|
||||
|
||||
+5
-1
@@ -64,6 +64,7 @@ pub enum Item {
|
||||
DebugPathLines,
|
||||
GameSpeed,
|
||||
Hints,
|
||||
Events,
|
||||
TouchUi,
|
||||
Voice,
|
||||
}
|
||||
@@ -74,7 +75,7 @@ fn tab_items(tab: usize) -> &'static [Item] {
|
||||
0 => &[Language, Volume, MusicVolume, Mute, Voice],
|
||||
1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
|
||||
2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale],
|
||||
3 => &[GameSpeed, Hints, TouchUi],
|
||||
3 => &[GameSpeed, Hints, Events, TouchUi],
|
||||
4 => &[DebugFps, DebugFrameTime, DebugPing, DebugTriggerZones, DebugCommandMarkers, DebugPathLines],
|
||||
_ => &[],
|
||||
}
|
||||
@@ -104,6 +105,7 @@ fn item_label(it: Item) -> String {
|
||||
Item::DebugPathLines => "set.debug_path_lines",
|
||||
Item::GameSpeed => "set.game_speed",
|
||||
Item::Hints => "set.hints",
|
||||
Item::Events => "set.events",
|
||||
Item::TouchUi => "set.touch_ui",
|
||||
Item::Voice => "set.voice",
|
||||
})
|
||||
@@ -160,6 +162,7 @@ fn item_value(it: Item, s: &Settings) -> String {
|
||||
}
|
||||
Item::GameSpeed => format!("{}x", s.game_speed as i32),
|
||||
Item::Hints => onoff(s.hints),
|
||||
Item::Events => onoff(s.events),
|
||||
Item::TouchUi => match s.touch_ui {
|
||||
1 => tr("val.on"),
|
||||
2 => tr("val.off"),
|
||||
@@ -207,6 +210,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
|
||||
Item::DebugPathLines => s.debug_path_lines = !s.debug_path_lines,
|
||||
Item::GameSpeed => s.game_speed = cycle(&[1.0f32, 2.0], s.game_speed, d),
|
||||
Item::Hints => s.hints = !s.hints,
|
||||
Item::Events => s.events = !s.events,
|
||||
Item::TouchUi => s.touch_ui = cycle(&[0u8, 1, 2], s.touch_ui, d),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ pub struct Settings {
|
||||
pub map_w: usize, // size of new maps (64..256)
|
||||
pub map_h: usize,
|
||||
pub surface: bool, // new free games: the eastern half lies above ground (forest)
|
||||
pub events: bool, // random events in free games and versus matches
|
||||
// key bindings: action id -> key name
|
||||
pub keys: BTreeMap<String, String>,
|
||||
}
|
||||
@@ -198,6 +199,7 @@ impl Default for Settings {
|
||||
map_w: 64,
|
||||
map_h: 64,
|
||||
surface: false,
|
||||
events: true,
|
||||
keys: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user