Feature: Shops, Economy and Enhanced NPC Interactions #2

Merged
lily merged 3 commits from feature/shops-and-interactions into main 2026-03-19 09:59:11 -06:00
3 changed files with 72 additions and 63 deletions
Showing only changes of commit 87baaee46f - Show all commits

View File

@@ -1476,11 +1476,8 @@ async fn cmd_spells(pid: usize, state: &SharedState) -> CommandResult {
async fn cmd_shop(pid: usize, args: &str, state: &SharedState) -> CommandResult { async fn cmd_shop(pid: usize, args: &str, state: &SharedState) -> CommandResult {
let mut st = state.lock().await; let mut st = state.lock().await;
let (conn, rid) = match st.players.get_mut(&pid) { let rid = match st.players.get(&pid) {
Some(c) => { Some(c) => c.player.room_id.clone(),
let rid = c.player.room_id.clone();
(c, rid)
}
None => return simple("Error\r\n"), None => return simple("Error\r\n"),
}; };
@@ -1543,42 +1540,48 @@ async fn cmd_shop(pid: usize, args: &str, state: &SharedState) -> CommandResult
if subargs.is_empty() { if subargs.is_empty() {
return simple("Buy what?\r\n"); return simple("Buy what?\r\n");
} }
let npc = st.world.get_npc(&merchant_id).unwrap(); let (shop, _npc_name) = {
let shop = npc.shop.as_ref().unwrap().clone(); let npc = st.world.get_npc(&merchant_id).unwrap();
(npc.shop.as_ref().unwrap().clone(), npc.name.clone())
};
let item_to_buy = shop.sells.iter().find(|id| { let item_id = shop.sells.iter().find(|id| {
if let Some(obj) = st.world.get_object(*id) { if let Some(obj) = st.world.get_object(*id) {
obj.name.to_lowercase().contains(&subargs.to_lowercase()) obj.name.to_lowercase().contains(&subargs.to_lowercase())
} else { } else {
false false
} }
}); }).cloned();
if let Some(item_id) = item_to_buy { if let Some(id) = item_id {
let obj = st.world.get_object(item_id).unwrap().clone(); let obj = st.world.get_object(&id).unwrap().clone();
let total_copper = (obj.value_gold * 10000 + obj.value_silver * 100 + obj.value_copper) as f32; let total_copper = (obj.value_gold * 10000 + obj.value_silver * 100 + obj.value_copper) as f32;
let price_copper = (total_copper * shop.markup).ceil() as i32; let price_copper = (total_copper * shop.markup).ceil() as i32;
let player_total_copper = conn.player.gold * 10000 + conn.player.silver * 100 + conn.player.copper; if let Some(conn) = st.players.get_mut(&pid) {
if player_total_copper < price_copper { let player_total_copper = conn.player.gold * 10000 + conn.player.silver * 100 + conn.player.copper;
return simple("You don't have enough money.\r\n"); if player_total_copper < price_copper {
return simple("You don't have enough money.\r\n");
}
// Deduct money
let mut remaining = player_total_copper - price_copper;
conn.player.gold = remaining / 10000;
remaining %= 10000;
conn.player.silver = remaining / 100;
conn.player.copper = remaining % 100;
// Add to inventory
conn.player.inventory.push(obj.clone());
simple(&format!(
"You buy {} for {} copper equivalents.\r\n",
ansi::color(ansi::CYAN, &obj.name),
price_copper
))
} else {
simple("Error\r\n")
} }
// Deduct money
let mut remaining = player_total_copper - price_copper;
conn.player.gold = remaining / 10000;
remaining %= 10000;
conn.player.silver = remaining / 100;
conn.player.copper = remaining % 100;
// Add to inventory
conn.player.inventory.push(obj.clone());
simple(&format!(
"You buy {} for {} copper equivalents.\r\n",
ansi::color(ansi::CYAN, &obj.name),
price_copper
))
} else { } else {
simple("The merchant doesn't sell that.\r\n") simple("The merchant doesn't sell that.\r\n")
} }
@@ -1588,14 +1591,16 @@ async fn cmd_shop(pid: usize, args: &str, state: &SharedState) -> CommandResult
if subargs.is_empty() { if subargs.is_empty() {
return simple("Sell what?\r\n"); return simple("Sell what?\r\n");
} }
let npc = st.world.get_npc(&merchant_id).unwrap(); let shop = st.world.get_npc(&merchant_id).unwrap().shop.as_ref().unwrap().clone();
let shop = npc.shop.as_ref().unwrap().clone();
let item_idx = conn.player.inventory.iter().position(|o| o.name.to_lowercase().contains(&subargs.to_lowercase())); let item_info = if let Some(conn) = st.players.get(&pid) {
conn.player.inventory.iter().position(|o| o.name.to_lowercase().contains(&subargs.to_lowercase()))
.map(|idx| (idx, conn.player.inventory[idx].clone()))
} else {
None
};
if let Some(idx) = item_idx { if let Some((idx, obj)) = item_info {
let obj = conn.player.inventory[idx].clone();
// Check if merchant buys this kind of item // Check if merchant buys this kind of item
let can_sell = shop.buys.is_empty() || shop.buys.iter().any(|k| { let can_sell = shop.buys.is_empty() || shop.buys.iter().any(|k| {
if let Some(kind) = &obj.kind { if let Some(kind) = &obj.kind {
@@ -1612,22 +1617,26 @@ async fn cmd_shop(pid: usize, args: &str, state: &SharedState) -> CommandResult
let total_copper = (obj.value_gold * 10000 + obj.value_silver * 100 + obj.value_copper) as f32; let total_copper = (obj.value_gold * 10000 + obj.value_silver * 100 + obj.value_copper) as f32;
let price_copper = (total_copper * shop.markdown).floor() as i32; let price_copper = (total_copper * shop.markdown).floor() as i32;
// Add money to player if let Some(conn) = st.players.get_mut(&pid) {
let mut player_total_copper = conn.player.gold * 10000 + conn.player.silver * 100 + conn.player.copper; // Add money to player
player_total_copper += price_copper; let mut player_total_copper = conn.player.gold * 10000 + conn.player.silver * 100 + conn.player.copper;
conn.player.gold = player_total_copper / 10000; player_total_copper += price_copper;
player_total_copper %= 10000; conn.player.gold = player_total_copper / 10000;
conn.player.silver = player_total_copper / 100; player_total_copper %= 10000;
conn.player.copper = player_total_copper % 100; conn.player.silver = player_total_copper / 100;
conn.player.copper = player_total_copper % 100;
// Remove from inventory // Remove from inventory
conn.player.inventory.remove(idx); conn.player.inventory.remove(idx);
simple(&format!( simple(&format!(
"You sell {} for {} copper equivalents.\r\n", "You sell {} for {} copper equivalents.\r\n",
ansi::color(ansi::CYAN, &obj.name), ansi::color(ansi::CYAN, &obj.name),
price_copper price_copper
)) ))
} else {
simple("Error\r\n")
}
} else { } else {
simple("You don't have that in your inventory.\r\n") simple("You don't have that in your inventory.\r\n")
} }

View File

@@ -78,7 +78,7 @@ impl SqliteDb {
.map_err(|e| format!("Failed to set pragmas: {e}"))?; .map_err(|e| format!("Failed to set pragmas: {e}"))?;
conn.execute_batch( conn.execute_batch(
CREATE TABLE IF NOT EXISTS players ( r#"CREATE TABLE IF NOT EXISTS players (
name TEXT PRIMARY KEY, name TEXT PRIMARY KEY,
race_id TEXT NOT NULL, race_id TEXT NOT NULL,
class_id TEXT NOT NULL, class_id TEXT NOT NULL,
@@ -89,8 +89,8 @@ impl SqliteDb {
max_hp INTEGER NOT NULL, max_hp INTEGER NOT NULL,
attack INTEGER NOT NULL, attack INTEGER NOT NULL,
defense INTEGER NOT NULL, defense INTEGER NOT NULL,
inventory_json TEXT NOT NULL DEFAULT '[]', inventory_json TEXT NOT NULL DEFAULT "[]",
equipped_json TEXT NOT NULL DEFAULT '{}', equipped_json TEXT NOT NULL DEFAULT "{}",
is_admin INTEGER NOT NULL DEFAULT 0, is_admin INTEGER NOT NULL DEFAULT 0,
mana INTEGER NOT NULL DEFAULT 0, mana INTEGER NOT NULL DEFAULT 0,
max_mana INTEGER NOT NULL DEFAULT 0, max_mana INTEGER NOT NULL DEFAULT 0,
@@ -127,13 +127,13 @@ impl SqliteDb {
guild_id TEXT NOT NULL, guild_id TEXT NOT NULL,
level INTEGER NOT NULL DEFAULT 1, level INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (player_name, guild_id) PRIMARY KEY (player_name, guild_id)
);", );"#,
) )
.map_err(|e| format!("Failed to create tables: {e}"))?; .map_err(|e| format!("Failed to create tables: {e}"))?;
// Migration: add is_admin column if missing // Migration: add is_admin column if missing
let has_admin: bool = conn let has_admin: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('players') WHERE name='is_admin'") .prepare(r#"SELECT COUNT(*) FROM pragma_table_info("players") WHERE name="is_admin""#)
.and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0))) .and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0)))
.map(|c| c > 0) .map(|c| c > 0)
.unwrap_or(false); .unwrap_or(false);
@@ -146,34 +146,34 @@ impl SqliteDb {
// Migration: equipped_weapon_json/equipped_armor_json -> equipped_json // Migration: equipped_weapon_json/equipped_armor_json -> equipped_json
let has_old_weapon: bool = conn let has_old_weapon: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('players') WHERE name='equipped_weapon_json'") .prepare(r#"SELECT COUNT(*) FROM pragma_table_info("players") WHERE name="equipped_weapon_json""#)
.and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0))) .and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0)))
.map(|c| c > 0) .map(|c| c > 0)
.unwrap_or(false); .unwrap_or(false);
let has_equipped: bool = conn let has_equipped: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('players') WHERE name='equipped_json'") .prepare(r#"SELECT COUNT(*) FROM pragma_table_info("players") WHERE name="equipped_json""#)
.and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0))) .and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0)))
.map(|c| c > 0) .map(|c| c > 0)
.unwrap_or(false); .unwrap_or(false);
if has_old_weapon && !has_equipped { if has_old_weapon && !has_equipped {
let _ = conn.execute( let _ = conn.execute(
"ALTER TABLE players ADD COLUMN equipped_json TEXT NOT NULL DEFAULT '{}'", r#"ALTER TABLE players ADD COLUMN equipped_json TEXT NOT NULL DEFAULT "{}"#,
[], [],
); );
log::info!("Migrating equipped_weapon_json/equipped_armor_json to equipped_json..."); log::info!("Migrating equipped_weapon_json/equipped_armor_json to equipped_json...");
let _ = conn.execute_batch( let _ = conn.execute_batch(
"UPDATE players SET equipped_json = '{}' WHERE equipped_weapon_json IS NULL AND equipped_armor_json IS NULL;" r#"UPDATE players SET equipped_json = "{}" WHERE equipped_weapon_json IS NULL AND equipped_armor_json IS NULL;"#
); );
} else if !has_equipped { } else if !has_equipped {
let _ = conn.execute( let _ = conn.execute(
"ALTER TABLE players ADD COLUMN equipped_json TEXT NOT NULL DEFAULT '{}'", r#"ALTER TABLE players ADD COLUMN equipped_json TEXT NOT NULL DEFAULT "{}"#,
[], [],
); );
} }
// Migration: add mana/endurance columns // Migration: add mana/endurance columns
let has_mana: bool = conn let has_mana: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('players') WHERE name='mana'") .prepare(r#"SELECT COUNT(*) FROM pragma_table_info("players") WHERE name="mana""#)
.and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0))) .and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0)))
.map(|c| c > 0) .map(|c| c > 0)
.unwrap_or(false); .unwrap_or(false);
@@ -186,7 +186,7 @@ impl SqliteDb {
// Migration: add currency columns // Migration: add currency columns
let has_gold: bool = conn let has_gold: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('players') WHERE name='gold'") .prepare(r#"SELECT COUNT(*) FROM pragma_table_info("players") WHERE name="gold""#)
.and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0))) .and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0)))
.map(|c| c > 0) .map(|c| c > 0)
.unwrap_or(false); .unwrap_or(false);

View File

@@ -2,7 +2,7 @@ name = "Breda"
description = "Breda is haughty in bearing, with thin auburn hair and narrow blue eyes. She wears simple clothing and several small tools hang from her belt. Breda will purchase monster teeth for a silver coin each." description = "Breda is haughty in bearing, with thin auburn hair and narrow blue eyes. She wears simple clothing and several small tools hang from her belt. Breda will purchase monster teeth for a silver coin each."
room = "lawold:well_market_trade_stalls" room = "lawold:well_market_trade_stalls"
race = "race:human" race = "race:human"
base_attitude = "aggressive" base_attitude = "neutral"
[dialogue] [dialogue]
greeting = "What do you want? I'm busy. Unless you have some teeth to sell?" greeting = "What do you want? I'm busy. Unless you have some teeth to sell?"