Get SitePricing information to clients, and use it to display coin-denominated prices in voxygen on tooltips during a trade. Also boost merchant spawn rate slightly.

This commit is contained in:
Avi Weinstock 2021-03-25 00:35:33 -04:00
parent 28952f6d7b
commit 8d90548331
13 changed files with 131 additions and 45 deletions

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Pickaxes (can be used to collect gems and mine weak rock) - Pickaxes (can be used to collect gems and mine weak rock)
- You can now jump out of rolls for a slight jump boost - You can now jump out of rolls for a slight jump boost
- Dungeons now have multiple kinds of stairs. - Dungeons now have multiple kinds of stairs.
- Trades now display item prices in tooltips.
### Changed ### Changed

View File

@ -35,7 +35,7 @@ use common::{
recipe::RecipeBook, recipe::RecipeBook,
resources::PlayerEntity, resources::PlayerEntity,
terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize}, terrain::{block::Block, neighbors, BiomeKind, SitesKind, TerrainChunk, TerrainChunkSize},
trade::{PendingTrade, TradeAction, TradeId, TradeResult}, trade::{PendingTrade, SitePrices, TradeAction, TradeId, TradeResult},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
vol::RectVolSize, vol::RectVolSize,
}; };
@ -156,7 +156,7 @@ pub struct Client {
// Pending invites that this client has sent out // Pending invites that this client has sent out
pending_invites: HashSet<Uid>, pending_invites: HashSet<Uid>,
// The pending trade the client is involved in, and it's id // The pending trade the client is involved in, and it's id
pending_trade: Option<(TradeId, PendingTrade)>, pending_trade: Option<(TradeId, PendingTrade, Option<SitePrices>)>,
_network: Network, _network: Network,
participant: Option<Participant>, participant: Option<Participant>,
@ -694,7 +694,7 @@ impl Client {
} }
pub fn perform_trade_action(&mut self, action: TradeAction) { pub fn perform_trade_action(&mut self, action: TradeAction) {
if let Some((id, _)) = self.pending_trade { if let Some((id, _, _)) = self.pending_trade {
if let TradeAction::Decline = action { if let TradeAction::Decline = action {
self.pending_trade.take(); self.pending_trade.take();
} }
@ -833,7 +833,9 @@ impl Client {
pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites } pub fn pending_invites(&self) -> &HashSet<Uid> { &self.pending_invites }
pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade)> { &self.pending_trade } pub fn pending_trade(&self) -> &Option<(TradeId, PendingTrade, Option<SitePrices>)> {
&self.pending_trade
}
pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) { pub fn send_invite(&mut self, invitee: Uid, kind: InviteKind) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite( self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InitiateInvite(
@ -1637,12 +1639,12 @@ impl Client {
impulse, impulse,
}); });
}, },
ServerGeneral::UpdatePendingTrade(id, trade) => { ServerGeneral::UpdatePendingTrade(id, trade, pricing) => {
tracing::trace!("UpdatePendingTrade {:?} {:?}", id, trade); tracing::trace!("UpdatePendingTrade {:?} {:?}", id, trade);
self.pending_trade = Some((id, trade)); self.pending_trade = Some((id, trade, pricing));
}, },
ServerGeneral::FinishedTrade(result) => { ServerGeneral::FinishedTrade(result) => {
if let Some((_, trade)) = self.pending_trade.take() { if let Some((_, trade, _)) = self.pending_trade.take() {
self.update_available_recipes(); self.update_available_recipes();
frontend_events.push(Event::TradeComplete { result, trade }) frontend_events.push(Event::TradeComplete { result, trade })
} }

View File

@ -8,7 +8,7 @@ use common::{
recipe::RecipeBook, recipe::RecipeBook,
resources::TimeOfDay, resources::TimeOfDay,
terrain::{Block, TerrainChunk}, terrain::{Block, TerrainChunk},
trade::{PendingTrade, TradeId, TradeResult}, trade::{PendingTrade, SitePrices, TradeId, TradeResult},
uid::Uid, uid::Uid,
uuid::Uuid, uuid::Uuid,
}; };
@ -126,7 +126,7 @@ pub enum ServerGeneral {
Disconnect(DisconnectReason), Disconnect(DisconnectReason),
/// Send a popup notification such as "Waypoint Saved" /// Send a popup notification such as "Waypoint Saved"
Notification(Notification), Notification(Notification),
UpdatePendingTrade(TradeId, PendingTrade), UpdatePendingTrade(TradeId, PendingTrade, Option<SitePrices>),
FinishedTrade(TradeResult), FinishedTrade(TradeResult),
/// Economic information about sites /// Economic information about sites
SiteEconomy(EconomyInfo), SiteEconomy(EconomyInfo),
@ -237,7 +237,7 @@ impl ServerMsg {
| ServerGeneral::SetViewDistance(_) | ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_) | ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) | ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _) | ServerGeneral::UpdatePendingTrade(_, _, _)
| ServerGeneral::FinishedTrade(_) | ServerGeneral::FinishedTrade(_)
| ServerGeneral::SiteEconomy(_) => { | ServerGeneral::SiteEconomy(_) => {
c_type == ClientType::Game && presence.is_some() c_type == ClientType::Game && presence.is_some()

View File

@ -320,6 +320,21 @@ impl Default for Good {
} }
} }
impl Good {
/// The discounting factor applied when selling goods back to a merchant
pub fn trade_margin(&self) -> f32 {
match self {
Good::Tools | Good::Armor => 0.5,
Good::Food | Good::Potions | Good::Ingredients => 0.75,
Good::Coin => 1.0,
// Certain abstract goods (like Territory) shouldn't be attached to concrete items;
// give a sale price of 0 if the player is trying to sell a concrete item that somehow
// has one of these categories
_ => 0.0,
}
}
}
// ideally this would be a real Id<Site> but that is from the world crate // ideally this would be a real Id<Site> but that is from the world crate
pub type SiteId = u64; pub type SiteId = u64;
@ -329,7 +344,7 @@ pub struct SiteInformation {
pub unconsumed_stock: HashMap<Good, f32>, pub unconsumed_stock: HashMap<Good, f32>,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SitePrices { pub struct SitePrices {
pub values: HashMap<Good, f32>, pub values: HashMap<Good, f32>,
} }

View File

@ -91,7 +91,7 @@ impl Client {
| ServerGeneral::SiteEconomy(_) | ServerGeneral::SiteEconomy(_)
| ServerGeneral::Outcomes(_) | ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) | ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _) | ServerGeneral::UpdatePendingTrade(_, _, _)
| ServerGeneral::FinishedTrade(_) => { | ServerGeneral::FinishedTrade(_) => {
self.in_game_stream.lock().unwrap().send(g) self.in_game_stream.lock().unwrap().send(g)
}, },
@ -162,7 +162,7 @@ impl Client {
| ServerGeneral::Outcomes(_) | ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_) | ServerGeneral::Knockback(_)
| ServerGeneral::SiteEconomy(_) | ServerGeneral::SiteEconomy(_)
| ServerGeneral::UpdatePendingTrade(_, _) | ServerGeneral::UpdatePendingTrade(_, _, _)
| ServerGeneral::FinishedTrade(_) => { | ServerGeneral::FinishedTrade(_) => {
PreparedMsg::new(2, &g, &self.in_game_stream) PreparedMsg::new(2, &g, &self.in_game_stream)
}, },

