diff --git a/mrpgi-core/src/state.rs b/mrpgi-core/src/state.rs index e0a7670..ec4b628 100644 --- a/mrpgi-core/src/state.rs +++ b/mrpgi-core/src/state.rs @@ -12,7 +12,6 @@ use crate::framebuffer::{CTRL_BLOCK, PIC_H, PIC_W}; use crate::parser::{self, Dlg, VerbDef}; use crate::render; use crate::sprite::{Prop, ScreenObj}; -use crate::view::default_robot_view; use crate::world::{find_spawn, ObjDef, RoomDoc, Stroke, World}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -147,7 +146,7 @@ pub struct GameState { impl GameState { pub fn new(world: World, ai: ai::AiConfig) -> Self { - let mut ego = ScreenObj::new(default_robot_view(), 80, 150); + let mut ego = ScreenObj::new(world.ego_view(), 80, 150); ego.min_x = 4; ego.max_x = PIC_W as i32 - 4; ego.min_y = 16; @@ -350,12 +349,14 @@ impl GameState { } Command::LoadGame { dir } => { self.world = World::load_game(dir); + self.ego.view = self.world.ego_view(); self.verbs = parser::build_verbs(&self.world.manifest.verbs); ev.push(Event::GameLoaded { name: self.world.manifest.name.clone(), room: self.world.current }); ev.extend(self.new_game(None)); } Command::ReloadSprites => { self.world.reload_sprites(); + self.ego.view = self.world.ego_view(); ev.push(self.edited()); } } @@ -459,7 +460,8 @@ impl GameState { for l in &cause_lines { ev.push(self.line(l)); } - ev.push(self.line("\u{2020} You have died. Morp is not cruel; Morp is accurate. The day restarts. \u{2020}")); + let death = self.world.manifest.death_text.clone(); + ev.push(self.line(&death)); ev } diff --git a/mrpgi-core/src/world.rs b/mrpgi-core/src/world.rs index 46b24f6..6fc45fc 100644 --- a/mrpgi-core/src/world.rs +++ b/mrpgi-core/src/world.rs @@ -203,11 +203,18 @@ pub struct GameManifest { /// Total points on offer; 0 = scoring off (no HUD line, no score events). #[serde(default)] pub max_score: u32, + /// The banner shown when the player dies and the day restarts. + #[serde(default = "default_death")] + pub death_text: String, } fn default_name() -> String { "untitled".into() } + +fn default_death() -> String { + "\u{2020} You have died. The day restarts. \u{2020}".into() +} fn default_intro() -> String { "You blink awake. Type 'help'.".into() } @@ -221,6 +228,7 @@ impl Default for GameManifest { verbs: Vec::new(), flags: HashMap::new(), max_score: 0, + death_text: default_death(), } } } @@ -486,6 +494,26 @@ impl World { self.bake(); } + /// The ego's view. A game folder overrides the default party robot by + /// dropping `sprites/ego.png` (feet on the bottom edge, like any prop); + /// `ego1.png` is an optional second cel for the walk bob. + pub fn ego_view(&self) -> view::View { + let cel = |f: &str| { + let p = self.base.join("sprites").join(f); + p.exists().then(|| assets::load_cel(&p.to_string_lossy(), false)) + }; + match cel("ego.png") { + Some(c0) => { + let cels = match cel("ego1.png") { + Some(c1) => vec![c0, c1], + None => vec![c0], + }; + view::View { loops: vec![view::ViewLoop { cels }] } + } + None => view::default_robot_view(), + } + } + /// Sanity-check a room before committing it (the lore-ingestion gate): /// exits must point at rooms that exist (on disk, in the overlay, or being /// upserted alongside), and the room must have somewhere to stand.