Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion late-ssh/src/app/chat/svc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1618,7 +1618,7 @@ impl ChatService {

async fn ensure_room_scoped_command_access(
&self,
client: &Client,
client: &tokio_postgres::Client,
user_id: Uuid,
room_id: Uuid,
command: RoomScopedCommand,
Expand Down
5 changes: 4 additions & 1 deletion late-ssh/src/app/door/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ fn handle_active_lateania_key(app: &mut App, byte: u8) -> bool {
let Some(state) = app.lateania_state.as_mut() else {
return true;
};
let _ = super::lateania::input::handle_key(state, byte);
if super::lateania::input::handle_key(state, byte) == super::lateania::input::InputAction::Leave
{
app.leave_lateania();
}
true
}

Expand Down
12 changes: 12 additions & 0 deletions late-ssh/src/app/door/lateania/classes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ impl Class {
}
}

/// The ability score that sharpens this class's strikes (its attack key).
pub fn primary_score(self) -> super::stats::Score {
use super::stats::Score;
match self {
Self::Warrior => Score::Strength,
Self::Mage => Score::Intelligence,
Self::Cleric => Score::Wisdom,
Self::Rogue => Score::Dexterity,
Self::Ranger => Score::Dexterity,
}
}

pub fn resource(self) -> Resource {
match self {
Self::Warrior => Resource::Rage,
Expand Down
22 changes: 15 additions & 7 deletions late-ssh/src/app/door/lateania/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
//
// Key scheme:
// - Before choosing a class: 1-5 pick Warrior/Mage/Cleric/Rogue/Ranger.
// - Movement: w/a/s/d and arrows (N/S/E/W); y/u/b/n diagonals; < > up/down.
// - Movement: w/a/s/d and arrows (N/S/E/W); y/u/n/m diagonals; < or , up and
// > or . down (also shown as a hint in-game when a room has a vertical exit).
// - Combat: space/x attack; 1-9 use the ability in that action-bar slot; z flee.
// - Panels: c character, v abilities, o look, b shop, t inventory ("things").
// In a list panel, 1-9 select a row, Enter activates (equip/use/buy),
// w/s move the cursor, x sells (inventory).
// - Esc leaves the world and returns to the Door Games list.
// - Esc leaves the world for the Door Games lobby.
//
// A full typed command prompt needs an input-capture mode; deferred.

Expand All @@ -25,7 +26,7 @@ pub enum InputAction {
}

pub fn handle_key(state: &mut State, byte: u8) -> InputAction {
// Esc is reserved by the Door Games shell for returning to the lobby.
// Active Door games reserve Esc for returning to the Door Games lobby.
if byte == 0x1B {
return InputAction::Leave;
}
Expand All @@ -36,21 +37,23 @@ pub fn handle_key(state: &mut State, byte: u8) -> InputAction {
return InputAction::Handled;
}

// Class selection gate: until a class is chosen, 1-5 pick it and nothing else acts.
// Class selection gate: until a class is chosen, 1-5 pick it, r rerolls the
// ability scores, and nothing else acts.
if !view.classed {
match byte {
b'1' => state.choose_class(Class::Warrior),
b'2' => state.choose_class(Class::Mage),
b'3' => state.choose_class(Class::Cleric),
b'4' => state.choose_class(Class::Rogue),
b'5' => state.choose_class(Class::Ranger),
b'r' | b'R' => state.reroll(),
_ => return InputAction::Ignored,
}
return InputAction::Handled;
}

let panel = state.panel();
let in_list = matches!(panel, Panel::Inventory | Panel::Shop);
let in_list = matches!(panel, Panel::Inventory | Panel::Shop | Panel::Examine);

// Number keys: select a list row when a list panel is open, else use an ability.
if (b'1'..=b'9').contains(&byte) {
Expand Down Expand Up @@ -88,7 +91,9 @@ pub fn handle_key(state: &mut State, byte: u8) -> InputAction {
InputAction::Handled
}
b'o' | b'O' => {
state.set_panel(Panel::Room);
// Open the Examine list (the "look at things" panel) and refresh the
// room description in the log.
state.toggle_panel(Panel::Examine);
state.look();
InputAction::Handled
}
Expand Down Expand Up @@ -188,7 +193,10 @@ fn select_row(state: &mut State, target: usize) {
}

pub fn handle_arrow(state: &mut State, key: u8) -> bool {
let in_list = matches!(state.panel(), Panel::Inventory | Panel::Shop);
let in_list = matches!(
state.panel(),
Panel::Inventory | Panel::Shop | Panel::Examine
);
match key {
b'A' => {
if in_list {
Expand Down
1 change: 1 addition & 0 deletions late-ssh/src/app/door/lateania/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod input;
pub mod items;
pub mod persist;
pub mod state;
pub mod stats;
pub mod svc;
pub mod ui;
pub mod world;
28 changes: 27 additions & 1 deletion late-ssh/src/app/door/lateania/persist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::classes::Class;
use super::stats::AbilityScores;
use super::world::RoomId;

const SCHEMA_VERSION: u32 = 1;
const SCHEMA_VERSION: u32 = 3;
const WORLD_SCHEMA_VERSION: u32 = 1;

pub struct SavedCharacterInit {
Expand All @@ -25,8 +26,11 @@ pub struct SavedCharacterInit {
pub gold: i64,
pub hp: i32,
pub room: RoomId,
pub visited: Vec<RoomId>,
pub inventory: Vec<u32>,
pub equipped: Vec<(String, u32)>,
pub scores: AbilityScores,
pub titles: Vec<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand All @@ -48,11 +52,21 @@ pub struct SavedCharacter {
/// Room the character logged out in; reloaded here if it still exists.
#[serde(default = "start_room")]
pub room: RoomId,
/// Rooms the character has visited, for the overhead map. Empty for pre-v3
/// saves, which simply start the map from wherever they reload.
#[serde(default)]
pub visited: Vec<RoomId>,
#[serde(default)]
pub inventory: Vec<u32>,
/// Equipped items as (slot-key, item-id) pairs.
#[serde(default)]
pub equipped: Vec<(String, u32)>,
/// Rolled D&D ability scores; default (all 10s) for pre-v2 saves.
#[serde(default)]
pub scores: AbilityScores,
/// Titles earned by slaying notable foes (most recent last).
#[serde(default)]
pub titles: Vec<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -112,8 +126,11 @@ impl SavedCharacter {
gold: init.gold,
hp: init.hp,
room: init.room,
visited: init.visited,
inventory: init.inventory,
equipped: init.equipped,
scores: init.scores,
titles: init.titles,
}
}

Expand Down Expand Up @@ -168,24 +185,32 @@ mod tests {

#[test]
fn round_trips_through_json() {
let mut scores = AbilityScores::default();
scores.dexterity = 16;
let c = SavedCharacter::new_for(SavedCharacterInit {
class: Some(Class::Rogue),
xp: 1234,
level: 7,
gold: 560,
hp: 42,
room: 18,
visited: vec![1, 5, 18],
inventory: vec![1300, 1301],
equipped: vec![("weapon".to_string(), 1004)],
scores,
titles: vec!["Wyrmbane".to_string()],
});
let json = c.to_json();
let back = SavedCharacter::from_json(&json).expect("parses");
assert_eq!(back.class(), Some(Class::Rogue));
assert_eq!(back.xp, 1234);
assert_eq!(back.level, 7);
assert_eq!(back.gold, 560);
assert_eq!(back.visited, vec![1, 5, 18]);
assert_eq!(back.inventory, vec![1300, 1301]);
assert_eq!(back.equipped, vec![("weapon".to_string(), 1004)]);
assert_eq!(back.scores.dexterity, 16);
assert_eq!(back.titles, vec!["Wyrmbane".to_string()]);
}

#[test]
Expand All @@ -202,6 +227,7 @@ mod tests {
assert_eq!(c.class(), Some(Class::Mage));
assert_eq!(c.level, 1);
assert_eq!(c.room, 1);
assert!(c.visited.is_empty());
assert!(c.inventory.is_empty());
}

Expand Down
19 changes: 19 additions & 0 deletions late-ssh/src/app/door/lateania/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub enum Panel {
Abilities,
Inventory,
Shop,
/// Lookable things in the room: select one and press Enter to examine it
/// (and use it, for a fountain).
Examine,
}

pub struct State {
Expand Down Expand Up @@ -133,6 +136,7 @@ impl State {
match self.panel {
Panel::Inventory => self.view().inventory.len(),
Panel::Shop => self.view().shop.map(|s| s.entries.len()).unwrap_or(0),
Panel::Examine => self.view().features.len(),
_ => 0,
}
}
Expand Down Expand Up @@ -168,6 +172,20 @@ impl State {
}
}

/// Re-roll ability scores on the selection screen (before choosing a class).
pub fn reroll(&mut self) {
if self.ensure_player_present() {
self.svc.reroll_task(self.user_id);
}
}

/// Examine the selected lookable feature in the room.
pub fn examine_selection(&mut self) {
if self.panel == Panel::Examine && self.ensure_player_present() {
self.svc.interact_task(self.user_id, self.cursor);
}
}

pub fn attack(&mut self) {
if self.ensure_player_present() {
self.svc.attack_task(self.user_id);
Expand Down Expand Up @@ -220,6 +238,7 @@ impl State {
self.svc.buy_task(self.user_id, entry.item_id);
}
}
Panel::Examine => self.svc.interact_task(self.user_id, self.cursor),
_ => {}
}
}
Expand Down
Loading