View File

@ -3,7 +3,7 @@ use crate::{client::Client, Server};
use common::{ use common::{
comp::{ comp::{
self, self,
agent::AgentEvent, agent::{Agent, AgentEvent},
group::GroupManager, group::GroupManager,
invite::{Invite, InviteKind, InviteResponse, PendingInvites}, invite::{Invite, InviteKind, InviteResponse, PendingInvites},
ChatType, ChatType,
@ -162,9 +162,11 @@ pub fn handle_invite_response(
} }
pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) { pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
let index = server.index.clone();
let state = server.state_mut(); let state = server.state_mut();
let clients = state.ecs().read_storage::<Client>(); let clients = state.ecs().read_storage::<Client>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let agents = state.ecs().read_storage::<Agent>();
let mut invites = state.ecs().write_storage::<Invite>(); let mut invites = state.ecs().write_storage::<Invite>();
if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| { if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
let Invite { inviter, kind } = invite; let Invite { inviter, kind } = invite;
@ -216,12 +218,20 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
let mut trades = state.ecs().write_resource::<Trades>(); let mut trades = state.ecs().write_resource::<Trades>();
let id = trades.begin_trade(inviter_uid, invitee_uid); let id = trades.begin_trade(inviter_uid, invitee_uid);
let trade = trades.trades[&id].clone(); let trade = trades.trades[&id].clone();
clients let pricing = agents
.get(inviter) .get(inviter)
.map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade.clone()))); .and_then(|a| index.get_site_prices(a))
.or_else(|| agents.get(entity).and_then(|a| index.get_site_prices(a)));
clients.get(inviter).map(|c| {
c.send(ServerGeneral::UpdatePendingTrade(
id,
trade.clone(),
pricing.clone(),
))
});
clients clients
.get(entity) .get(entity)
.map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade))); .map(|c| c.send(ServerGeneral::UpdatePendingTrade(id, trade, pricing)));
} }
}, },
} }

