Traders come through the portal and sell rune plans, rare creatures, wood and mana

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 00:18:47 +01:00
1 parent 74837f1c34
commit 4c9fbe47e4
24 files changed
+406 -3

No files matched your search

+12 -1
View File
@@ -130,6 +130,7 @@ pub enum EK {
// wild beasts of the caverns (beasts.rs): neutral until tamed
CaveBear, // strong and shaggy, likes the mushroom garden
Beetle, // a giant armoured beetle, likes the lair
Trader, // a merchant visiting through the portal (traders.rs)
}
pub struct St {
@@ -176,6 +177,7 @@ pub fn st(k: EK) -> St {
EK::Catapult => s("unit.catapult", 320.0, 40.0, 7.5, 0.9, 3.5, 0, 200),
EK::CaveBear => s("unit.cavebear", 420.0, 24.0, 1.0, 1.9, 1.4, 110, 0),
EK::Beetle => s("unit.beetle", 300.0, 13.0, 1.0, 1.7, 1.1, 70, 0),
EK::Trader => s("unit.trader", 200.0, 1.0, 1.0, 1.5, 2.0, 0, 0),
}
}
@@ -391,7 +393,7 @@ impl Ent {
}
// the player's creatures (not imps, heroes or the rivals' units)
pub fn creature(&self) -> bool {
!self.hero && self.side == me_side() && self.kind != EK::Worker && !self.chained
!self.hero && self.side == me_side() && self.kind != EK::Worker && self.kind != EK::Trader && !self.chained
}
// 0 player, 1 humans, 2 and 3 rival masters
pub fn side(&self) -> u8 {
@@ -609,6 +611,8 @@ pub struct Game {
#[serde(default)]
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
#[serde(default)]
pub trading: crate::traders::Trading, // the visiting trader (traders.rs)
#[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)
@@ -950,6 +954,7 @@ impl Game {
vrng: crate::particles::vrng(),
dwarf_caves: Vec::new(),
bridgework: Default::default(),
trading: Default::default(),
happenings: None,
stats: Default::default(),
wall_plans: Vec::new(),
@@ -1724,6 +1729,7 @@ impl Game {
self.update_bridges(dt);
self.update_wood(dt);
self.update_events(dt);
self.update_traders(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
@@ -2400,6 +2406,8 @@ impl Game {
self.captive_update(e, dt);
} else if self.is_rival_side(e.side) || (e.rival() && !self.is_master(e.side)) {
self.rival_ai(e, dt);
} else if e.kind == EK::Trader {
self.trader_ai(e, dt);
} else if e.dwarf() {
self.dwarf_ai(e, dt);
} else if e.hero {
@@ -5412,6 +5420,9 @@ impl Game {
return false;
}
let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return false };
if self.buy_from_trader(j) {
return false;
}
let e = &self.ents[j];
if e.rival() || e.dwarf() {
return false; // the rivals' creatures and the dwarves cannot be picked up
+15 -1
View File
@@ -1574,6 +1574,19 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
"...y..y...",
"..........",
],
// trader: a hooded merchant with a big pack and a lantern
EK::Trader => [
"...kkk....",
"..kPPPk.BB",
".kPfefPkBb",
".kPffffkBb",
"..kPPPPkBB",
".kPPyPPPk.",
"ykPPPPPPk.",
"o.kPPPPk..",
"..kPk.kPk.",
"..kk...kk.",
],
// cave bear: shaggy brown fur, small yellow eyes
EK::CaveBear => [
".kk....kk.",
@@ -1708,7 +1721,7 @@ fn pal(ch: char) -> Option<Color> {
})
}
pub const SPRITE_KINDS: [EK; 26] = [
pub const SPRITE_KINDS: [EK; 27] = [
EK::Worker,
EK::Kobold,
EK::Spider,
@@ -1735,6 +1748,7 @@ pub const SPRITE_KINDS: [EK; 26] = [
EK::Catapult,
EK::CaveBear,
EK::Beetle,
EK::Trader,
];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
+1
View File
@@ -40,6 +40,7 @@ mod settings;
mod siege;
mod sfx;
mod tracks;
mod traders;
mod traps;
mod sound;
mod surface;
+170
View File
@@ -0,0 +1,170 @@
// Traders: now and then a merchant comes through one of the master's portals and waits there
// for a minute. A click with the hand buys what he offers - the plans of a rune forge tower, a
// rare creature, a bundle of wood or a potion that fills the mana. Single player only (in a
// match the masters would have to agree on who buys).
use crate::game::*;
use crate::map::*;
use serde::{Deserialize, Serialize};
// seconds until the first trader, between two traders, and how long one waits
pub const TRADER_FIRST: f32 = 420.0;
pub const TRADER_GAP: f32 = 540.0;
pub const TRADER_WAIT: f32 = 60.0;
// what a trader may offer: (language key, price)
pub const OFFERS: [(&str, f32); 5] = [("trader.rune", 450.0), ("trader.wyrm", 1200.0), ("trader.chainbreaker", 900.0), ("trader.wood", 250.0), ("trader.mana", 200.0)];
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Trading {
pub t: f32, // seconds until the next trader comes
pub here: Option<(u32, u8, f32)>, // the trader on the map: unit id, offer, seconds left
}
impl Game {
pub(crate) fn update_traders(&mut self, dt: f32) {
if self.netplay.is_some() || self.demo.is_some() || self.state != 0 {
return;
}
if self.trading.t == 0.0 && self.trading.here.is_none() {
self.trading.t = TRADER_FIRST;
}
if let Some((id, offer, left)) = self.trading.here {
let left = left - dt;
if left <= 0.0 || self.find(id).is_none() {
self.trader_leaves(true);
} else {
self.trading.here = Some((id, offer, left));
}
return;
}
self.trading.t -= dt;
if self.trading.t > 0.0 {
return;
}
self.trading.t = TRADER_GAP + self.rng.f() * 180.0;
// he comes through an own portal
let portals: Vec<(i32, i32)> = (0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).map(|k| self.map.portals[k]).collect();
let Some(&(px, py)) = portals.get((self.rng.next() as usize) % portals.len().max(1)) else { return };
let spot = DIR8.iter().map(|&(dx, dy)| (px + 1 + dx * 2, py + 1 + dy * 2)).find(|&(x, y)| self.map.walk(x, y));
let Some((x, y)) = spot else { return };
let offer = (self.rng.next() % OFFERS.len() as u32) as u8;
let id = self.spawn(EK::Trader, false, x as f32 + 0.5, y as f32 + 0.5, 1);
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
e.side = self.me_side;
e.home = Some((x as f32 + 0.5, y as f32 + 0.5));
e.settled = true;
e.happy = 1.0;
}
self.trading.here = Some((id, offer, TRADER_WAIT));
let (key, price) = OFFERS[offer as usize];
self.msg("msg.trader_arrives", vec![format!("@{}", key), (price as i32).to_string()]);
self.sfx_ui(Sfx::Deposit);
}
// the trader goes back through the portal
fn trader_leaves(&mut self, unsold: bool) {
if let Some((id, _, _)) = self.trading.here.take() {
if let Some(j) = self.find(id) {
let (x, y) = (self.ents[j].x, self.ents[j].y);
self.ents[j].alive = false;
self.fx(7, x, y, 0.0, 0.0, 0.8, 0xffff60c0);
}
if unsold {
self.msg("msg.trader_left", vec![]);
}
}
}
// the hand on the trader: buy his offer (true: it was the trader)
pub(crate) fn buy_from_trader(&mut self, j: usize) -> bool {
let Some((id, offer, _)) = self.trading.here else { return false };
if self.ents[j].id != id {
return false;
}
let (key, price) = OFFERS[offer as usize];
if self.gold < price {
self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error);
return true;
}
self.gold -= price;
let (x, y) = (self.ents[j].x, self.ents[j].y);
match offer {
0 => self.rune_free += 1,
1 | 2 => {
let (kind, level) = if offer == 1 { (EK::Wyrm, 3) } else { (EK::Chainbreaker, 4) };
let nid = self.spawn(kind, false, x, y, level);
if let Some(e) = self.ents.iter_mut().find(|e| e.id == nid) {
e.side = self.me_side;
e.settled = false;
e.job = Job::Register;
e.timer = 1.0;
e.happy = 1.0;
}
}
3 => self.wood += 30.0,
_ => self.mana = self.mana_max(),
}
self.msg("msg.trader_sold", vec![format!("@{}", key)]);
self.sfx_ui(Sfx::Payday);
self.trader_leaves(false);
true
}
// a trader just stands by the portal and waits
pub(crate) fn trader_ai(&mut self, e: &mut Ent, dt: f32) {
e.hunger = 0.0;
e.energy = 1.0;
e.happy = 1.0;
if let Some((hx, hy)) = e.home {
if (e.x - hx).abs() + (e.y - hy).abs() > 0.3 && e.path.is_empty() {
e.x = hx;
e.y = hy;
}
}
let _ = dt;
}
// what the trader offers (hover information): (offer key, price, seconds left)
pub fn trader_offer(&self, id: u32) -> Option<(&'static str, f32, f32)> {
self.trading.here.filter(|h| h.0 == id).map(|(_, o, left)| (OFFERS[o as usize].0, OFFERS[o as usize].1, left))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_trader_comes_and_sells() {
let mut g = Game::new(7, 64, 64);
// own the portal
let (px, py) = g.map.portals[0];
for y in py - 2..py + 5 {
for x in px - 2..px + 5 {
if inb(x, y) && g.map.t[idx(x, y)].kind != Kind::Portal {
g.map.t[idx(x, y)].kind = Kind::Floor;
}
if inb(x, y) {
g.map.t[idx(x, y)].owner = Owner::Player;
}
}
}
g.trading.t = 0.01;
g.update_traders(0.05);
let (id, offer, _) = g.trading.here.expect("a trader came");
let j = g.find(id).unwrap();
assert!(!g.ents[j].creature(), "not one of the household");
g.gold = 5000.0;
let (gold, runes, wood) = (g.gold, g.rune_free, g.wood);
assert!(g.buy_from_trader(j));
assert!(g.gold < gold);
match offer {
0 => assert_eq!(g.rune_free, runes + 1),
3 => assert!(g.wood > wood),
_ => {}
}
assert!(g.trading.here.is_none() && !g.ents[j].alive, "he leaves");
}
}
+7
View File
@@ -1508,6 +1508,13 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
if let Some(j) = ent {
let e = &g.ents[j];
let s = st(e.kind);
// the trader: his offer, the price and how long he waits
if let Some((key, price, left)) = g.trader_offer(e.id) {
out.push((tr(s.name), GOLD));
out.push((trf("trader.info", &[tr(key), (price as i32).to_string()]), FRAME_HI));
out.push((trf("trader.left", &[(left.ceil() as i32).to_string()]), DIM));
return out;
}
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
out.push((trf("info.rank", &[tr(&format!("rank.{}", e.level.clamp(1, 10)))]), GOLD));
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));