Implement currency, shops, and enhanced NPC interaction system
This commit is contained in:
@@ -76,17 +76,24 @@ pub fn resolve_combat_tick(
|
|||||||
}
|
}
|
||||||
npc_died = true;
|
npc_died = true;
|
||||||
xp_gained = npc_combat.xp_reward;
|
xp_gained = npc_combat.xp_reward;
|
||||||
|
let gold_gained = npc_template.gold;
|
||||||
|
let silver_gained = npc_template.silver;
|
||||||
|
let copper_gained = npc_template.copper;
|
||||||
|
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
" {} {} collapses! You gain {} XP.\r\n",
|
" {} {} collapses! You gain {} XP and {}g {}s {}c.\r\n",
|
||||||
ansi::color(ansi::GREEN, "**"),
|
ansi::color(ansi::GREEN, "**"),
|
||||||
ansi::color(ansi::RED, &npc_template.name),
|
ansi::color(ansi::RED, &npc_template.name),
|
||||||
ansi::bold(&xp_gained.to_string()),
|
ansi::bold(&xp_gained.to_string()),
|
||||||
|
gold_gained, silver_gained, copper_gained
|
||||||
));
|
));
|
||||||
|
|
||||||
if let Some(conn) = state.players.get_mut(&player_id) {
|
if let Some(conn) = state.players.get_mut(&player_id) {
|
||||||
conn.combat = None;
|
conn.combat = None;
|
||||||
conn.player.stats.xp += xp_gained;
|
conn.player.stats.xp += xp_gained;
|
||||||
|
conn.player.gold += gold_gained;
|
||||||
|
conn.player.silver += silver_gained;
|
||||||
|
conn.player.copper += copper_gained;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(inst) = state.npc_instances.get_mut(&npc_id) {
|
if let Some(inst) = state.npc_instances.get_mut(&npc_id) {
|
||||||
|
|||||||
232
src/commands.rs
232
src/commands.rs
@@ -134,6 +134,7 @@ pub async fn execute(
|
|||||||
"spells" | "skills" => cmd_spells(player_id, state).await,
|
"spells" | "skills" => cmd_spells(player_id, state).await,
|
||||||
"guild" => cmd_guild(player_id, &args, state).await,
|
"guild" => cmd_guild(player_id, &args, state).await,
|
||||||
"stats" | "st" => cmd_stats(player_id, state).await,
|
"stats" | "st" => cmd_stats(player_id, state).await,
|
||||||
|
"shop" => cmd_shop(player_id, &args, state).await,
|
||||||
"admin" => cmd_admin(player_id, &args, state).await,
|
"admin" => cmd_admin(player_id, &args, state).await,
|
||||||
"help" | "h" | "?" => cmd_help(player_id, state).await,
|
"help" | "h" | "?" => cmd_help(player_id, state).await,
|
||||||
"quit" | "exit" => CommandResult {
|
"quit" | "exit" => CommandResult {
|
||||||
@@ -999,9 +1000,9 @@ async fn cmd_examine(pid: usize, target: &str, state: &SharedState) -> CommandRe
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cmd_talk(pid: usize, target: &str, state: &SharedState) -> CommandResult {
|
async fn cmd_talk(pid: usize, input: &str, state: &SharedState) -> CommandResult {
|
||||||
if target.is_empty() {
|
if input.is_empty() {
|
||||||
return simple("Talk to whom?\r\n");
|
return simple("Talk to whom? (Usage: talk <npc> [keyword])\r\n");
|
||||||
}
|
}
|
||||||
let st = state.lock().await;
|
let st = state.lock().await;
|
||||||
let conn = match st.players.get(&pid) {
|
let conn = match st.players.get(&pid) {
|
||||||
@@ -1012,12 +1013,17 @@ async fn cmd_talk(pid: usize, target: &str, state: &SharedState) -> CommandResul
|
|||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
None => return simple("Void\r\n"),
|
None => return simple("Void\r\n"),
|
||||||
};
|
};
|
||||||
let low = target.to_lowercase();
|
|
||||||
|
let (target, keyword) = match input.split_once(' ') {
|
||||||
|
Some((t, k)) => (t.to_lowercase(), k.trim().to_lowercase()),
|
||||||
|
None => (input.to_lowercase(), String::new()),
|
||||||
|
};
|
||||||
|
|
||||||
let pname = &conn.player.name;
|
let pname = &conn.player.name;
|
||||||
|
|
||||||
for nid in &room.npcs {
|
for nid in &room.npcs {
|
||||||
if let Some(npc) = st.world.get_npc(nid) {
|
if let Some(npc) = st.world.get_npc(nid) {
|
||||||
if npc.name.to_lowercase().contains(&low) {
|
if npc.name.to_lowercase().contains(&target) {
|
||||||
if !st.npc_instances.get(nid).map(|i| i.alive).unwrap_or(true) {
|
if !st.npc_instances.get(nid).map(|i| i.alive).unwrap_or(true) {
|
||||||
return simple(&format!(
|
return simple(&format!(
|
||||||
"{}\r\n",
|
"{}\r\n",
|
||||||
@@ -1031,17 +1037,58 @@ async fn cmd_talk(pid: usize, target: &str, state: &SharedState) -> CommandResul
|
|||||||
ansi::color(ansi::RED, &npc.name)
|
ansi::color(ansi::RED, &npc.name)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let greeting = npc.greeting.as_deref().unwrap_or("...");
|
|
||||||
|
if !keyword.is_empty() {
|
||||||
|
if let Some(response) = npc.keywords.get(&keyword) {
|
||||||
return CommandResult {
|
return CommandResult {
|
||||||
output: format!(
|
output: format!(
|
||||||
"\r\n{} says: \"{}\"\r\n",
|
"\r\n{} says: \"{}\"\r\n",
|
||||||
ansi::color(ansi::YELLOW, &npc.name),
|
ansi::color(ansi::YELLOW, &npc.name),
|
||||||
ansi::color(ansi::WHITE, greeting)
|
ansi::color(ansi::WHITE, response)
|
||||||
),
|
),
|
||||||
broadcasts: Vec::new(),
|
broadcasts: Vec::new(),
|
||||||
kick_targets: Vec::new(),
|
kick_targets: Vec::new(),
|
||||||
quit: false,
|
quit: false,
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
return simple(&format!(
|
||||||
|
"{} looks at you blankly, not understanding '{}'.\r\n",
|
||||||
|
ansi::color(ansi::YELLOW, &npc.name),
|
||||||
|
keyword
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let greeting = npc.greeting.as_deref().unwrap_or("...");
|
||||||
|
let mut output = format!(
|
||||||
|
"\r\n{} says: \"{}\"\r\n",
|
||||||
|
ansi::color(ansi::YELLOW, &npc.name),
|
||||||
|
ansi::color(ansi::WHITE, greeting)
|
||||||
|
);
|
||||||
|
|
||||||
|
if !npc.keywords.is_empty() {
|
||||||
|
let mut keys: Vec<_> = npc.keywords.keys().cloned().collect();
|
||||||
|
keys.sort();
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {} {}\r\n",
|
||||||
|
ansi::color(ansi::DIM, "You can ask about:"),
|
||||||
|
keys.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if npc.shop.is_some() {
|
||||||
|
output.push_str(&format!(
|
||||||
|
" {}\r\n",
|
||||||
|
ansi::color(ansi::CYAN, "This person appears to be a merchant. Try 'shop list'.")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult {
|
||||||
|
output,
|
||||||
|
broadcasts: Vec::new(),
|
||||||
|
kick_targets: Vec::new(),
|
||||||
|
quit: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1427,6 +1474,169 @@ async fn cmd_spells(pid: usize, state: &SharedState) -> CommandResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cmd_shop(pid: usize, args: &str, state: &SharedState) -> CommandResult {
|
||||||
|
let mut st = state.lock().await;
|
||||||
|
let (conn, rid) = match st.players.get_mut(&pid) {
|
||||||
|
Some(c) => {
|
||||||
|
let rid = c.player.room_id.clone();
|
||||||
|
(c, rid)
|
||||||
|
}
|
||||||
|
None => return simple("Error\r\n"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find a merchant in the room
|
||||||
|
let mut merchant_id = None;
|
||||||
|
if let Some(room) = st.world.get_room(&rid) {
|
||||||
|
for nid in &room.npcs {
|
||||||
|
if let Some(npc) = st.world.get_npc(nid) {
|
||||||
|
if npc.shop.is_some() {
|
||||||
|
merchant_id = Some(nid.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let merchant_id = match merchant_id {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return simple("There is no merchant here.\r\n"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (subcmd, subargs) = match args.split_once(' ') {
|
||||||
|
Some((c, a)) => (c.to_lowercase(), a.trim()),
|
||||||
|
None => (args.to_lowercase(), ""),
|
||||||
|
};
|
||||||
|
|
||||||
|
match subcmd.as_str() {
|
||||||
|
"list" | "ls" | "" => {
|
||||||
|
let npc = st.world.get_npc(&merchant_id).unwrap();
|
||||||
|
let shop = npc.shop.as_ref().unwrap();
|
||||||
|
let mut out = format!(
|
||||||
|
"\r\n{}'s Shop Inventory (Markup: x{:.1})\r\n",
|
||||||
|
ansi::bold(&npc.name),
|
||||||
|
shop.markup
|
||||||
|
);
|
||||||
|
|
||||||
|
if shop.sells.is_empty() {
|
||||||
|
out.push_str(" (nothing for sale)\r\n");
|
||||||
|
} else {
|
||||||
|
for item_id in &shop.sells {
|
||||||
|
if let Some(obj) = st.world.get_object(item_id) {
|
||||||
|
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 g = price_copper / 10000;
|
||||||
|
let s = (price_copper % 10000) / 100;
|
||||||
|
let c = price_copper % 100;
|
||||||
|
|
||||||
|
out.push_str(&format!(
|
||||||
|
" - {} [{}g {}s {}c]\r\n",
|
||||||
|
ansi::color(ansi::CYAN, &obj.name),
|
||||||
|
g, s, c
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
simple(&out)
|
||||||
|
}
|
||||||
|
|
||||||
|
"buy" => {
|
||||||
|
if subargs.is_empty() {
|
||||||
|
return simple("Buy what?\r\n");
|
||||||
|
}
|
||||||
|
let npc = st.world.get_npc(&merchant_id).unwrap();
|
||||||
|
let shop = npc.shop.as_ref().unwrap().clone();
|
||||||
|
|
||||||
|
let item_to_buy = shop.sells.iter().find(|id| {
|
||||||
|
if let Some(obj) = st.world.get_object(*id) {
|
||||||
|
obj.name.to_lowercase().contains(&subargs.to_lowercase())
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(item_id) = item_to_buy {
|
||||||
|
let obj = st.world.get_object(item_id).unwrap().clone();
|
||||||
|
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 player_total_copper = conn.player.gold * 10000 + conn.player.silver * 100 + conn.player.copper;
|
||||||
|
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("The merchant doesn't sell that.\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"sell" => {
|
||||||
|
if subargs.is_empty() {
|
||||||
|
return simple("Sell what?\r\n");
|
||||||
|
}
|
||||||
|
let npc = st.world.get_npc(&merchant_id).unwrap();
|
||||||
|
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()));
|
||||||
|
|
||||||
|
if let Some(idx) = item_idx {
|
||||||
|
let obj = conn.player.inventory[idx].clone();
|
||||||
|
|
||||||
|
// Check if merchant buys this kind of item
|
||||||
|
let can_sell = shop.buys.is_empty() || shop.buys.iter().any(|k| {
|
||||||
|
if let Some(kind) = &obj.kind {
|
||||||
|
kind.to_lowercase() == k.to_lowercase()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !can_sell {
|
||||||
|
return simple("The merchant isn't interested in that kind of item.\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Add money to player
|
||||||
|
let mut player_total_copper = conn.player.gold * 10000 + conn.player.silver * 100 + conn.player.copper;
|
||||||
|
player_total_copper += price_copper;
|
||||||
|
conn.player.gold = player_total_copper / 10000;
|
||||||
|
player_total_copper %= 10000;
|
||||||
|
conn.player.silver = player_total_copper / 100;
|
||||||
|
conn.player.copper = player_total_copper % 100;
|
||||||
|
|
||||||
|
// Remove from inventory
|
||||||
|
conn.player.inventory.remove(idx);
|
||||||
|
|
||||||
|
simple(&format!(
|
||||||
|
"You sell {} for {} copper equivalents.\r\n",
|
||||||
|
ansi::color(ansi::CYAN, &obj.name),
|
||||||
|
price_copper
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
simple("You don't have that in your inventory.\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => simple("Usage: shop list | shop buy <item> | shop sell <item>\r\n"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn cmd_guild(pid: usize, args: &str, state: &SharedState) -> CommandResult {
|
async fn cmd_guild(pid: usize, args: &str, state: &SharedState) -> CommandResult {
|
||||||
let (subcmd, subargs) = match args.split_once(' ') {
|
let (subcmd, subargs) = match args.split_once(' ') {
|
||||||
Some((c, a)) => (c.to_lowercase(), a.trim().to_string()),
|
Some((c, a)) => (c.to_lowercase(), a.trim().to_string()),
|
||||||
@@ -1665,6 +1875,14 @@ async fn cmd_stats(pid: usize, state: &SharedState) -> CommandResult {
|
|||||||
s.xp,
|
s.xp,
|
||||||
s.xp_to_next
|
s.xp_to_next
|
||||||
));
|
));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" {} {}{}g {}{}s {}{}c{}\r\n",
|
||||||
|
ansi::color(ansi::DIM, "Money:"),
|
||||||
|
ansi::YELLOW, p.gold,
|
||||||
|
ansi::WHITE, p.silver,
|
||||||
|
ansi::RED, p.copper,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
if !p.guilds.is_empty() {
|
if !p.guilds.is_empty() {
|
||||||
out.push_str(&format!(" {}\r\n", ansi::color(ansi::DIM, "Guilds:")));
|
out.push_str(&format!(" {}\r\n", ansi::color(ansi::DIM, "Guilds:")));
|
||||||
let mut guild_list: Vec<_> = p.guilds.iter().collect();
|
let mut guild_list: Vec<_> = p.guilds.iter().collect();
|
||||||
|
|||||||
45
src/db.rs
45
src/db.rs
@@ -20,6 +20,9 @@ pub struct SavedPlayer {
|
|||||||
pub endurance: i32,
|
pub endurance: i32,
|
||||||
pub max_endurance: i32,
|
pub max_endurance: i32,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
|
pub gold: i32,
|
||||||
|
pub silver: i32,
|
||||||
|
pub copper: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NpcAttitudeRow {
|
pub struct NpcAttitudeRow {
|
||||||
@@ -75,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 (
|
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,
|
||||||
@@ -88,9 +91,17 @@ impl SqliteDb {
|
|||||||
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,
|
||||||
|
max_mana INTEGER NOT NULL DEFAULT 0,
|
||||||
|
endurance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_endurance INTEGER NOT NULL DEFAULT 0,
|
||||||
|
gold INTEGER NOT NULL DEFAULT 0,
|
||||||
|
silver INTEGER NOT NULL DEFAULT 0,
|
||||||
|
copper INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS npc_attitudes (
|
CREATE TABLE IF NOT EXISTS npc_attitudes (
|
||||||
player_name TEXT NOT NULL,
|
player_name TEXT NOT NULL,
|
||||||
npc_id TEXT NOT NULL,
|
npc_id TEXT NOT NULL,
|
||||||
@@ -173,6 +184,18 @@ impl SqliteDb {
|
|||||||
let _ = conn.execute("ALTER TABLE players ADD COLUMN max_endurance INTEGER NOT NULL DEFAULT 0", []);
|
let _ = conn.execute("ALTER TABLE players ADD COLUMN max_endurance INTEGER NOT NULL DEFAULT 0", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: add currency columns
|
||||||
|
let has_gold: bool = conn
|
||||||
|
.prepare("SELECT COUNT(*) FROM pragma_table_info('players') WHERE name='gold'")
|
||||||
|
.and_then(|mut s| s.query_row([], |r| r.get::<_, i32>(0)))
|
||||||
|
.map(|c| c > 0)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !has_gold {
|
||||||
|
let _ = conn.execute("ALTER TABLE players ADD COLUMN gold INTEGER NOT NULL DEFAULT 0", []);
|
||||||
|
let _ = conn.execute("ALTER TABLE players ADD COLUMN silver INTEGER NOT NULL DEFAULT 0", []);
|
||||||
|
let _ = conn.execute("ALTER TABLE players ADD COLUMN copper INTEGER NOT NULL DEFAULT 0", []);
|
||||||
|
}
|
||||||
|
|
||||||
log::info!("Database opened: {}", path.display());
|
log::info!("Database opened: {}", path.display());
|
||||||
Ok(SqliteDb {
|
Ok(SqliteDb {
|
||||||
conn: std::sync::Mutex::new(conn),
|
conn: std::sync::Mutex::new(conn),
|
||||||
@@ -186,7 +209,7 @@ impl GameDb for SqliteDb {
|
|||||||
conn.query_row(
|
conn.query_row(
|
||||||
"SELECT name, race_id, class_id, room_id, level, xp, hp, max_hp,
|
"SELECT name, race_id, class_id, room_id, level, xp, hp, max_hp,
|
||||||
attack, defense, inventory_json, equipped_json, is_admin,
|
attack, defense, inventory_json, equipped_json, is_admin,
|
||||||
mana, max_mana, endurance, max_endurance
|
mana, max_mana, endurance, max_endurance, gold, silver, copper
|
||||||
FROM players WHERE name = ?1",
|
FROM players WHERE name = ?1",
|
||||||
[name],
|
[name],
|
||||||
|row| {
|
|row| {
|
||||||
@@ -208,6 +231,9 @@ impl GameDb for SqliteDb {
|
|||||||
max_mana: row.get::<_, i32>(14).unwrap_or(0),
|
max_mana: row.get::<_, i32>(14).unwrap_or(0),
|
||||||
endurance: row.get::<_, i32>(15).unwrap_or(0),
|
endurance: row.get::<_, i32>(15).unwrap_or(0),
|
||||||
max_endurance: row.get::<_, i32>(16).unwrap_or(0),
|
max_endurance: row.get::<_, i32>(16).unwrap_or(0),
|
||||||
|
gold: row.get::<_, i32>(17).unwrap_or(0),
|
||||||
|
silver: row.get::<_, i32>(18).unwrap_or(0),
|
||||||
|
copper: row.get::<_, i32>(19).unwrap_or(0),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -219,20 +245,22 @@ impl GameDb for SqliteDb {
|
|||||||
let _ = conn.execute(
|
let _ = conn.execute(
|
||||||
"INSERT INTO players (name, race_id, class_id, room_id, level, xp, hp, max_hp,
|
"INSERT INTO players (name, race_id, class_id, room_id, level, xp, hp, max_hp,
|
||||||
attack, defense, inventory_json, equipped_json, is_admin,
|
attack, defense, inventory_json, equipped_json, is_admin,
|
||||||
mana, max_mana, endurance, max_endurance)
|
mana, max_mana, endurance, max_endurance, gold, silver, copper)
|
||||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)
|
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20)
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
ON CONFLICT(name) DO UPDATE SET
|
||||||
room_id=excluded.room_id, level=excluded.level, xp=excluded.xp,
|
room_id=excluded.room_id, level=excluded.level, xp=excluded.xp,
|
||||||
hp=excluded.hp, max_hp=excluded.max_hp, attack=excluded.attack,
|
hp=excluded.hp, max_hp=excluded.max_hp, attack=excluded.attack,
|
||||||
defense=excluded.defense, inventory_json=excluded.inventory_json,
|
defense=excluded.defense, inventory_json=excluded.inventory_json,
|
||||||
equipped_json=excluded.equipped_json, is_admin=excluded.is_admin,
|
equipped_json=excluded.equipped_json, is_admin=excluded.is_admin,
|
||||||
mana=excluded.mana, max_mana=excluded.max_mana,
|
mana=excluded.mana, max_mana=excluded.max_mana,
|
||||||
endurance=excluded.endurance, max_endurance=excluded.max_endurance",
|
endurance=excluded.endurance, max_endurance=excluded.max_endurance,
|
||||||
|
gold=excluded.gold, silver=excluded.silver, copper=excluded.copper",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
p.name, p.race_id, p.class_id, p.room_id, p.level, p.xp,
|
p.name, p.race_id, p.class_id, p.room_id, p.level, p.xp,
|
||||||
p.hp, p.max_hp, p.attack, p.defense,
|
p.hp, p.max_hp, p.attack, p.defense,
|
||||||
p.inventory_json, p.equipped_json, p.is_admin as i32,
|
p.inventory_json, p.equipped_json, p.is_admin as i32,
|
||||||
p.mana, p.max_mana, p.endurance, p.max_endurance,
|
p.mana, p.max_mana, p.endurance, p.max_endurance,
|
||||||
|
p.gold, p.silver, p.copper,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -262,7 +290,7 @@ impl GameDb for SqliteDb {
|
|||||||
.prepare(
|
.prepare(
|
||||||
"SELECT name, race_id, class_id, room_id, level, xp, hp, max_hp,
|
"SELECT name, race_id, class_id, room_id, level, xp, hp, max_hp,
|
||||||
attack, defense, inventory_json, equipped_json, is_admin,
|
attack, defense, inventory_json, equipped_json, is_admin,
|
||||||
mana, max_mana, endurance, max_endurance
|
mana, max_mana, endurance, max_endurance, gold, silver, copper
|
||||||
FROM players ORDER BY name",
|
FROM players ORDER BY name",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -285,6 +313,9 @@ impl GameDb for SqliteDb {
|
|||||||
max_mana: row.get::<_, i32>(14).unwrap_or(0),
|
max_mana: row.get::<_, i32>(14).unwrap_or(0),
|
||||||
endurance: row.get::<_, i32>(15).unwrap_or(0),
|
endurance: row.get::<_, i32>(15).unwrap_or(0),
|
||||||
max_endurance: row.get::<_, i32>(16).unwrap_or(0),
|
max_endurance: row.get::<_, i32>(16).unwrap_or(0),
|
||||||
|
gold: row.get::<_, i32>(17).unwrap_or(0),
|
||||||
|
silver: row.get::<_, i32>(18).unwrap_or(0),
|
||||||
|
copper: row.get::<_, i32>(19).unwrap_or(0),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
12
src/game.rs
12
src/game.rs
@@ -35,6 +35,9 @@ pub struct Player {
|
|||||||
pub guilds: HashMap<String, i32>,
|
pub guilds: HashMap<String, i32>,
|
||||||
pub cooldowns: HashMap<String, i32>,
|
pub cooldowns: HashMap<String, i32>,
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
|
pub gold: i32,
|
||||||
|
pub silver: i32,
|
||||||
|
pub copper: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
@@ -344,6 +347,9 @@ impl GameState {
|
|||||||
guilds,
|
guilds,
|
||||||
cooldowns: HashMap::new(),
|
cooldowns: HashMap::new(),
|
||||||
is_admin: false,
|
is_admin: false,
|
||||||
|
gold: 0,
|
||||||
|
silver: 0,
|
||||||
|
copper: 10, // Start with some copper
|
||||||
},
|
},
|
||||||
channel,
|
channel,
|
||||||
handle,
|
handle,
|
||||||
@@ -401,6 +407,9 @@ impl GameState {
|
|||||||
guilds,
|
guilds,
|
||||||
cooldowns: HashMap::new(),
|
cooldowns: HashMap::new(),
|
||||||
is_admin: saved.is_admin,
|
is_admin: saved.is_admin,
|
||||||
|
gold: saved.gold,
|
||||||
|
silver: saved.silver,
|
||||||
|
copper: saved.copper,
|
||||||
},
|
},
|
||||||
channel,
|
channel,
|
||||||
handle,
|
handle,
|
||||||
@@ -435,6 +444,9 @@ impl GameState {
|
|||||||
endurance: p.stats.endurance,
|
endurance: p.stats.endurance,
|
||||||
max_endurance: p.stats.max_endurance,
|
max_endurance: p.stats.max_endurance,
|
||||||
is_admin: p.is_admin,
|
is_admin: p.is_admin,
|
||||||
|
gold: p.gold,
|
||||||
|
silver: p.silver,
|
||||||
|
copper: p.copper,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub async fn run_tick_engine(state: SharedState) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let att = st.npc_attitude_toward(npc_id, &conn.player.name);
|
let att = st.npc_attitude_toward(npc_id, &conn.player.name);
|
||||||
if att.will_attack() {
|
if att.is_hostile() {
|
||||||
new_combats.push((*pid, npc_id.clone()));
|
new_combats.push((*pid, npc_id.clone()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/world.rs
45
src/world.rs
@@ -79,10 +79,22 @@ pub struct RoomFile {
|
|||||||
pub exits: HashMap<String, String>,
|
pub exits: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct ShopFile {
|
||||||
|
pub buys: Vec<String>, // List of item kinds or IDs the shop buys
|
||||||
|
pub sells: Vec<String>, // List of item IDs the shop sells
|
||||||
|
#[serde(default)]
|
||||||
|
pub markup: f32, // Multiplier for sell price (default 1.0)
|
||||||
|
#[serde(default)]
|
||||||
|
pub markdown: f32, // Multiplier for buy price (default 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct NpcDialogue {
|
pub struct NpcDialogue {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub greeting: Option<String>,
|
pub greeting: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keywords: HashMap<String, String>, // keyword -> response
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -113,6 +125,14 @@ pub struct NpcFile {
|
|||||||
pub dialogue: Option<NpcDialogue>,
|
pub dialogue: Option<NpcDialogue>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub combat: Option<NpcCombatFile>,
|
pub combat: Option<NpcCombatFile>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub shop: Option<ShopFile>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub gold: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub silver: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub copper: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_attitude() -> Attitude {
|
fn default_attitude() -> Attitude {
|
||||||
@@ -143,6 +163,12 @@ pub struct ObjectFile {
|
|||||||
pub takeable: bool,
|
pub takeable: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub stats: Option<ObjectStatsFile>,
|
pub stats: Option<ObjectStatsFile>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub value_gold: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub value_silver: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub value_copper: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Race TOML schema ---
|
// --- Race TOML schema ---
|
||||||
@@ -409,7 +435,12 @@ pub struct Npc {
|
|||||||
pub fixed_class: Option<String>,
|
pub fixed_class: Option<String>,
|
||||||
pub respawn_secs: Option<u64>,
|
pub respawn_secs: Option<u64>,
|
||||||
pub greeting: Option<String>,
|
pub greeting: Option<String>,
|
||||||
|
pub keywords: HashMap<String, String>,
|
||||||
pub combat: Option<NpcCombatStats>,
|
pub combat: Option<NpcCombatStats>,
|
||||||
|
pub shop: Option<ShopFile>,
|
||||||
|
pub gold: i32,
|
||||||
|
pub silver: i32,
|
||||||
|
pub copper: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@@ -429,6 +460,9 @@ pub struct Object {
|
|||||||
pub slot: Option<String>,
|
pub slot: Option<String>,
|
||||||
pub takeable: bool,
|
pub takeable: bool,
|
||||||
pub stats: ObjectStats,
|
pub stats: ObjectStats,
|
||||||
|
pub value_gold: i32,
|
||||||
|
pub value_silver: i32,
|
||||||
|
pub value_copper: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DEFAULT_HUMANOID_SLOTS: &[&str] = &[
|
pub const DEFAULT_HUMANOID_SLOTS: &[&str] = &[
|
||||||
@@ -637,14 +671,18 @@ impl World {
|
|||||||
|
|
||||||
load_entities_from_dir(®ion_path.join("npcs"), ®ion_name, &mut |id, content| {
|
load_entities_from_dir(®ion_path.join("npcs"), ®ion_name, &mut |id, content| {
|
||||||
let nf: NpcFile = toml::from_str(content).map_err(|e| format!("Bad npc {id}: {e}"))?;
|
let nf: NpcFile = toml::from_str(content).map_err(|e| format!("Bad npc {id}: {e}"))?;
|
||||||
|
let (greeting, keywords) = match nf.dialogue {
|
||||||
|
Some(d) => (d.greeting, d.keywords),
|
||||||
|
None => (None, HashMap::new()),
|
||||||
|
};
|
||||||
let combat = Some(nf.combat.map(|c| NpcCombatStats { max_hp: c.max_hp, attack: c.attack, defense: c.defense, xp_reward: c.xp_reward })
|
let combat = Some(nf.combat.map(|c| NpcCombatStats { max_hp: c.max_hp, attack: c.attack, defense: c.defense, xp_reward: c.xp_reward })
|
||||||
.unwrap_or(NpcCombatStats { max_hp: 20, attack: 4, defense: 2, xp_reward: 5 }));
|
.unwrap_or(NpcCombatStats { max_hp: 20, attack: 4, defense: 2, xp_reward: 5 }));
|
||||||
let greeting = nf.dialogue.and_then(|d| d.greeting);
|
|
||||||
npcs.insert(id.clone(), Npc {
|
npcs.insert(id.clone(), Npc {
|
||||||
id: id.clone(), name: nf.name, description: nf.description, room: nf.room,
|
id: id.clone(), name: nf.name, description: nf.description, room: nf.room,
|
||||||
base_attitude: nf.base_attitude, faction: nf.faction,
|
base_attitude: nf.base_attitude, faction: nf.faction,
|
||||||
fixed_race: nf.race, fixed_class: nf.class,
|
fixed_race: nf.race, fixed_class: nf.class,
|
||||||
respawn_secs: nf.respawn_secs, greeting, combat,
|
respawn_secs: nf.respawn_secs, greeting, keywords, combat,
|
||||||
|
shop: nf.shop, gold: nf.gold, silver: nf.silver, copper: nf.copper,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
@@ -656,6 +694,7 @@ impl World {
|
|||||||
id: id.clone(), name: of.name, description: of.description, room: of.room,
|
id: id.clone(), name: of.name, description: of.description, room: of.room,
|
||||||
kind: of.kind, slot: of.slot, takeable: of.takeable,
|
kind: of.kind, slot: of.slot, takeable: of.takeable,
|
||||||
stats: ObjectStats { damage: stats.damage, armor: stats.armor, heal_amount: stats.heal_amount },
|
stats: ObjectStats { damage: stats.damage, armor: stats.armor, heal_amount: stats.heal_amount },
|
||||||
|
value_gold: of.value_gold, value_silver: of.value_silver, value_copper: of.value_copper,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -3,3 +3,13 @@ description = "Breda is haughty in bearing, with thin auburn hair and narrow blu
|
|||||||
room = "lawold:well_market_trade_stalls"
|
room = "lawold:well_market_trade_stalls"
|
||||||
race = "race:human"
|
race = "race:human"
|
||||||
base_attitude = "aggressive"
|
base_attitude = "aggressive"
|
||||||
|
|
||||||
|
[dialogue]
|
||||||
|
greeting = "What do you want? I'm busy. Unless you have some teeth to sell?"
|
||||||
|
keywords = { teeth = "I buy monster teeth. One silver each. No questions asked.", tools = "My tools aren't for sale, but I have some spares if you have the coin." }
|
||||||
|
|
||||||
|
[shop]
|
||||||
|
buys = ["junk", "teeth", "tool"]
|
||||||
|
sells = ["town:small_hammer", "town:chisel"]
|
||||||
|
markup = 1.2
|
||||||
|
markdown = 0.5
|
||||||
|
|||||||
7
world/town/objects/chisel.toml
Normal file
7
world/town/objects/chisel.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name = "Chisel"
|
||||||
|
description = "A sharp steel chisel, used for fine woodwork or stone carving."
|
||||||
|
kind = "tool"
|
||||||
|
takeable = true
|
||||||
|
value_gold = 0
|
||||||
|
value_silver = 3
|
||||||
|
value_copper = 50
|
||||||
7
world/town/objects/small_hammer.toml
Normal file
7
world/town/objects/small_hammer.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name = "Small Hammer"
|
||||||
|
description = "A sturdy iron hammer with a wooden handle, suitable for small repairs or light construction."
|
||||||
|
kind = "tool"
|
||||||
|
takeable = true
|
||||||
|
value_gold = 0
|
||||||
|
value_silver = 5
|
||||||
|
value_copper = 0
|
||||||
Reference in New Issue
Block a user