View File

@ -33,19 +33,18 @@ fn notify_agent_prices(
event: AgentEvent, event: AgentEvent,
) { ) {
if let Some(agent) = agents.get_mut(entity) { if let Some(agent) = agents.get_mut(entity) {
let prices = index.get_site_prices(agent);
if let AgentEvent::UpdatePendingTrade(boxval) = event { if let AgentEvent::UpdatePendingTrade(boxval) = event {
// Box<(tid, pend, _, inventories)>) = event { // Box<(tid, pend, _, inventories)>) = event {
let prices = agent
.trade_for_site
.map(|i| index.sites.recreate_id(i))
.flatten()
.map(|i| index.sites.get(i))
.map(|s| s.economy.get_site_prices())
.unwrap_or_default();
agent agent
.inbox .inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new(( .push_front(AgentEvent::UpdatePendingTrade(Box::new((
boxval.0, boxval.1, prices, boxval.3, // Prefer using this Agent's price data, but use the counterparty's price data
// if we don't have price data
boxval.0,
boxval.1,
prices.unwrap_or(boxval.2),
boxval.3,
)))); ))));
} }
} }
@ -103,6 +102,8 @@ pub fn handle_process_trade_action(
} else { } else {
let mut entities: [Option<specs::Entity>; 2] = [None, None]; let mut entities: [Option<specs::Entity>; 2] = [None, None];
let mut inventories: [Option<ReducedInventory>; 2] = [None, None]; let mut inventories: [Option<ReducedInventory>; 2] = [None, None];
let mut prices = None;
let agents = server.state.ecs().read_storage::<Agent>();
// sadly there is no map and collect on arrays // sadly there is no map and collect on arrays
for i in 0..2 { for i in 0..2 {
// parties.len()) { // parties.len()) {
@ -114,13 +115,23 @@ pub fn handle_process_trade_action(
.read_component::<Inventory>() .read_component::<Inventory>()
.get(e) .get(e)
.map(|i| ReducedInventory::from(i)); .map(|i| ReducedInventory::from(i));
// Get price info from the first Agent in the trade (currently, an
// Agent will never initiate a trade with another agent though)
prices = prices.or_else(|| {
agents.get(e).and_then(|a| server.index.get_site_prices(a))
});
} }
} }
drop(agents);
for party in entities.iter() { for party in entities.iter() {
if let Some(e) = *party { if let Some(e) = *party {
server.notify_client( server.notify_client(
e, e,
ServerGeneral::UpdatePendingTrade(trade_id, entry.get().clone()), ServerGeneral::UpdatePendingTrade(
trade_id,
entry.get().clone(),
prices.clone(),
),
); );
notify_agent_prices( notify_agent_prices(
server.state.ecs().write_storage::<Agent>(), server.state.ecs().write_storage::<Agent>(),
@ -129,7 +140,7 @@ pub fn handle_process_trade_action(
AgentEvent::UpdatePendingTrade(Box::new(( AgentEvent::UpdatePendingTrade(Box::new((
trade_id, trade_id,
entry.get().clone(), entry.get().clone(),
Default::default(), prices.clone().unwrap_or_default(),
inventories.clone(), inventories.clone(),
))), ))),
); );

View File

@ -22,7 +22,7 @@ use common::{
rtsim::{Memory, MemoryItem, RtSimEntity, RtSimEvent}, rtsim::{Memory, MemoryItem, RtSimEntity, RtSimEvent},
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
time::DayPeriod, time::DayPeriod,
trade::{Good, TradeAction, TradePhase, TradeResult}, trade::{TradeAction, TradePhase, TradeResult},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
util::Dir, util::Dir,
vol::ReadVol, vol::ReadVol,
@ -947,14 +947,6 @@ impl<'a> AgentData<'a> {
// This needs revisiting when agents can initiate trades (e.g. to offer // This needs revisiting when agents can initiate trades (e.g. to offer
// mercenary contracts as quests) // mercenary contracts as quests)
const WHO: usize = 1; const WHO: usize = 1;
fn trade_margin(g: Good) -> f32 {
match g {
Good::Tools | Good::Armor => 0.5,
Good::Food | Good::Potions | Good::Ingredients => 0.75,
Good::Coin => 1.0,
_ => 0.0, // what is this?
}
}
let balance = |who: usize, reduce: bool| { let balance = |who: usize, reduce: bool| {
pending.offers[who] pending.offers[who]
.iter() .iter()
@ -972,7 +964,7 @@ impl<'a> AgentData<'a> {
.unwrap_or_default() .unwrap_or_default()
* factor * factor
* (*amount as f32) * (*amount as f32)
* if reduce { trade_margin(material) } else { 1.0 } * if reduce { material.trade_margin() } else { 1.0 }
}) })
}) })
.flatten() .flatten()

View File

@ -64,6 +64,7 @@ pub struct InventoryScrollerState {
#[derive(WidgetCommon)] #[derive(WidgetCommon)]
pub struct InventoryScroller<'a> { pub struct InventoryScroller<'a> {
client: &'a Client,
imgs: &'a Imgs, imgs: &'a Imgs,
item_imgs: &'a ItemImgs, item_imgs: &'a ItemImgs,
fonts: &'a Fonts, fonts: &'a Fonts,
@ -87,6 +88,7 @@ pub struct InventoryScroller<'a> {
impl<'a> InventoryScroller<'a> { impl<'a> InventoryScroller<'a> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
client: &'a Client,
imgs: &'a Imgs, imgs: &'a Imgs,
item_imgs: &'a ItemImgs, item_imgs: &'a ItemImgs,
fonts: &'a Fonts, fonts: &'a Fonts,
@ -105,6 +107,7 @@ impl<'a> InventoryScroller<'a> {
bg_ids: &'a BackgroundIds, bg_ids: &'a BackgroundIds,
) -> Self { ) -> Self {
InventoryScroller { InventoryScroller {
client,
imgs, imgs,
item_imgs, item_imgs,
fonts, fonts,
@ -302,6 +305,11 @@ impl<'a> InventoryScroller<'a> {
Quality::Artifact => self.imgs.inv_slot_orange, Quality::Artifact => self.imgs.inv_slot_orange,
_ => self.imgs.inv_slot_red, _ => self.imgs.inv_slot_red,
}; };
let mut desc = desc.to_string();
if let Some((_, _, prices)) = self.client.pending_trade() {
super::util::append_price_desc(&mut desc, prices, item.item_definition_id());
}
slot_widget slot_widget
.filled_slot(quality_col_img) .filled_slot(quality_col_img)
.with_tooltip( .with_tooltip(
@ -571,6 +579,7 @@ impl<'a> Widget for Bag<'a> {
.desc_text_color(TEXT_COLOR); .desc_text_color(TEXT_COLOR);
InventoryScroller::new( InventoryScroller::new(
self.client,
self.imgs, self.imgs,
self.item_imgs, self.item_imgs,
self.fonts, self.fonts,

View File

@ -20,7 +20,7 @@ use common::{
inventory::item::{MaterialStatManifest, Quality}, inventory::item::{MaterialStatManifest, Quality},
Inventory, Inventory,
}, },
trade::{PendingTrade, TradeAction, TradePhase}, trade::{PendingTrade, SitePrices, TradeAction, TradePhase},
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
use conrod_core::{ use conrod_core::{
@ -156,6 +156,7 @@ impl<'a> Trade<'a> {
state: &mut ConrodState<'_, State>, state: &mut ConrodState<'_, State>,
ui: &mut UiCell<'_>, ui: &mut UiCell<'_>,
trade: &'a PendingTrade, trade: &'a PendingTrade,
prices: &'a Option<SitePrices>,
ours: bool, ours: bool,
) -> <Self as Widget>::Event { ) -> <Self as Widget>::Event {
let inventories = self.client.inventories(); let inventories = self.client.inventories();
@ -233,7 +234,17 @@ impl<'a> Trade<'a> {
.collect(); .collect();
if matches!(trade.phase(), TradePhase::Mutate) { if matches!(trade.phase(), TradePhase::Mutate) {
self.phase1_itemwidget(state, ui, inventory, who, ours, entity, name, &tradeslots); self.phase1_itemwidget(
state,
ui,
inventory,
who,
ours,
entity,
name,
prices,
&tradeslots,
);
} else { } else {
self.phase2_itemwidget(state, ui, inventory, who, ours, entity, &tradeslots); self.phase2_itemwidget(state, ui, inventory, who, ours, entity, &tradeslots);
} }
@ -250,6 +261,7 @@ impl<'a> Trade<'a> {
ours: bool, ours: bool,
entity: EcsEntity, entity: EcsEntity,
name: String, name: String,
prices: &'a Option<SitePrices>,
tradeslots: &[TradeSlot], tradeslots: &[TradeSlot],
) { ) {
let item_tooltip = Tooltip::new({ let item_tooltip = Tooltip::new({
@ -272,6 +284,7 @@ impl<'a> Trade<'a> {
if !ours { if !ours {
InventoryScroller::new( InventoryScroller::new(
self.client,
self.imgs, self.imgs,
self.item_imgs, self.item_imgs,
self.fonts, self.fonts,
@ -353,6 +366,8 @@ impl<'a> Trade<'a> {
Quality::Artifact => self.imgs.inv_slot_orange, Quality::Artifact => self.imgs.inv_slot_orange,
_ => self.imgs.inv_slot_red, _ => self.imgs.inv_slot_red,
}; };
let mut desc = desc.to_string();
super::util::append_price_desc(&mut desc, prices, item.item_definition_id());
slot_widget slot_widget
.filled_slot(quality_col_img) .filled_slot(quality_col_img)
.with_tooltip( .with_tooltip(
@ -496,8 +511,8 @@ impl<'a> Widget for Trade<'a> {
let widget::UpdateArgs { mut state, ui, .. } = args; let widget::UpdateArgs { mut state, ui, .. } = args;
let mut event = None; let mut event = None;
let trade = match self.client.pending_trade() { let (trade, prices) = match self.client.pending_trade() {
Some((_, trade)) => trade, Some((_, trade, prices)) => (trade, prices),
None => return Some(TradeAction::Decline), None => return Some(TradeAction::Decline),
}; };
@ -523,8 +538,12 @@ impl<'a> Widget for Trade<'a> {
self.title(&mut state, ui); self.title(&mut state, ui);
self.phase_indicator(&mut state, ui, &trade); self.phase_indicator(&mut state, ui, &trade);
event = self.item_pane(&mut state, ui, &trade, false).or(event); event = self
event = self.item_pane(&mut state, ui, &trade, true).or(event); .item_pane(&mut state, ui, &trade, &prices, false)
.or(event);
event = self
.item_pane(&mut state, ui, &trade, &prices, true)
.or(event);
event = self event = self
.accept_decline_buttons(&mut state, ui, &trade) .accept_decline_buttons(&mut state, ui, &trade)
.or(event); .or(event);

View File

@ -1,5 +1,6 @@
use common::{ use common::{
comp::{ comp::{
inventory::trade_pricing::TradePricing,
item::{ item::{
armor::{Armor, ArmorKind, Protection}, armor::{Armor, ArmorKind, Protection},
tool::{Hands, StatKind, Stats, Tool, ToolKind}, tool::{Hands, StatKind, Stats, Tool, ToolKind},
@ -8,6 +9,7 @@ use common::{
BuffKind, BuffKind,
}, },
effect::Effect, effect::Effect,
trade::{Good, SitePrices},
}; };
use std::{borrow::Cow, fmt::Write}; use std::{borrow::Cow, fmt::Write};
@ -64,6 +66,20 @@ pub fn item_text<'a>(
(item.name(), desc) (item.name(), desc)
} }
pub fn append_price_desc(desc: &mut String, prices: &Option<SitePrices>, item_definition_id: &str) {
if let Some(prices) = prices {
let (material, factor) = TradePricing::get_material(item_definition_id);
let coinprice = prices.values.get(&Good::Coin).cloned().unwrap_or(1.0);
let buyprice = prices.values.get(&material).cloned().unwrap_or_default() * factor;
let sellprice = buyprice * material.trade_margin();
*desc += &format!(
"\n\nBuy price: {:0.1} coins\nSell price: {:0.1} coins",
buyprice / coinprice,
sellprice / coinprice
);
}
}
// TODO: localization // TODO: localization
fn modular_component_desc( fn modular_component_desc(
mc: &ModularComponent, mc: &ModularComponent,

View File

@ -4,7 +4,9 @@ use crate::{
}; };
use common::{ use common::{
assets::{AssetExt, AssetHandle}, assets::{AssetExt, AssetHandle},
comp::Agent,
store::Store, store::Store,
trade::SitePrices,
}; };
use core::ops::Deref; use core::ops::Deref;
use noise::{Seedable, SuperSimplex}; use noise::{Seedable, SuperSimplex};
@ -69,6 +71,15 @@ impl Index {
} }
pub fn colors(&self) -> AssetHandle<Arc<Colors>> { self.colors } pub fn colors(&self) -> AssetHandle<Arc<Colors>> { self.colors }
pub fn get_site_prices(&self, agent: &Agent) -> Option<SitePrices> {
agent
.trade_for_site
.map(|i| self.sites.recreate_id(i))
.flatten()
.map(|i| self.sites.get(i))
.map(|s| s.economy.get_site_prices())
}
} }
impl IndexOwned { impl IndexOwned {

View File

@ -944,7 +944,7 @@ impl Settlement {
.do_if(!is_dummy, |e| e.with_automatic_name()) .do_if(!is_dummy, |e| e.with_automatic_name())
.do_if(is_dummy, |e| e.with_name("Training Dummy")) .do_if(is_dummy, |e| e.with_name("Training Dummy"))
.do_if(is_human && dynamic_rng.gen(), |entity| { .do_if(is_human && dynamic_rng.gen(), |entity| {
match dynamic_rng.gen_range(0..5) { match dynamic_rng.gen_range(0..6) {
0 => entity 0 => entity
.with_main_tool(Item::new_from_asset_expect( .with_main_tool(Item::new_from_asset_expect(
"common.items.weapons.sword.iron-4", "common.items.weapons.sword.iron-4",
@ -955,7 +955,7 @@ impl Settlement {
.with_skillset_config( .with_skillset_config(
common::skillset_builder::SkillSetConfig::Guard, common::skillset_builder::SkillSetConfig::Guard,
), ),
1 => entity 1 | 2 => entity
.with_main_tool(Item::new_from_asset_expect( .with_main_tool(Item::new_from_asset_expect(
"common.items.weapons.bow.eldwood-0", "common.items.weapons.bow.eldwood-0",
)) ))