Implement /price_list (work in progress), stub for /buy and /sell

remove outdated economic simulation code

remove old values, document

add natural resources to economy

Remove NaturalResources from Place (now in Economy)

find closest site to each chunk

implement natural resources (the distance scale is wrong)

cargo fmt

working distance calculation

this collection of natural resources seem to make sense, too much Wheat though

use natural resources and controlled area to replenish goods

increase the amount of chunks controlled by one guard to 50

add new professions and goods to the list

implement multiple products per worker

remove the old code and rename the new code to the previous name

correctly determine which goods guards will give you access to

correctly estimate the amount of natural resources controlled

adapt to new server API

instrument tooltips

Now I just need to figure out how to store a (reference to) a closure

closures for tooltip content generation

pass site/cave id to the client

Add economic information to the client structure
(not yet exchanged with the server)

Send SiteId to the client, prepare messages for economy request
Make client::sites a HashMap

Specialize the Crafter into Brewer,Bladesmith and Blacksmith

working server request for economic info from within tooltip

fully operational economic tooltips
I need to fix the id clash between caves and towns though

fix overlapping ids between caves and sites

display stock amount

correctly handle invalid (cave) ids in the request

some initial balancing, turn off most info logging

less intrusive way of implementing the dynamic tool tips in map

further tooltip cleanup

further cleanup, dynamic tooltip not fully working as intended

correctly working tooltip visibility logic

cleanup, display labor value

separate economy info request in a separate translation unit

display values as well

nicer display format for economy

add last_exports and coin to the new economy

do not allocate natural resources to Dungeons (making town so much larger)

balancing attempt

print town size statistics

cargo fmt (dead code)

resource tweaks, csv debugging

output a more interesting town (and then all sites)

fix the labor value logic (now we have meaningful prices)

load professions from ron (WIP)

use assets manager in economy

loading professions works

use professions from ron file

fix Labor debug logic

count chunks per type separately
(preparing for better resource control)

better structured resource data

traders, more professions (WIP)

fix exception when starting the simulation

fix replenish function
TODO:
- use area_ratio for resource usage (chunks should be added to stock, ratio on usage?)
- fix trading

documentation clean up

fix merge artifact

Revise trader mechanic

start Coin with a reasonable default

remove the outdated economy code

preserve documentation from removed old structure

output neighboring sites (preparation)

pass list of neighbors to economy

add trade structures

trading stub

Description of purpose by zesterer on Discord

remember prices (needed for planning)

avoid growing the order vector unboundedly

into_iter doesn't clear the Vec, so clear it manually

use drain to process Vecs, avoid clone

fix the test server

implement a test stub (I need to get it faster than 30 seconds to be more useful)

enable info output in test

debug missing and extra goods

use the same logging extension as water, activate feature

update dependencies

determine good prices, good exchange goods

a working set of revisions

a cozy world which economy tests in 2s

first order planning version

fun with package version

buy according to value/priority, not missing amount

introduce min price constant, fix order priority

in depth debugging

with a correct sign the trading plans start to make sense

move the trade planning to a separate function

rename new function

reorganize code into subroutines (much cleaner)

each trading step now has its own function

cut down the number of debugging output

introduce RoadSecurity and Transportation

transport capacity bookkeeping

only plan to pay with valuable goods, you can no longer stockpile unused options
(which interestingly shows a huge impact, to be investigated)

Coin is now listed as a payment (although not used)

proper transportation estimation (although 0)

remove more left overs uncovered by viewing the code in a merge request

use the old default values, handle non-pileable stocks directly before increasing it
(as economy is based on last year's products)

don't order the missing good multiple times
also it uses coin to buy things!

fix warnings and use the transportation from stock again

cargo fmt

prepare evaluation of trade

don't count transportation multiple times

fix merge artifact

operational trade planning
trade itself is still misleading

make clippy happy

clean up

correct labor ratio of merchants (no need to multiply with amount produced)

incomplete merchant labor_value computation

correct last commit

make economy of scale more explicit

make clippy happy (and code cleaner)

more merchant tweaks (more pop=better)

beginning of real trading code

revert the update of dependencies

remove stale comments/unused code

trading implementation complete (but untested)

something is still strange ...

fix sign in trading

another sign fix

some bugfixes and plenty of debugging code

another bug fixed, more to go

fix another invariant (rounding will lead to very small negative value)

introduce Terrain and Territory

fix merge mistakes
This commit is contained in:
Christof Petig 2021-03-14 03:18:32 +00:00 committed by Imbris
parent fcdd7f1c8b
commit 15b11d9154
49 changed files with 3115 additions and 834 deletions

View File

@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reset button for graphics settings
- Gave weapons critical strike {chance, multiplier} stats
- A system to add glow and reflection effects to figures (i.e: characters, armour, weapons, etc.)
- Merchants will trade wares with players
### Changed

2
Cargo.lock generated
View File

@ -5586,6 +5586,7 @@ dependencies = [
"spin_sleep",
"structopt",
"tracing",
"tracing-subscriber",
"uuid",
"vek 0.14.1",
"veloren-common-base",
@ -5873,6 +5874,7 @@ name = "veloren-world"
version = "0.8.0"
dependencies = [
"arr_macro",
"assets_manager",
"bincode",
"bitvec",
"criterion",

View File

@ -0,0 +1,44 @@
(
loot_tables: [
// balance the loot tables against each other (higher= more common= smaller price)
// the fact that loot tables have an own probability not accessible outside of the lottery call doesn't help here
(0.5,"common.loot_tables.loot_table_animal_ice"),
(4,"common.loot_tables.loot_table_animal_parts"),
(1,"common.loot_tables.loot_table_armor_cloth"),
(0.01,"common.loot_tables.loot_table_armor_heavy"),
(0.1,"common.loot_tables.loot_table_armor_light"),
(0.1,"common.loot_tables.loot_table_armor_misc"),
(0.5,"common.loot_tables.loot_table_armor_nature"),
(0.1,"common.loot_tables.loot_table_cave_large"),
(0.1,"common.loot_tables.loot_table_consumables"),
(1,"common.loot_tables.loot_table_crafting"),
(0.005,"common.loot_tables.loot_table_cultists"),
(1,"common.loot_tables.loot_table_fish"),
(1,"common.loot_tables.loot_table_food"),
(1,"common.loot_tables.loot_table_humanoids"),
(1,"common.loot_tables.loot_table_maneater"),
(0.0001,"common.loot_tables.loot_table_mindflayer"),
(0.001,"common.loot_tables.loot_table_miniboss"),
(0.05,"common.loot_tables.loot_table_raptor"),
(1,"common.loot_tables.loot_table_rocks"),
(1,"common.loot_tables.loot_table"),
(0.04,"common.loot_tables.loot_table_saurok"),
(0.02,"common.loot_tables.loot_table_troll"),
(2,"common.loot_tables.loot_table_villager"),
(1,"common.loot_tables.loot_table_weapon_common"),
(0.008,"common.loot_tables.loot_table_weapon_rare"),
(0.01,"common.loot_tables.loot_table_weapon_uncommon"),
(0.01,"common.loot_tables.loot_table_wendigo"),
// we probably want to include all the scattered scatter information
//(0.5,"common.cave_scatter"),
],
// this is the amount of that good the most common item represents
// so basically this table balances the goods against each other (higher=less valuable)
good_scaling: [
(Potions, 0.5), // common.items.consumable.potion_minor
(Food, 3.0), // common.items.food.mushroom
(Coin, 1.0), // common.items.utility.coins
(Armor, 0.3), // common.items.armor.misc.pants.worker_blue
(Tools, 1.0), // common.items.weapons.staff.starter_staff
(Ingredients, 5.0), // common.items.crafting_ing.leather_scraps
])

View File

@ -17,7 +17,7 @@
(1, "common.items.armor.misc.chest.worker_red_1"),
(1, "common.items.armor.misc.chest.worker_yellow_0"),
(1, "common.items.armor.misc.chest.worker_yellow_1"),
(1, "common.items.armor.misc.pants.worker_blue_0"),
(1, "common.items.armor.misc.pants.worker_blue"),
// Utility
(0.05, "common.items.utility.collar"),
// Food

View File

@ -0,0 +1,110 @@
// we use a vector to easily generate a key into all the economic data containers
([
(
name: "Cook",
orders: [ (Flour, 12.0), (Meat, 4.0), (Wood, 1.5), (Stone, 1.0) ],
products: [ (Food, 16.0) ],
),
(
name: "Lumberjack",
orders: [ (Territory(Forest), 0.5), (Tools, 0.1) ],
products: [ (Wood, 0.5)],
),
(
name: "Miner",
orders: [ (Territory(Mountain), 0.5), (Tools, 0.1) ],
products: [ (Stone, 0.5) ],
),
(
name: "Fisher",
orders: [ (Territory(Lake), 4.0), (Tools, 0.02) ],
products: [ (Meat, 4.0) ],
),
(
name: "Hunter", // Hunter operate outside of uncontrolled areas and resemble guards
// due to the low number of products we tune down the Armor,Tools,Potions in comparison
orders: [ (Armor, 0.1), (Tools, 0.1), (Potions, 1.0), (Terrain(Forest), 4.0) ],
products: [ (Meat, 4.0) ],
),
(
name: "Hunter2", // Hunter operate outside of uncontrolled areas and resemble guards
// due to the low number of products we tune down the Armor,Tools,Potions in comparison
orders: [ (Armor, 0.1), (Tools, 0.1), (Potions, 1.0), (Terrain(Desert), 5.0) ],
products: [ (Meat, 3.0) ],
),
(
name: "Farmer",
orders: [ (Territory(Grassland), 2.0), (Tools, 0.05) ],
products: [ (Flour, 2.0) ],
),
(
name: "Brewer",
orders: [ (Ingredients, 2.0), (Flour, 2.0) ],
products: [ (Potions, 6.0) ],
),
(
name: "Bladesmith",
orders: [ (Ingredients, 4.0), (Wood, 1.0) ],
products: [ (Tools, 2.0) ],
),
(
name: "Blacksmith",
orders: [ (Ingredients, 8.0), (Wood, 2.0) ],
products: [ (Armor, 4.0) ],
),
(
name: "Naval Guard",
orders: [ (Armor, 0.3), (Tools, 0.3), (Potions, 4.0), (Terrain(Lake), 50) ],
products: [ (Territory(Lake), 50) ],
),
(
name: "Mountain Guard",
orders: [ (Armor, 0.4), (Tools, 0.4), (Potions, 3.5), (Terrain(Mountain), 50) ],
products: [ (Territory(Mountain), 50) ],
),
(
name: "Field Guard",
orders: [ (Armor, 0.5), (Tools, 0.3), (Potions, 3.0), (Terrain(Grassland), 50) ],
products: [ (Territory(Grassland), 50) ],
),
(
name: "Road Patrol",
orders: [ (Armor, 0.5), (Tools, 0.3), (Potions, 3.0), ],
products: [ (RoadSecurity, 50) ],
),
(
name: "Ranger",
orders: [ (Armor, 0.5), (Tools, 0.3), (Potions, 3.0), (Terrain(Forest), 50) ],
products: [ (Territory(Forest), 50) ],
),
(
name: "Armed Gatherer", // similar to guards
orders: [ (Armor, 0.5), (Tools, 0.3), (Potions, 3.0), (Terrain(Desert), 10) ],
products: [ (Ingredients, 10) ],
),
(
name: "Gatherer", // operates on controlled area
orders: [ (Territory(Grassland), 0.1) ],
products: [ (Ingredients, 4) ],
),
(
name: "Gatherer2", // operates on controlled area
orders: [ (Territory(Forest), 0.1) ],
products: [ (Ingredients, 4) ],
),
(
name: "Gatherer3", // operates on controlled area
orders: [ (Territory(Mountain), 0.3) ],
products: [ (Ingredients, 4) ],
),
(
name: "Merchant",
orders: [ (RoadSecurity, 0.5) ],
products: [ (Transportation, 30.0) ],
),
(
name: "_",
orders: [ (Food, 0.5) ],
products: [],
),
])

View File

@ -41,10 +41,12 @@ use common::{
use common_base::span;
use common_net::{
msg::{
self, validate_chat_msg, world_msg::SiteInfo, ChatMsgValidationError, ClientGeneral,
ClientMsg, ClientRegister, ClientType, DisconnectReason, InviteAnswer, Notification,
PingMsg, PlayerInfo, PlayerListUpdate, PresenceKind, RegisterError, ServerGeneral,
ServerInfo, ServerInit, ServerRegisterAnswer, MAX_BYTES_CHAT_MSG,
self, validate_chat_msg,
world_msg::{EconomyInfo, SiteId, SiteInfo},
ChatMsgValidationError, ClientGeneral, ClientMsg, ClientRegister, ClientType,
DisconnectReason, InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate,
PresenceKind, RegisterError, ServerGeneral, ServerInfo, ServerInit, ServerRegisterAnswer,
MAX_BYTES_CHAT_MSG,
},
sync::WorldSyncExt,
};
@ -126,6 +128,11 @@ impl WorldData {
pub fn max_chunk_alt(&self) -> f32 { self.map.2.y }
}
pub struct SiteInfoRich {
pub site: SiteInfo,
pub economy: Option<EconomyInfo>,
}
pub struct Client {
registered: bool,
presence: Option<PresenceKind>,
@ -134,7 +141,7 @@ pub struct Client {
world_data: WorldData,
player_list: HashMap<Uid, PlayerInfo>,
character_list: CharacterList,
sites: Vec<SiteInfo>,
sites: HashMap<SiteId, SiteInfoRich>,
pub chat_mode: ChatMode,
recipe_book: RecipeBook,
available_recipes: HashSet<String>,
@ -440,7 +447,15 @@ impl Client {
},
player_list: HashMap::new(),
character_list: CharacterList::default(),
sites,
sites: sites
.iter()
.map(|s| {
(s.id, SiteInfoRich {
site: s.clone(),
economy: None,
})
})
.collect(),
recipe_book,
available_recipes: HashSet::default(),
chat_mode: ChatMode::default(),
@ -575,6 +590,7 @@ impl Client {
| ClientGeneral::PlayerPhysics { .. }
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_)
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_) => &mut self.in_game_stream,
//Only in game, terrain
ClientGeneral::TerrainChunkRequest { .. } => &mut self.terrain_stream,
@ -777,7 +793,9 @@ impl Client {
}
/// Unstable, likely to be removed in a future release
pub fn sites(&self) -> &[SiteInfo] { &self.sites }
pub fn sites(&self) -> &HashMap<SiteId, SiteInfoRich> { &self.sites }
pub fn sites_mut(&mut self) -> &mut HashMap<SiteId, SiteInfoRich> { &mut self.sites }
pub fn enable_lantern(&mut self) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::EnableLantern));
@ -1038,6 +1056,10 @@ impl Client {
}
}
pub fn request_site_economy(&mut self, id: SiteId) {
self.send_msg(ClientGeneral::RequestSiteInfo(id))
}
pub fn inventories(&self) -> ReadStorage<comp::Inventory> { self.state.read_storage() }
/// Send a chat message to the server.
@ -1604,6 +1626,11 @@ impl Client {
frontend_events.push(Event::TradeComplete { result, trade })
}
},
ServerGeneral::SiteEconomy(economy) => {
if let Some(rich) = self.sites_mut().get_mut(&economy.id) {
rich.economy = Some(economy);
}
},
_ => unreachable!("Not a in_game message"),
}
Ok(())

View File

@ -66,6 +66,9 @@ specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", rev = "b65fb22
#bench
criterion = "0.3"
#test
tracing-subscriber = { version = "0.2.15", default-features = false, features = ["fmt", "chrono", "ansi", "smallvec", "env-filter"] }
[[bench]]
name = "chonk_benchmark"
harness = false

View File

@ -1,4 +1,4 @@
use super::PingMsg;
use super::{world_msg::SiteId, PingMsg};
use common::{
character::CharacterId,
comp,
@ -73,6 +73,7 @@ pub enum ClientGeneral {
UnlockSkill(Skill),
RefundSkill(Skill),
UnlockSkillGroup(SkillGroupKind),
RequestSiteInfo(SiteId),
//Only in Game, via terrain stream
TerrainChunkRequest {
key: Vec2<i32>,
@ -115,6 +116,7 @@ impl ClientMsg {
| ClientGeneral::TerrainChunkRequest { .. }
| ClientGeneral::UnlockSkill(_)
| ClientGeneral::RefundSkill(_)
| ClientGeneral::RequestSiteInfo(_)
| ClientGeneral::UnlockSkillGroup(_) => {
c_type == ClientType::Game && presence.is_some()
},

View File

@ -1,4 +1,4 @@
use super::{ClientType, EcsCompPacket, PingMsg};
use super::{world_msg::EconomyInfo, ClientType, EcsCompPacket, PingMsg};
use crate::sync;
use authc::AuthClientError;
use common::{
@ -127,6 +127,8 @@ pub enum ServerGeneral {
Notification(Notification),
UpdatePendingTrade(TradeId, PendingTrade),
FinishedTrade(TradeResult),
/// Economic information about sites
SiteEconomy(EconomyInfo),
}
impl ServerGeneral {
@ -235,7 +237,8 @@ impl ServerMsg {
| ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _)
| ServerGeneral::FinishedTrade(_) => {
| ServerGeneral::FinishedTrade(_)
| ServerGeneral::SiteEconomy(_) => {
c_type == ClientType::Game && presence.is_some()
},
// Always possible

View File

@ -1,5 +1,6 @@
use common::grid::Grid;
use common::{grid::Grid, trade::Good};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use vek::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -124,8 +125,11 @@ pub struct WorldMapMsg {
pub sites: Vec<SiteInfo>,
}
pub type SiteId = common::trade::SiteId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiteInfo {
pub id: SiteId,
pub kind: SiteKind,
pub wpos: Vec2<i32>,
pub name: Option<String>,
@ -140,3 +144,15 @@ pub enum SiteKind {
Cave,
Tree,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EconomyInfo {
pub id: SiteId,
pub population: u32,
pub stock: HashMap<Good, f32>,
pub labor_values: HashMap<Good, f32>,
pub values: HashMap<Good, f32>,
pub labors: Vec<f32>,
pub last_exports: HashMap<Good, f32>,
pub resources: HashMap<Good, f32>,
}

View File

@ -2,6 +2,7 @@ use crate::{
comp::{humanoid, quadruped_low, quadruped_medium, quadruped_small, Body},
path::Chaser,
rtsim::RtSimController,
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid,
};
use specs::{Component, Entity as EcsEntity};
@ -10,6 +11,7 @@ use std::collections::VecDeque;
use vek::*;
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
#[derive(Eq, PartialEq)]
pub enum Tactic {
@ -173,7 +175,17 @@ impl<'a> From<&'a Body> for Psyche {
pub enum AgentEvent {
/// Engage in conversation with entity with Uid
Talk(Uid),
Trade(Uid),
TradeInvite(Uid),
FinishedTrade(TradeResult),
UpdatePendingTrade(
// this data structure is large so box it to keep AgentEvent small
Box<(
TradeId,
PendingTrade,
SitePrices,
[Option<ReducedInventory>; 2],
)>,
),
// Add others here
}
@ -192,6 +204,8 @@ pub struct Agent {
/// Does the agent talk when e.g. hit by the player
// TODO move speech patterns into a Behavior component
pub can_speak: bool,
pub trade_for_site: Option<SiteId>,
pub trading: bool,
pub psyche: Psyche,
pub inbox: VecDeque<AgentEvent>,
pub action_timer: f32,
@ -207,12 +221,14 @@ impl Agent {
pub fn new(
patrol_origin: Option<Vec3<f32>>,
can_speak: bool,
trade_for_site: Option<SiteId>,
body: &Body,
no_flee: bool,
) -> Self {
Agent {
patrol_origin,
can_speak,
trade_for_site,
psyche: if no_flee {
Psyche { aggro: 1.0 }
} else {

View File

@ -264,6 +264,9 @@ impl ItemDef {
}
}
// currently needed by trade_pricing
pub fn id(&self) -> &str { &self.item_definition_id }
#[cfg(test)]
pub fn new_test(
item_definition_id: String,

View File

@ -1,11 +1,15 @@
use crate::comp::{
biped_large, biped_small, golem,
inventory::{
loadout::Loadout,
slot::{ArmorSlot, EquipSlot},
use crate::{
comp::{
biped_large, biped_small, golem,
inventory::{
loadout::Loadout,
slot::{ArmorSlot, EquipSlot},
trade_pricing::TradePricing,
},
item::{tool::ToolKind, Item, ItemKind},
object, quadruped_low, quadruped_medium, theropod, Body,
},
item::{tool::ToolKind, Item, ItemKind},
object, quadruped_low, quadruped_medium, theropod, Body,
trade::{Good, SiteInformation},
};
use rand::Rng;
@ -39,6 +43,7 @@ pub enum LoadoutConfig {
Myrmidon,
Guard,
Villager,
Merchant,
Outcast,
Highwayman,
Bandit,
@ -78,6 +83,7 @@ impl LoadoutBuilder {
body: Body,
mut main_tool: Option<Item>,
config: Option<LoadoutConfig>,
economy: Option<&SiteInformation>,
) -> Self {
// If no main tool is passed in, checks if species has a default main tool
if main_tool.is_none() {
@ -502,6 +508,128 @@ impl LoadoutBuilder {
_ => None,
})
.build(),
Merchant => {
let mut backpack =
Item::new_from_asset_expect("common.items.armor.misc.back.backpack");
let mut coins = Item::new_from_asset_expect("common.items.utility.coins");
coins
.set_amount(
(economy
.map(|e| e.unconsumed_stock.get(&Good::Coin))
.flatten()
.copied()
.unwrap_or_default()
.round() as u32)
.max(1),
)
.expect("coins should be stackable");
backpack.slots_mut()[0] = Some(coins);
let armor = economy
.map(|e| e.unconsumed_stock.get(&Good::Armor))
.flatten()
.copied()
.unwrap_or_default()
/ 10.0;
for i in 1..18 {
backpack.slots_mut()[i] = Some(Item::new_from_asset_expect(
&TradePricing::random_item(Good::Armor, armor),
));
}
let mut bag1 = Item::new_from_asset_expect(
"common.items.armor.misc.bag.reliable_backpack",
);
let weapon = economy
.map(|e| e.unconsumed_stock.get(&Good::Tools))
.flatten()
.copied()
.unwrap_or_default()
/ 10.0;
for i in 0..16 {
bag1.slots_mut()[i] = Some(Item::new_from_asset_expect(
&TradePricing::random_item(Good::Tools, weapon),
));
}
let mut bag2 = Item::new_from_asset_expect(
"common.items.armor.misc.bag.reliable_backpack",
);
let ingredients = economy
.map(|e| e.unconsumed_stock.get(&Good::Ingredients))
.flatten()
.copied()
.unwrap_or_default()
/ 10.0;
for i in 0..16 {
bag2.slots_mut()[i] = Some(Item::new_from_asset_expect(
&TradePricing::random_item(Good::Ingredients, ingredients),
));
}
let mut bag3 = Item::new_from_asset_expect(
"common.items.armor.misc.bag.reliable_backpack",
);
let food = economy
.map(|e| e.unconsumed_stock.get(&Good::Food))
.flatten()
.copied()
.unwrap_or_default()
/ 10.0;
for i in 0..16 {
bag3.slots_mut()[i] = Some(Item::new_from_asset_expect(
&TradePricing::random_item(Good::Food, food),
));
}
let mut bag4 = Item::new_from_asset_expect(
"common.items.armor.misc.bag.reliable_backpack",
);
let potions = economy
.map(|e| e.unconsumed_stock.get(&Good::Potions))
.flatten()
.copied()
.unwrap_or_default()
/ 10.0;
for i in 0..16 {
bag4.slots_mut()[i] = Some(Item::new_from_asset_expect(
&TradePricing::random_item(Good::Potions, potions),
));
}
LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
"common.items.armor.twigsflowers.shoulder",
)))
.chest(Some(Item::new_from_asset_expect(
"common.items.armor.twigsflowers.chest",
)))
.belt(Some(Item::new_from_asset_expect(
"common.items.armor.twigsflowers.belt",
)))
.hands(Some(Item::new_from_asset_expect(
"common.items.armor.twigsflowers.hand",
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.twigsflowers.pants",
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.twigsflowers.foot",
)))
.lantern(Some(Item::new_from_asset_expect(
"common.items.lantern.black_0",
)))
.back(Some(backpack))
.neck(Some(Item::new_from_asset_expect(
"common.items.armor.misc.neck.plain_1",
)))
.ring1(Some(Item::new_from_asset_expect(
"common.items.armor.misc.ring.gold",
)))
.ring2(Some(Item::new_from_asset_expect(
"common.items.armor.misc.ring.gold",
)))
.bag(ArmorSlot::Bag1, Some(bag1))
.bag(ArmorSlot::Bag2, Some(bag2))
.bag(ArmorSlot::Bag3, Some(bag3))
.bag(ArmorSlot::Bag4, Some(bag4))
.build()
},
Outcast => LoadoutBuilder::new()
.active_item(active_item)
.shoulder(Some(Item::new_from_asset_expect(
@ -824,5 +952,10 @@ impl LoadoutBuilder {
self
}
pub fn bag(mut self, which: ArmorSlot, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(which), item);
self
}
pub fn build(self) -> Loadout { self.0 }
}

View File

@ -26,6 +26,7 @@ pub mod loadout_builder;
pub mod slot;
#[cfg(test)] mod test;
#[cfg(test)] mod test_helpers;
pub mod trade_pricing;
pub type InvSlot = Option<Item>;
const DEFAULT_INVENTORY_SLOTS: usize = 18;

View File

@ -0,0 +1,368 @@
use crate::{
assets::{self, AssetExt},
recipe::{default_recipe_book, RecipeInput},
trade::Good,
};
use assets_manager::AssetGuard;
use hashbrown::HashMap;
use lazy_static::lazy_static;
use serde::Deserialize;
use tracing::info;
#[derive(Debug)]
struct Entry {
probability: f32,
item: String,
}
type Entries = Vec<(String, f32)>;
const PRICING_DEBUG: bool = false;
#[derive(Default, Debug)]
pub struct TradePricing {
tools: Entries,
armor: Entries,
potions: Entries,
food: Entries,
ingredients: Entries,
other: Entries,
coin_scale: f32,
// rng: ChaChaRng,
// get amount of material per item
material_cache: HashMap<String, (Good, f32)>,
}
lazy_static! {
static ref TRADE_PRICING: TradePricing = TradePricing::read();
}
struct ProbabilityFile {
pub content: Vec<(f32, String)>,
}
impl assets::Asset for ProbabilityFile {
type Loader = assets::LoadFrom<Vec<(f32, String)>, assets::RonLoader>;
const EXTENSION: &'static str = "ron";
}
impl From<Vec<(f32, String)>> for ProbabilityFile {
fn from(content: Vec<(f32, String)>) -> ProbabilityFile { Self { content } }
}
#[derive(Debug, Deserialize)]
struct TradingPriceFile {
pub loot_tables: Vec<(f32, String)>,
pub good_scaling: Vec<(Good, f32)>, // the amount of Good equivalent to the most common item
}
impl assets::Asset for TradingPriceFile {
type Loader = assets::LoadFrom<TradingPriceFile, assets::RonLoader>;
const EXTENSION: &'static str = "ron";
}
#[derive(Debug)]
struct RememberedRecipe {
output: String,
amount: u32,
material_cost: f32,
input: Vec<(String, u32)>,
}
impl TradePricing {
const COIN_ITEM: &'static str = "common.items.utility.coins";
const CRAFTING_FACTOR: f32 = 0.95;
// increase price a bit compared to sum of ingredients
const INVEST_FACTOR: f32 = 0.33;
const UNAVAILABLE_PRICE: f32 = 1000000.0;
// add this much of a non-consumed crafting tool price
fn get_list(&self, good: Good) -> &Entries {
match good {
Good::Armor => &self.armor,
Good::Tools => &self.tools,
Good::Potions => &self.potions,
Good::Food => &self.food,
Good::Ingredients => &self.ingredients,
_ => panic!("invalid good"),
}
}
fn get_list_mut(&mut self, good: Good) -> &mut Entries {
match good {
Good::Armor => &mut self.armor,
Good::Tools => &mut self.tools,
Good::Potions => &mut self.potions,
Good::Food => &mut self.food,
Good::Ingredients => &mut self.ingredients,
_ => panic!("invalid good"),
}
}
fn get_list_by_path(&self, name: &str) -> &Entries {
match name {
_ if name.starts_with("common.items.crafting_ing.") => &self.ingredients,
_ if name.starts_with("common.items.armor.") => &self.armor,
_ if name.starts_with("common.items.glider.") => &self.other,
_ if name.starts_with("common.items.weapons.") => &self.tools,
_ if name.starts_with("common.items.consumable.") => &self.potions,
_ if name.starts_with("common.items.food.") => &self.food,
_ if name.starts_with("common.items.utility.") => &self.other,
_ if name.starts_with("common.items.boss_drops.") => &self.other,
_ if name.starts_with("common.items.ore.") => &self.ingredients,
_ if name.starts_with("common.items.flowers.") => &self.ingredients,
_ if name.starts_with("common.items.crafting_tools.") => &self.other,
_ => {
info!("unknown loot item {}", name);
&self.other
},
}
}
fn get_list_by_path_mut(&mut self, name: &str) -> &mut Entries {
match name {
_ if name.starts_with("common.items.crafting_ing.") => &mut self.ingredients,
_ if name.starts_with("common.items.armor.") => &mut self.armor,
_ if name.starts_with("common.items.glider.") => &mut self.other,
_ if name.starts_with("common.items.weapons.") => &mut self.tools,
_ if name.starts_with("common.items.consumable.") => &mut self.potions,
_ if name.starts_with("common.items.food.") => &mut self.food,
_ if name.starts_with("common.items.utility.") => &mut self.other,
_ if name.starts_with("common.items.boss_drops.") => &mut self.other,
_ if name.starts_with("common.items.ore.") => &mut self.ingredients,
_ if name.starts_with("common.items.flowers.") => &mut self.ingredients,
_ if name.starts_with("common.items.crafting_tools.") => &mut self.other,
_ => {
info!("unknown loot item {}", name);
&mut self.other
},
}
}
fn read() -> Self {
fn add(entryvec: &mut Entries, itemname: &str, probability: f32) {
let val = entryvec.iter_mut().find(|j| *j.0 == *itemname);
if let Some(r) = val {
if PRICING_DEBUG {
info!("Update {} {}+{}", r.0, r.1, probability);
}
r.1 += probability;
} else {
if PRICING_DEBUG {
info!("New {} {}", itemname, probability);
}
entryvec.push((itemname.to_string(), probability));
}
}
fn sort_and_normalize(entryvec: &mut Entries, scale: f32) {
if !entryvec.is_empty() {
entryvec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let rescale = scale / entryvec.last().unwrap().1;
for i in entryvec.iter_mut() {
i.1 *= rescale;
}
}
}
fn get_scaling(contents: &AssetGuard<TradingPriceFile>, good: Good) -> f32 {
contents
.good_scaling
.iter()
.find(|i| i.0 == good)
.map(|i| i.1)
.unwrap_or(1.0)
}
let mut result = TradePricing::default();
let files = TradingPriceFile::load_expect("common.item_price_calculation");
let contents = files.read();
for i in contents.loot_tables.iter() {
if PRICING_DEBUG {
info!(?i);
}
let loot = ProbabilityFile::load_expect(&i.1);
for j in loot.read().content.iter() {
add(&mut result.get_list_by_path_mut(&j.1), &j.1, i.0 * j.0);
}
}
// Apply recipe book
let book = default_recipe_book().read();
let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
for (_, r) in book.iter() {
ordered_recipes.push(RememberedRecipe {
output: r.output.0.id().into(),
amount: r.output.1,
material_cost: TradePricing::UNAVAILABLE_PRICE,
input: r
.inputs
.iter()
.filter(|i| matches!(i.0, RecipeInput::Item(_)))
.map(|i| {
(
if let RecipeInput::Item(it) = &i.0 {
it.id().into()
} else {
panic!("recipe logic broken");
},
i.1,
)
})
.collect(),
});
}
// look up price (inverse frequency) of an item
fn price_lookup(s: &TradePricing, name: &str) -> f32 {
let vec = s.get_list_by_path(name);
vec.iter()
.find(|(n, _)| n == name)
.map(|(_, freq)| 1.0 / freq)
// even if we multiply by INVEST_FACTOR we need to remain above UNAVAILABLE_PRICE (add 1.0 to compensate rounding errors)
.unwrap_or(TradePricing::UNAVAILABLE_PRICE/TradePricing::INVEST_FACTOR+1.0)
}
fn calculate_material_cost(s: &TradePricing, r: &RememberedRecipe) -> f32 {
r.input
.iter()
.map(|(name, amount)| {
price_lookup(s, name) * (*amount as f32).max(TradePricing::INVEST_FACTOR)
})
.sum()
}
// re-look up prices and sort the vector by ascending material cost, return
// whether first cost is finite
fn price_sort(s: &TradePricing, vec: &mut Vec<RememberedRecipe>) -> bool {
if !vec.is_empty() {
for e in vec.iter_mut() {
e.material_cost = calculate_material_cost(s, e);
}
vec.sort_by(|a, b| a.material_cost.partial_cmp(&b.material_cost).unwrap());
//info!(?vec);
vec.first().unwrap().material_cost < TradePricing::UNAVAILABLE_PRICE
} else {
false
}
}
// re-evaluate prices based on crafting tables
// (start with cheap ones to avoid changing material prices after evaluation)
while price_sort(&result, &mut ordered_recipes) {
ordered_recipes.retain(|e| {
if e.material_cost < TradePricing::UNAVAILABLE_PRICE {
let actual_cost = calculate_material_cost(&result, e);
add(
&mut result.get_list_by_path_mut(&e.output),
&e.output,
(e.amount as f32) / actual_cost * TradePricing::CRAFTING_FACTOR,
);
false
} else {
true
}
});
//info!(?ordered_recipes);
}
let good_list = [
Good::Armor,
Good::Tools,
Good::Potions,
Good::Food,
Good::Ingredients,
];
for &g in good_list.iter() {
sort_and_normalize(result.get_list_mut(g), get_scaling(&contents, g));
let mut materials = result
.get_list(g)
.iter()
.map(|i| (i.0.clone(), (g, 1.0 / i.1)))
.collect::<Vec<_>>();
result.material_cache.extend(materials.drain(..));
}
result.coin_scale = get_scaling(&contents, Good::Coin);
result
}
fn random_item_impl(&self, good: Good, amount: f32) -> String {
if good == Good::Coin {
TradePricing::COIN_ITEM.into()
} else {
let table = self.get_list(good);
let upper = table.len();
let lower = table
.iter()
.enumerate()
.find(|i| i.1.1 * amount >= 1.0)
.map(|i| i.0)
.unwrap_or(upper - 1);
let index = (rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
//.gen_range(lower..upper);
table.get(index).unwrap().0.clone()
}
}
pub fn random_item(good: Good, amount: f32) -> String {
TRADE_PRICING.random_item_impl(good, amount)
}
pub fn get_material(item: &str) -> (Good, f32) {
if item == TradePricing::COIN_ITEM {
(Good::Coin, 1.0 / TRADE_PRICING.coin_scale)
} else {
TRADE_PRICING
.material_cache
.get(item)
.cloned()
.unwrap_or((Good::Terrain(crate::terrain::BiomeKind::Void), 0.0))
}
}
#[cfg(test)]
fn instance() -> &'static Self { &TRADE_PRICING }
#[cfg(test)]
fn print_sorted(&self) {
fn printvec(x: &str, e: &[(String, f32)]) {
println!("{}", x);
for i in e.iter() {
println!("{} {}", i.0, 1.0 / i.1);
}
}
printvec("Armor", &self.armor);
printvec("Tools", &self.tools);
printvec("Potions", &self.potions);
printvec("Food", &self.food);
printvec("Ingredients", &self.ingredients);
println!("{} {}", TradePricing::COIN_ITEM, self.coin_scale);
}
}
#[cfg(test)]
mod tests {
use crate::{comp::inventory::trade_pricing::TradePricing, trade::Good};
use tracing::{info, Level};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
FmtSubscriber,
};
fn init() {
FmtSubscriber::builder()
.with_max_level(Level::ERROR)
.with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into()))
.init();
}
#[test]
fn test_prices() {
init();
info!("init");
TradePricing::instance().print_sorted();
info!("Armor 5 {}", TradePricing::random_item(Good::Armor, 5.0));
info!("Armor 5 {}", TradePricing::random_item(Good::Armor, 5.0));
info!("Armor 5 {}", TradePricing::random_item(Good::Armor, 5.0));
info!("Armor 5 {}", TradePricing::random_item(Good::Armor, 5.0));
info!("Armor 5 {}", TradePricing::random_item(Good::Armor, 5.0));
}
}

View File

@ -16,6 +16,8 @@ use specs::Entity as EcsEntity;
use std::{collections::VecDeque, ops::DerefMut, sync::Mutex};
use vek::*;
pub type SiteId = u64;
pub enum LocalEvent {
/// Applies upward force to entity's `Vel`
Jump(EcsEntity),
@ -148,6 +150,10 @@ pub enum ServerEvent {
entity: EcsEntity,
change: i32,
},
RequestSiteInfo {
entity: EcsEntity,
id: SiteId,
},
}
pub struct EventBus<E> {

View File

@ -2,6 +2,7 @@ use crate::{
comp::{self, humanoid, inventory::loadout_builder::LoadoutConfig, Alignment, Body, Item},
npc::{self, NPC_NAMES},
skillset_builder::SkillSetConfig,
trade::SiteInformation,
};
use vek::*;
@ -27,6 +28,9 @@ pub struct EntityInfo {
pub loadout_config: Option<LoadoutConfig>,
pub skillset_config: Option<SkillSetConfig>,
pub pet: Option<Box<EntityInfo>>,
// we can't use DHashMap, do we want to move that into common?
pub trading_information: Option<crate::trade::SiteInformation>,
//Option<hashbrown::HashMap<crate::trade::Good, (f32, f32)>>, /* price and available amount */
}
impl EntityInfo {
@ -47,6 +51,7 @@ impl EntityInfo {
loadout_config: None,
skillset_config: None,
pet: None,
trading_information: None,
}
}
@ -151,6 +156,12 @@ impl EntityInfo {
});
self
}
// map contains price+amount
pub fn with_economy(mut self, e: &SiteInformation) -> Self {
self.trading_information = Some(e.clone());
self
}
}
#[derive(Default)]

View File

@ -15,6 +15,7 @@ pub enum SkillSetConfig {
Myrmidon,
Guard,
Villager,
Merchant,
Outcast,
Highwayman,
Bandit,
@ -255,7 +256,7 @@ impl SkillSetBuilder {
_ => Self::default(),
}
},
Some(Bandit) => {
Some(Bandit) | Some(Merchant) => {
match active_item {
Some(ToolKind::Sword) => {
// Sword

View File

@ -5,6 +5,7 @@ use std::{
ops::{Index, IndexMut},
};
/// Type safe index into Store
pub struct Id<T> {
idx: u32,
gen: u32,
@ -58,6 +59,8 @@ struct Entry<T> {
item: Option<T>,
}
/// A general-purpose high performance allocator, basically Vec with type safe
/// indices (Id)
pub struct Store<T> {
entries: Vec<Entry<T>>,
len: usize,
@ -183,6 +186,22 @@ impl<T> Store<T> {
None
}
}
pub fn recreate_id(&self, i: u64) -> Option<Id<T>> {
if i as usize >= self.entries.len() {
None
} else {
Some(Id {
idx: i as u32,
gen: self
.entries
.get(i as usize)
.map(|e| e.gen)
.unwrap_or_default(),
phantom: PhantomData,
})
}
}
}
impl<T> Index<Id<T>> for Store<T> {

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum BiomeKind {
Void,
Lake,

View File

@ -1,5 +1,6 @@
use crate::{
comp::inventory::{slot::InvSlotId, Inventory},
terrain::BiomeKind,
uid::Uid,
};
use hashbrown::HashMap;
@ -291,3 +292,71 @@ impl Default for Trades {
}
}
}
// we need this declaration in common for Merchant loadout creation, it is not
// directly related to trade between entities, but between sites (more abstract)
// economical information
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum Good {
Territory(BiomeKind),
Flour,
Meat,
Terrain(BiomeKind),
Transportation,
Food,
Wood,
Stone,
Tools, // weapons, farming tools
Armor,
Ingredients, // raw material for Armor+Tools+Potions
Potions,
Coin, // exchange material across sites
RoadSecurity,
}
impl Default for Good {
fn default() -> Self {
Good::Terrain(crate::terrain::BiomeKind::Void) // Arbitrary
}
}
// ideally this would be a real Id<Site> but that is from the world crate
pub type SiteId = u64;
#[derive(Clone, Debug)]
pub struct SiteInformation {
pub id: SiteId,
pub unconsumed_stock: HashMap<Good, f32>,
}
#[derive(Clone, Debug, Default)]
pub struct SitePrices {
pub values: HashMap<Good, f32>,
}
#[derive(Clone, Debug, Default)]
pub struct ReducedInventoryItem {
pub name: String,
pub amount: u32,
}
#[derive(Clone, Debug, Default)]
pub struct ReducedInventory {
pub inventory: HashMap<InvSlotId, ReducedInventoryItem>,
}
impl ReducedInventory {
pub fn from(inventory: &Inventory) -> Self {
let items = inventory
.slots_with_id()
.filter(|(_, it)| it.is_some())
.map(|(sl, it)| {
(sl, ReducedInventoryItem {
name: it.as_ref().unwrap().item_definition_id().to_string(),
amount: it.as_ref().unwrap().amount(),
})
})
.collect();
Self { inventory: items }
}
}

View File

@ -88,6 +88,7 @@ impl Client {
| ServerGeneral::ExitInGameSuccess
| ServerGeneral::InventoryUpdate(_, _)
| ServerGeneral::SetViewDistance(_)
| ServerGeneral::SiteEconomy(_)
| ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_)
| ServerGeneral::UpdatePendingTrade(_, _)
@ -160,6 +161,7 @@ impl Client {
| ServerGeneral::SetViewDistance(_)
| ServerGeneral::Outcomes(_)
| ServerGeneral::Knockback(_)
| ServerGeneral::SiteEconomy(_)
| ServerGeneral::UpdatePendingTrade(_, _)
| ServerGeneral::FinishedTrade(_) => {
PreparedMsg::new(2, &g, &self.in_game_stream)

View File

@ -841,7 +841,8 @@ fn handle_spawn(
let body = body();
let loadout = LoadoutBuilder::build_loadout(body, None, None).build();
let loadout =
LoadoutBuilder::build_loadout(body, None, None, None).build();
let inventory = Inventory::new_with_loadout(loadout);

View File

@ -0,0 +1,87 @@
use crate::{client::Client, Server};
use common_net::msg::{world_msg::EconomyInfo, ServerGeneral};
use specs::{Entity as EcsEntity, WorldExt};
use std::collections::HashMap;
#[cfg(not(feature = "worldgen"))]
pub fn handle_site_info(server: &Server, entity: EcsEntity, id: u64) {
let info = EconomyInfo {
id,
population: 0,
stock: HashMap::new(),
labor_values: HashMap::new(),
values: HashMap::new(),
labors: Vec::new(),
};
let msg = ServerGeneral::SiteEconomy(info);
server
.state
.ecs()
.read_storage::<Client>()
.get(entity)
.map(|c| c.send(msg));
}
#[cfg(feature = "worldgen")]
pub fn handle_site_info(server: &Server, entity: EcsEntity, id: u64) {
let site_id = server.index.sites.recreate_id(id);
let info = if let Some(site_id) = site_id {
let site = server.index.sites.get(site_id);
EconomyInfo {
id,
population: site.economy.pop.floor() as u32,
stock: site.economy.stocks.iter().map(|(g, a)| (g, *a)).collect(),
labor_values: site
.economy
.labor_values
.iter()
.filter(|a| a.1.is_some())
.map(|(g, a)| (g, a.unwrap()))
.collect(),
values: site
.economy
.values
.iter()
.filter(|a| a.1.is_some())
.map(|(g, a)| (g, a.unwrap()))
.collect(),
labors: site.economy.labors.iter().map(|(_, a)| (*a)).collect(),
last_exports: site
.economy
.last_exports
.iter()
.map(|(g, a)| (g, *a))
.collect(),
resources: site
.economy
.natural_resources
.chunks_per_resource
.iter()
.map(|(g, a)| {
(
g,
((*a) as f32) * site.economy.natural_resources.average_yield_per_chunk[g],
)
})
.collect(),
}
} else {
EconomyInfo {
id,
population: 0,
stock: HashMap::new(),
labor_values: HashMap::new(),
values: HashMap::new(),
labors: Vec::new(),
last_exports: HashMap::new(),
resources: HashMap::new(),
}
};
let msg = ServerGeneral::SiteEconomy(info);
server
.state
.ecs()
.read_storage::<Client>()
.get(entity)
.map(|c| c.send(msg));
}

View File

@ -3,6 +3,7 @@ use crate::{client::Client, Server};
use common::{
comp::{
self,
agent::AgentEvent,
group::GroupManager,
invite::{Invite, InviteKind, InviteResponse, PendingInvites},
ChatType,
@ -72,7 +73,7 @@ pub fn handle_invite(
}
}
let agents = state.ecs().read_storage::<comp::Agent>();
let mut agents = state.ecs().write_storage::<comp::Agent>();
let mut invites = state.ecs().write_storage::<Invite>();
if invites.contains(invitee) {
@ -128,8 +129,13 @@ pub fn handle_invite(
kind,
});
}
} else if agents.contains(invitee) {
send_invite();
} else if let Some(agent) = agents.get_mut(invitee) {
if send_invite() {
if let Some(inviter) = uids.get(inviter) {
agent.inbox.push_front(AgentEvent::TradeInvite(*inviter));
invite_sent = true;
}
}
} else if let Some(client) = clients.get(inviter) {
client.send_fallible(ServerGeneral::server_msg(
ChatType::Meta,

View File

@ -11,6 +11,7 @@ use entity_manipulation::{
handle_respawn,
};
use group_manip::handle_group;
use information::handle_site_info;
use interaction::{
handle_lantern, handle_mount, handle_npc_interaction, handle_possess, handle_unmount,
};
@ -23,6 +24,7 @@ use trade::handle_process_trade_action;
mod entity_creation;
mod entity_manipulation;
mod group_manip;
mod information;
mod interaction;
mod inventory_manip;
mod invite;
@ -187,6 +189,7 @@ impl Server {
ServerEvent::ComboChange { entity, change } => {
handle_combo_change(&self, entity, change)
},
ServerEvent::RequestSiteInfo { entity, id } => handle_site_info(&self, entity, id),
}
}

View File

@ -1,7 +1,10 @@
use crate::Server;
use common::{
comp::inventory::{item::MaterialStatManifest, Inventory},
trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades},
comp::{
agent::{Agent, AgentEvent},
inventory::{item::MaterialStatManifest, Inventory},
},
trade::{PendingTrade, ReducedInventory, TradeAction, TradeId, TradeResult, Trades},
};
use common_net::{
msg::ServerGeneral,
@ -11,6 +14,42 @@ use hashbrown::hash_map::Entry;
use specs::{world::WorldExt, Entity as EcsEntity};
use std::cmp::Ordering;
use tracing::{error, trace};
use world::IndexOwned;
fn notify_agent_simple(
mut agents: specs::WriteStorage<Agent>,
entity: EcsEntity,
event: AgentEvent,
) {
if let Some(agent) = agents.get_mut(entity) {
agent.inbox.push_front(event);
}
}
fn notify_agent_prices(
mut agents: specs::WriteStorage<Agent>,
index: &IndexOwned,
entity: EcsEntity,
event: AgentEvent,
) {
if let Some(agent) = agents.get_mut(entity) {
if let AgentEvent::UpdatePendingTrade(boxval) = 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
.inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new((
boxval.0, boxval.1, prices, boxval.3,
))));
}
}
}
/// Invoked when the trade UI is up, handling item changes, accepts, etc
pub fn handle_process_trade_action(
@ -26,7 +65,12 @@ pub fn handle_process_trade_action(
to_notify
.and_then(|u| server.state.ecs().entity_from_uid(u.0))
.map(|e| {
server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined))
server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined));
notify_agent_simple(
server.state.ecs().write_storage::<Agent>(),
e,
AgentEvent::FinishedTrade(TradeResult::Declined),
);
});
} else {
{
@ -43,20 +87,54 @@ pub fn handle_process_trade_action(
}
if let Entry::Occupied(entry) = trades.trades.entry(trade_id) {
let parties = entry.get().parties;
let msg = if entry.get().should_commit() {
if entry.get().should_commit() {
let result = commit_trade(server.state.ecs(), entry.get());
entry.remove();
ServerGeneral::FinishedTrade(result)
for party in parties.iter() {
if let Some(e) = server.state.ecs().entity_from_uid(party.0) {
server.notify_client(e, ServerGeneral::FinishedTrade(result.clone()));
notify_agent_simple(
server.state.ecs().write_storage::<Agent>(),
e,
AgentEvent::FinishedTrade(result.clone()),
);
}
}
} else {
ServerGeneral::UpdatePendingTrade(trade_id, entry.get().clone())
};
// send the updated state to both parties
for party in parties.iter() {
server
.state
.ecs()
.entity_from_uid(party.0)
.map(|e| server.notify_client(e, msg.clone()));
let mut entities: [Option<specs::Entity>; 2] = [None, None];
let mut inventories: [Option<ReducedInventory>; 2] = [None, None];
// sadly there is no map and collect on arrays
for i in 0..2 {
// parties.len()) {
entities[i] = server.state.ecs().entity_from_uid(parties[i].0);
if let Some(e) = entities[i] {
inventories[i] = server
.state
.ecs()
.read_component::<Inventory>()
.get(e)
.map(|i| ReducedInventory::from(i));
}
}
for party in entities.iter() {
if let Some(e) = *party {
server.notify_client(
e,
ServerGeneral::UpdatePendingTrade(trade_id, entry.get().clone()),
);
notify_agent_prices(
server.state.ecs().write_storage::<Agent>(),
&server.index,
e,
AgentEvent::UpdatePendingTrade(Box::new((
trade_id,
entry.get().clone(),
Default::default(),
inventories.clone(),
))),
);
}
}
}
}
}

View File

@ -114,7 +114,7 @@ impl Entity {
"common.items.armor.swift.shoulder",
));
LoadoutBuilder::build_loadout(self.get_body(), Some(main_tool), None)
LoadoutBuilder::build_loadout(self.get_body(), Some(main_tool), None, None)
.back(back)
.lantern(lantern)
.chest(chest)

View File

@ -114,6 +114,7 @@ impl<'a> System<'a> for Sys {
agent: Some(comp::Agent::new(
None,
matches!(body, comp::Body::Humanoid(_)),
None,
&body,
false,
)),

View File

@ -1,9 +1,9 @@
use common::{
comp::{
self,
agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME},
agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
group,
inventory::slot::EquipSlot,
inventory::{slot::EquipSlot, trade_pricing::TradePricing},
invite::InviteResponse,
item::{
tool::{ToolKind, UniqueKind},
@ -19,6 +19,7 @@ use common::{
resources::{DeltaTime, TimeOfDay},
terrain::{Block, TerrainGrid},
time::DayPeriod,
trade::{Good, TradeAction, TradePhase, TradeResult},
uid::{Uid, UidAllocator},
util::Dir,
vol::ReadVol,
@ -79,7 +80,6 @@ pub struct ReadData<'a> {
alignments: ReadStorage<'a, Alignment>,
bodies: ReadStorage<'a, Body>,
mount_states: ReadStorage<'a, MountState>,
//ReadStorage<'a, Invite>,
time_of_day: Read<'a, TimeOfDay>,
light_emitter: ReadStorage<'a, LightEmitter>,
}
@ -456,7 +456,13 @@ impl<'a> AgentData<'a> {
agent.action_timer = 0.1;
}
if agent.action_timer > 0.0 {
if agent.action_timer < DEFAULT_INTERACTION_TIME {
if agent.action_timer
< (if agent.trading {
TRADE_INTERACTION_TIME
} else {
DEFAULT_INTERACTION_TIME
})
{
self.interact(agent, controller, &read_data, event_emitter);
} else {
agent.action_timer = 0.0;
@ -691,71 +697,201 @@ impl<'a> AgentData<'a> {
) {
// TODO: Process group invites
// TODO: Add Group AgentEvent
let accept = false; // set back to "matches!(alignment, Alignment::Npc)" when we got better NPC recruitment mechanics
if accept {
// Clear agent comp
*agent = Agent::default();
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Accept));
} else {
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
}
// let accept = false; // set back to "matches!(alignment, Alignment::Npc)"
// when we got better NPC recruitment mechanics if accept {
// // Clear agent comp
// //*agent = Agent::default();
// controller
// .events
// .push(ControlEvent::InviteResponse(InviteResponse::Accept));
// } else {
// controller
// .events
// .push(ControlEvent::InviteResponse(InviteResponse::Decline));
// }
agent.action_timer += read_data.dt.0;
if agent.can_speak {
if let Some(AgentEvent::Talk(by)) = agent.inbox.pop_back() {
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id()) {
agent.target = Some(Target {
target,
hostile: false,
});
if let Some(tgt_pos) = read_data.positions.get(target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset =
read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
) {
controller.inputs.look_dir = dir;
}
controller.actions.push(ControlAction::Stand);
controller.actions.push(ControlAction::Talk);
if let Some((_travel_to, destination_name)) =
&agent.rtsim_controller.travel_to
{
let msg =
format!("I'm heading to {}! Want to come along?", destination_name);
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
} else {
let msg = "npc.speech.villager".to_string();
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
let msg = agent.inbox.pop_back();
match msg {
Some(AgentEvent::Talk(by)) => {
if agent.can_speak {
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id())
{
agent.target = Some(Target {
target,
hostile: false,
});
if let Some(tgt_pos) = read_data.positions.get(target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset =
read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(
self.pos.0.x,
self.pos.0.y,
self.pos.0.z + eye_offset,
),
) {
controller.inputs.look_dir = dir;
}
controller.actions.push(ControlAction::Stand);
controller.actions.push(ControlAction::Talk);
if let Some((_travel_to, destination_name)) =
&agent.rtsim_controller.travel_to
{
let msg = format!(
"I'm heading to {}! Want to come along?",
destination_name
);
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, msg,
)));
} else {
let msg = "npc.speech.villager".to_string();
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, msg,
)));
}
}
}
}
} else if let Some(Target { target, .. }) = &agent.target {
if let Some(tgt_pos) = read_data.positions.get(*target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset = read_data
.bodies
.get(*target)
.map_or(0.0, |b| b.eye_height());
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
) {
controller.inputs.look_dir = dir;
},
Some(AgentEvent::TradeInvite(_with)) => {
if agent.trade_for_site.is_some() && !agent.trading {
// stand still and looking towards the trading player
controller.actions.push(ControlAction::Stand);
controller.actions.push(ControlAction::Talk);
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Accept));
agent.trading = true;
} else {
// TODO: Provide a hint where to find the closest merchant?
controller
.events
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
}
},
Some(AgentEvent::FinishedTrade(result)) => {
if agent.trading {
match result {
TradeResult::Completed => {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
"Thank you for trading with me!".to_string(),
)))
},
_ => event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid,
"Maybe another time, have a good day!".to_string(),
))),
}
agent.trading = false;
}
},
Some(AgentEvent::UpdatePendingTrade(boxval)) => {
let (tradeid, pending, prices, inventories) = *boxval;
if agent.trading {
// I assume player is [0], agent is [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 balance0: f32 = pending.offers[0]
.iter()
.map(|(slot, amount)| {
inventories[0]
.as_ref()
.map(|ri| {
ri.inventory.get(slot).map(|item| {
let (material, factor) =
TradePricing::get_material(&item.name);
prices.values.get(&material).cloned().unwrap_or_default()
* factor
* (*amount as f32)
* trade_margin(material)
})
})
.flatten()
.unwrap_or_default()
})
.sum();
let balance1: f32 = pending.offers[1]
.iter()
.map(|(slot, amount)| {
inventories[1]
.as_ref()
.map(|ri| {
ri.inventory.get(slot).map(|item| {
let (material, factor) =
TradePricing::get_material(&item.name);
prices.values.get(&material).cloned().unwrap_or_default()
* factor
* (*amount as f32)
})
})
.flatten()
.unwrap_or_default()
})
.sum();
tracing::debug!("UpdatePendingTrade({}, {})", balance0, balance1);
if balance0 >= balance1 {
event_emitter.emit(ServerEvent::ProcessTradeAction(
*self.entity,
tradeid,
TradeAction::Accept(pending.phase),
));
} else {
if balance1 > 0.0 {
let msg = format!(
"That only covers {:.1}% of my costs!",
balance0 / balance1 * 100.0
);
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
}
if pending.phase != TradePhase::Mutate {
// we got into the review phase but without balanced goods, decline
event_emitter.emit(ServerEvent::ProcessTradeAction(
*self.entity,
tradeid,
TradeAction::Decline,
));
}
}
}
} else {
agent.action_timer = 0.0;
}
} else {
agent.inbox.clear();
},
None => {
if agent.can_speak {
// no new events, continue looking towards the last interacting player for some
// time
if let Some(Target { target, .. }) = &agent.target {
if let Some(tgt_pos) = read_data.positions.get(*target) {
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
let tgt_eye_offset = read_data
.bodies
.get(*target)
.map_or(0.0, |b| b.eye_height());
if let Some(dir) = Dir::from_unnormalized(
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
- Vec3::new(
self.pos.0.x,
self.pos.0.y,
self.pos.0.z + eye_offset,
),
) {
controller.inputs.look_dir = dir;
}
}
} else {
agent.action_timer = 0.0;
}
}
},
}
}

View File

@ -126,6 +126,9 @@ impl Sys {
.get_mut(entity)
.map(|mut s| s.skill_set.unlock_skill_group(skill_group_kind));
},
ClientGeneral::RequestSiteInfo(id) => {
server_emitter.emit(ServerEvent::RequestSiteInfo { entity, id });
},
_ => tracing::error!("not a client_in_game msg"),
}
Ok(())

View File

@ -2,7 +2,7 @@ use crate::{
chunk_generator::ChunkGenerator, client::Client, presence::Presence, rtsim::RtSim, Tick,
};
use common::{
comp::{self, bird_medium, Alignment, Pos},
comp::{self, bird_medium, inventory::loadout_builder::LoadoutConfig, Alignment, Pos},
event::{EventBus, ServerEvent},
generation::get_npc_name,
npc::NPC_NAMES,
@ -143,12 +143,13 @@ impl<'a> System<'a> for Sys {
}
let loadout_config = entity.loadout_config;
let economy = entity.trading_information.as_ref();
let skillset_config = entity.skillset_config;
stats.skill_set =
SkillSetBuilder::build_skillset(&main_tool, skillset_config).build();
let loadout =
LoadoutBuilder::build_loadout(body, main_tool, loadout_config).build();
LoadoutBuilder::build_loadout(body, main_tool, loadout_config, economy).build();
let health = comp::Health::new(body, entity.level.unwrap_or(0));
let poise = comp::Poise::new(body);
@ -162,6 +163,11 @@ impl<'a> System<'a> for Sys {
},
_ => false,
};
let trade_for_site = if matches!(loadout_config, Some(LoadoutConfig::Merchant)) {
economy.map(|e| e.id)
} else {
None
};
// TODO: This code sets an appropriate base_damage for the enemy. This doesn't
// work because the damage is now saved in an ability
@ -182,6 +188,7 @@ impl<'a> System<'a> for Sys {
Some(comp::Agent::new(
Some(entity.pos),
can_speak,
trade_for_site,
&body,
matches!(
loadout_config,

View File

@ -8,9 +8,9 @@ use crate::{
ui::{fonts::Fonts, img_ids, ImageFrame, Tooltip, TooltipManager, Tooltipable},
GlobalState,
};
use client::{self, Client};
use common::{comp, comp::group::Role, terrain::TerrainChunkSize, vol::RectVolSize};
use common_net::msg::world_msg::SiteKind;
use client::{self, Client, SiteInfoRich};
use common::{comp, comp::group::Role, terrain::TerrainChunkSize, trade::Good, vol::RectVolSize};
use common_net::msg::world_msg::{SiteId, SiteKind};
use conrod_core::{
color, position,
widget::{self, Button, Image, Rectangle, Text},
@ -66,6 +66,8 @@ widget_ids! {
}
}
const SHOW_ECONOMY: bool = false; // turn this display off (for 0.9) until we have an improved look
#[derive(WidgetCommon)]
pub struct Map<'a> {
client: &'a Client,
@ -122,6 +124,40 @@ pub enum Event {
ShowCaves(bool),
ShowTrees(bool),
Close,
RequestSiteInfo(SiteId),
}
fn get_site_economy(site_rich: &SiteInfoRich) -> String {
if SHOW_ECONOMY {
let site = &site_rich.site;
if let Some(economy) = &site_rich.economy {
use common::trade::Good::{Armor, Coin, Food, Ingredients, Potions, Tools};
let mut result = format!("\n\nPopulation {:?}", economy.population);
result += "\nStock";
for i in [Food, Potions, Ingredients, Coin, Tools, Armor].iter() {
result += &format!(" {:?}={:.1}", *i, *economy.stock.get(i).unwrap_or(&0.0));
}
result += "\nPrice";
for i in [Food, Potions, Ingredients, Coin, Tools, Armor].iter() {
result += &format!(" {:?}={:.1}", *i, *economy.values.get(i).unwrap_or(&0.0));
}
let mut trade_sorted: Vec<(&Good, &f32)> = economy.last_exports.iter().collect();
trade_sorted.sort_unstable_by(|a, b| a.1.partial_cmp(b.1).unwrap());
if trade_sorted.first().is_some() {
result += &format!("\nTrade {:.1} ", *(trade_sorted.first().unwrap().1));
for i in trade_sorted.iter().filter(|x| *x.1 != 0.0) {
result += &format!("{:?} ", i.0);
}
result += &format!("{:.1}", *(trade_sorted.last().unwrap().1));
}
result
} else {
format!("\nloading economy for\n{:?}", site.id)
}
} else {
"".into()
}
}
impl<'a> Widget for Map<'a> {
@ -527,7 +563,8 @@ impl<'a> Widget for Map<'a> {
.resize(self.client.sites().len(), &mut ui.widget_id_generator())
});
}
for (i, site) in self.client.sites().iter().enumerate() {
for (i, site_rich) in self.client.sites().values().enumerate() {
let site = &site_rich.site;
// Site pos in world coordinates relative to the player
let rwpos = site.wpos.map(|e| e as f32) - player_pos;
// Convert to chunk coordinates
@ -564,6 +601,7 @@ impl<'a> Widget for Map<'a> {
SiteKind::Cave => (0, i18n.get("hud.map.cave").to_string()),
SiteKind::Tree => (0, i18n.get("hud.map.tree").to_string()),
};
let desc = desc + &get_site_economy(site_rich);
let site_btn = Button::image(match &site.kind {
SiteKind::Town => self.imgs.mmap_site_town,
SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon,
@ -607,32 +645,19 @@ impl<'a> Widget for Map<'a> {
},
);
// Only display sites that are toggled on
match &site.kind {
SiteKind::Town => {
if show_towns {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
SiteKind::Dungeon { .. } => {
if show_dungeons {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
SiteKind::Castle => {
if show_castles {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
SiteKind::Cave => {
if show_caves {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
SiteKind::Tree => {
if show_trees {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
let show_site = match &site.kind {
SiteKind::Town => show_towns,
SiteKind::Dungeon { .. } => show_dungeons,
SiteKind::Castle => show_castles,
SiteKind::Cave => show_caves,
SiteKind::Tree => show_trees,
};
if show_site {
let tooltip_visible = site_btn.set_ext(state.ids.mmap_site_icons[i], ui).1;
if SHOW_ECONOMY && tooltip_visible && site_rich.economy.is_none() {
events.push(Event::RequestSiteInfo(site.id));
}
}
// Difficulty from 0-6

View File

@ -277,7 +277,8 @@ impl<'a> Widget for MiniMap<'a> {
.resize(self.client.sites().len(), &mut ui.widget_id_generator())
});
}
for (i, site) in self.client.sites().iter().enumerate() {
for (i, site_rich) in self.client.sites().values().enumerate() {
let site = &site_rich.site;
// Site pos in world coordinates relative to the player
let rwpos = site.wpos.map(|e| e as f32) - player_pos;
// Convert to chunk coordinates

View File

@ -77,7 +77,7 @@ use common::{
vol::RectRasterableVol,
};
use common_base::span;
use common_net::msg::{Notification, PresenceKind};
use common_net::msg::{world_msg::SiteId, Notification, PresenceKind};
use conrod_core::{
text::cursor::Index,
widget::{self, Button, Image, Text},
@ -433,6 +433,7 @@ pub enum Event {
UnlockSkill(Skill),
MinimapShow(bool),
MinimapFaceNorth(bool),
RequestSiteInfo(SiteId),
}
// TODO: Are these the possible layouts we want?
@ -2689,6 +2690,9 @@ impl Hud {
map::Event::ShowTrees(map_show_trees) => {
events.push(Event::MapShowTrees(map_show_trees));
},
map::Event::RequestSiteInfo(id) => {
events.push(Event::RequestSiteInfo(id));
},
}
}
} else {

View File

@ -1274,6 +1274,10 @@ impl PlayState for SessionState {
global_state.settings.interface.map_show_trees = map_show_trees;
global_state.settings.save_to_file_warn();
},
HudEvent::RequestSiteInfo(id) => {
let mut client = self.client.borrow_mut();
client.request_site_economy(id);
},
HudEvent::ChangeGamma(new_gamma) => {
global_state.settings.graphics.gamma = new_gamma;
global_state.settings.save_to_file_warn();

View File

@ -90,6 +90,7 @@ impl TooltipManager {
}
}
// return true if visible
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
fn set_tooltip(
&mut self,
@ -101,7 +102,7 @@ impl TooltipManager {
image_dims: Option<(f64, f64)>,
src_id: widget::Id,
ui: &mut UiCell,
) {
) -> bool {
let tooltip_id = self.tooltip_id;
let mp_h = MOUSE_PAD_Y / self.logical_scale_factor;
@ -154,6 +155,7 @@ impl TooltipManager {
},
_ => (),
}
matches!(self.state, HoverState::Hovering(Hover(id, _)) if id == src_id)
}
}
@ -178,9 +180,9 @@ impl<'a, W: Widget> Tooltipped<'a, W> {
self
}
pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event {
pub fn set_ext(self, id: widget::Id, ui: &mut UiCell) -> (W::Event, bool) {
let event = self.inner.set(id, ui);
self.tooltip_manager.set_tooltip(
let visible = self.tooltip_manager.set_tooltip(
self.tooltip,
self.title_text,
self.desc_text,
@ -190,8 +192,10 @@ impl<'a, W: Widget> Tooltipped<'a, W> {
id,
ui,
);
event
(event, visible)
}
pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event { self.set_ext(id, ui).0 }
}
pub trait Tooltipable {

View File

@ -34,10 +34,11 @@ packed_simd = { version = "0.3.4", package = "packed_simd_2" }
rayon = "1.5"
serde = { version = "1.0.110", features = ["derive"] }
ron = { version = "0.6", default-features = false }
assets_manager = {version = "0.4.3", features = ["ron"]}
[dev-dependencies]
criterion = "0.3"
tracing-subscriber = { version = "0.2.3", default-features = false, features = ["fmt", "chrono", "ansi", "smallvec"] }
tracing-subscriber = { version = "0.2.15", default-features = false, features = ["fmt", "chrono", "ansi", "smallvec", "env-filter"] }
minifb = "0.19.1"
svg_fmt = "0.4"
structopt = "0.3"

220
world/economy_testinput.ron Normal file
View File

@ -0,0 +1,220 @@
[
(
name: "Credge",
position: (4176, 4080),
kind: Settlement,
neighbors: [
(2, 112),
(1, 146),
(6, 98),
(9, 145),
],
resources: [
(
good: Terrain(Lake),
amount: 4,
),
(
good: Terrain(Mountain),
amount: 52,
),
(
good: Terrain(Grassland),
amount: 7073,
),
(
good: Terrain(Ocean),
amount: 4929,
),
(
good: Terrain(Forest),
amount: 6360,
),
],
),
(
name: "Etodren",
position: (1200, 2928),
kind: Settlement,
neighbors: [
(2, 258),
(0, 146),
(6, 60),
(9, 158),
],
resources: [
(
good: Terrain(Mountain),
amount: 288,
),
(
good: Terrain(Grassland),
amount: 4129,
),
(
good: Terrain(Desert),
amount: 230,
),
(
good: Terrain(Ocean),
amount: 2923,
),
(
good: Terrain(Forest),
amount: 3139,
),
],
),
(
name: "Twige",
position: (2000, 7632),
kind: Settlement,
neighbors: [
(0, 112),
(1, 258),
(6, 109),
(9, 32),
],
resources: [
(
good: Terrain(Lake),
amount: 1,
),
(
good: Terrain(Mountain),
amount: 231,
),
(
good: Terrain(Grassland),
amount: 3308,
),
(
good: Terrain(Desert),
amount: 1695,
),
(
good: Terrain(Ocean),
amount: 487,
),
(
good: Terrain(Forest),
amount: 1338,
),
],
),
(
name: "Pleed Dungeon",
position: (6922, 1034),
kind: Dungeon,
neighbors: [],
resources: [],
),
(
name: "Fred Lair",
position: (3786, 2250),
kind: Dungeon,
neighbors: [],
resources: [],
),
(
name: "Frer Dungeon",
position: (6602, 2250),
kind: Dungeon,
neighbors: [],
resources: [],
),
(
name: "Inige Castle",
position: (1360, 4304),
kind: Castle,
neighbors: [
(0, 98),
(1, 60),
(2, 109),
(9, 99),
],
resources: [
(
good: Terrain(Mountain),
amount: 424,
),
(
good: Terrain(Grassland),
amount: 958,
),
(
good: Terrain(Desert),
amount: 1285,
),
(
good: Terrain(Ocean),
amount: 669,
),
(
good: Terrain(Forest),
amount: 1781,
),
],
),
(
name: "Estedock Catacombs",
position: (4650, 330),
kind: Dungeon,
neighbors: [],
resources: [],
),
(
name: "Oreefey Lair",
position: (1578, 3754),
kind: Dungeon,
neighbors: [],
resources: [],
),
(
name: "Lasnast Keep",
position: (1136, 6832),
kind: Castle,
neighbors: [
(0, 145),
(2, 32),
(1, 158),
(6, 99),
],
resources: [
(
good: Terrain(Mountain),
amount: 352,
),
(
good: Terrain(Grassland),
amount: 887,
),
(
good: Terrain(Desert),
amount: 2606,
),
(
good: Terrain(Ocean),
amount: 286,
),
(
good: Terrain(Forest),
amount: 679,
),
],
),
(
name: "Oren Lair",
position: (6730, 2506),
kind: Dungeon,
neighbors: [],
resources: [],
),
(
name: "Ween Crib",
position: (2250, 8010),
kind: Dungeon,
neighbors: [],
resources: [],
),
]

View File

@ -8,7 +8,10 @@ use common::{
use rayon::prelude::*;
use std::{f64, io::Write, path::PathBuf, time::SystemTime};
use tracing::{warn, Level};
use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
FmtSubscriber,
};
use vek::*;
use veloren_world::{
sim::{self, get_horizon_map, sample_pos, sample_wpos, WorldOpts},

View File

@ -2,13 +2,12 @@
mod econ;
use self::{Occupation::*, Stock::*};
use crate::{
config::CONFIG,
sim::WorldSim,
site::{namegen::NameGen, Castle, Dungeon, Settlement, Site as WorldSite, Tree},
site2,
util::{attempt, seed_expan, MapVec, CARDINALS, NEIGHBORS},
util::{attempt, seed_expan, NEIGHBORS},
Index, Land,
};
use common::{
@ -16,16 +15,12 @@ use common::{
path::Path,
spiral::Spiral2d,
store::{Id, Store},
terrain::{MapSizeLg, TerrainChunkSize},
terrain::{uniform_idx_as_vec2, MapSizeLg, TerrainChunkSize},
vol::RectVolSize,
};
use core::{
fmt,
hash::{BuildHasherDefault, Hash},
ops::Range,
};
use fxhash::{FxHasher32, FxHasher64};
use hashbrown::{HashMap, HashSet};
use core::{fmt, hash::BuildHasherDefault, ops::Range};
use fxhash::FxHasher64;
use hashbrown::HashMap;
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::{debug, info, warn};
@ -113,35 +108,17 @@ impl Civs {
_ => (SiteKind::Dungeon, 0),
};
let loc = find_site_loc(&mut ctx, None, size)?;
this.establish_site(&mut ctx.reseed(), loc, |place| Site {
Some(this.establish_site(&mut ctx.reseed(), loc, |place| Site {
kind,
center: loc,
place,
site_tmp: None,
population: 0.0,
stocks: Stocks::from_default(100.0),
surplus: Stocks::from_default(0.0),
values: Stocks::from_default(None),
labors: MapVec::from_default(0.01),
yields: MapVec::from_default(1.0),
productivity: MapVec::from_default(1.0),
last_exports: Stocks::from_default(0.0),
export_targets: Stocks::from_default(0.0),
//trade_states: Stocks::default(),
coin: 1000.0,
})
}))
});
}
// Tick
const SIM_YEARS: usize = 1000;
for _ in 0..SIM_YEARS {
this.tick(&mut ctx, 1.0);
}
//=== old economy is gone
// Flatten ground around sites
for site in this.sites.values() {
@ -243,6 +220,52 @@ impl Civs {
//this.display_info();
// remember neighbor information in economy
for (s1, val) in this.track_map.iter() {
if let Some(index1) = this.sites.get(*s1).site_tmp {
for (s2, t) in val.iter() {
if let Some(index2) = this.sites.get(*s2).site_tmp {
if index.sites.get(index1).do_economic_simulation()
&& index.sites.get(index2).do_economic_simulation()
{
let cost = this.tracks.get(*t).path.len();
index
.sites
.get_mut(index1)
.economy
.add_neighbor(index2, cost);
index
.sites
.get_mut(index2)
.economy
.add_neighbor(index1, cost);
}
}
}
}
}
// collect natural resources
let sites = &mut index.sites;
(0..ctx.sim.map_size_lg().chunks_len())
.into_iter()
.for_each(|posi| {
let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi);
let wpos = chpos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let closest_site = (*sites)
.iter_mut()
.filter(|s| !matches!(s.1.kind, crate::site::SiteKind::Dungeon(_)))
.min_by_key(|(_id, s)| s.get_origin().distance_squared(wpos));
if let Some((_id, s)) = closest_site {
let distance_squared = s.get_origin().distance_squared(wpos);
s.economy
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
}
});
sites
.iter_mut()
.for_each(|(_, s)| s.economy.cache_economy());
this
}
@ -405,27 +428,16 @@ impl Civs {
fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
let site = attempt(5, || {
let loc = find_site_loc(ctx, None, 1)?;
self.establish_site(ctx, loc, |place| Site {
Some(self.establish_site(ctx, loc, |place| Site {
kind: SiteKind::Settlement,
site_tmp: None,
center: loc,
place,
population: 24.0,
stocks: Stocks::from_default(100.0),
surplus: Stocks::from_default(0.0),
values: Stocks::from_default(None),
labors: MapVec::from_default(0.01),
yields: MapVec::from_default(1.0),
productivity: MapVec::from_default(1.0),
last_exports: Stocks::from_default(0.0),
export_targets: Stocks::from_default(0.0),
//trade_states: Stocks::default(),
coin: 1000.0,
})
/* most economic members have moved to site/Economy */
/* last_exports: Stocks::from_default(0.0),
* export_targets: Stocks::from_default(0.0),
* //trade_states: Stocks::default(), */
}))
})?;
let civ = self.civs.insert(Civ {
@ -438,60 +450,11 @@ impl Civs {
fn establish_place(
&mut self,
ctx: &mut GenCtx<impl Rng>,
_ctx: &mut GenCtx<impl Rng>,
loc: Vec2<i32>,
area: Range<usize>,
) -> Option<Id<Place>> {
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut dead = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
let mut alive = HashSet::with_hasher(BuildHasherDefault::<FxHasher64>::default());
alive.insert(loc);
// Fill the surrounding area
while let Some(cloc) = alive.iter().choose(&mut ctx.rng).copied() {
for dir in CARDINALS.iter() {
if site_in_dir(&ctx.sim, cloc, *dir) {
let rloc = cloc + *dir;
if !dead.contains(&rloc)
&& ctx
.sim
.get(rloc)
.map(|c| c.place.is_none())
.unwrap_or(false)
{
alive.insert(rloc);
}
}
}
alive.remove(&cloc);
dead.insert(cloc);
if dead.len() + alive.len() >= area.end {
break;
}
}
// Make sure the place is large enough
if dead.len() + alive.len() <= area.start {
return None;
}
let place = self.places.insert(Place {
center: loc,
nat_res: NaturalResources::default(),
});
// Write place to map
for cell in dead.union(&alive) {
if let Some(chunk) = ctx.sim.get_mut(*cell) {
chunk.place = Some(place);
self.places.get_mut(place).nat_res.include_chunk(ctx, *cell);
}
}
Some(place)
_area: Range<usize>,
) -> Id<Place> {
self.places.insert(Place { center: loc })
}
fn establish_site(
@ -499,12 +462,12 @@ impl Civs {
ctx: &mut GenCtx<impl Rng>,
loc: Vec2<i32>,
site_fn: impl FnOnce(Id<Place>) -> Site,
) -> Option<Id<Site>> {
) -> Id<Site> {
const SITE_AREA: Range<usize> = 1..4; //64..256;
let place = match ctx.sim.get(loc).and_then(|site| site.place) {
Some(place) => place,
None => self.establish_place(ctx, loc, SITE_AREA)?,
None => self.establish_place(ctx, loc, SITE_AREA),
};
let site = self.sites.insert(site_fn(place));
@ -568,76 +531,7 @@ impl Civs {
}
}
Some(site)
}
fn tick(&mut self, _ctx: &mut GenCtx<impl Rng>, years: f32) {
for site in self.sites.values_mut() {
site.simulate(years, &self.places.get(site.place).nat_res);
}
// Trade stocks
// let mut stocks = TRADE_STOCKS;
// stocks.shuffle(ctx.rng); // Give each stock a chance to be traded
// first for stock in stocks.iter().copied() {
// let mut sell_orders = self.sites
// .iter_ids()
// .map(|(id, site)| (id, {
// econ::SellOrder {
// quantity:
// site.export_targets[stock].max(0.0).min(site.stocks[stock]),
// price:
// site.trade_states[stock].sell_belief.choose_price(ctx) * 1.25, //
// Trade cost q_sold: 0.0,
// }
// }))
// .filter(|(_, order)| order.quantity > 0.0)
// .collect::<Vec<_>>();
// let mut sites = self.sites
// .ids()
// .collect::<Vec<_>>();
// sites.shuffle(ctx.rng); // Give all sites a chance to buy first
// for site in sites {
// let (max_spend, max_price, max_import) = {
// let site = self.sites.get(site);
// let budget = site.coin * 0.5;
// let total_value = site.values.iter().map(|(_, v)|
// (*v).unwrap_or(0.0)).sum::<f32>(); (
// 100000.0,//(site.values[stock].unwrap_or(0.1) /
// total_value * budget).min(budget),
// site.trade_states[stock].buy_belief.price,
// -site.export_targets[stock].min(0.0), )
// };
// let (quantity, spent) = econ::buy_units(ctx, sell_orders
// .iter_mut()
// .filter(|(id, _)| site != *id && self.track_between(site,
// *id).is_some()) .map(|(_, order)| order),
// max_import,
// 1000000.0, // Max price TODO
// max_spend,
// );
// let mut site = self.sites.get_mut(site);
// site.coin -= spent;
// if quantity > 0.0 {
// site.stocks[stock] += quantity;
// site.last_exports[stock] = -quantity;
// site.trade_states[stock].buy_belief.update_buyer(years,
// spent / quantity); println!("Belief: {:?}",
// site.trade_states[stock].buy_belief); }
// }
// for (site, order) in sell_orders {
// let mut site = self.sites.get_mut(site);
// site.coin += order.q_sold * order.price;
// if order.q_sold > 0.0 {
// site.stocks[stock] -= order.q_sold;
// site.last_exports[stock] = order.q_sold;
//
// site.trade_states[stock].sell_belief.update_seller(order.q_sold /
// order.quantity); }
// }
// }
site
}
}
@ -782,45 +676,10 @@ pub struct Civ {
#[derive(Debug)]
pub struct Place {
center: Vec2<i32>,
nat_res: NaturalResources,
}
// Productive capacity per year
#[derive(Default, Debug)]
pub struct NaturalResources {
wood: f32,
rock: f32,
river: f32,
farmland: f32,
}
impl NaturalResources {
fn include_chunk(&mut self, ctx: &mut GenCtx<impl Rng>, loc: Vec2<i32>) {
let chunk = if let Some(chunk) = ctx.sim.get(loc) {
chunk
} else {
return;
};
self.wood += chunk.tree_density;
self.rock += chunk.rockiness;
self.river += if chunk.river.is_river() { 5.0 } else { 0.0 };
self.farmland += if chunk.humidity > 0.35
&& chunk.temp > -0.3
&& chunk.temp < 0.75
&& chunk.chaos < 0.5
&& ctx
.sim
.get_gradient_approx(loc)
.map(|grad| grad < 0.7)
.unwrap_or(false)
{
1.0
} else {
0.0
};
}
pub center: Vec2<i32>,
/* act sort of like territory with sites belonging to it
* nat_res/NaturalResources was moved to Economy
* nat_res: NaturalResources, */
}
pub struct Track {
@ -838,61 +697,11 @@ pub struct Site {
pub site_tmp: Option<Id<crate::site::Site>>,
pub center: Vec2<i32>,
pub place: Id<Place>,
population: f32,
// Total amount of each stock
stocks: Stocks<f32>,
// Surplus stock compared to demand orders
surplus: Stocks<f32>,
// For some goods, such a goods without any supply, it doesn't make sense to talk about value
values: Stocks<Option<f32>>,
// Proportion of individuals dedicated to an industry
labors: MapVec<Occupation, f32>,
// Per worker, per year, of their output good
yields: MapVec<Occupation, f32>,
productivity: MapVec<Occupation, f32>,
last_exports: Stocks<f32>,
export_targets: Stocks<f32>,
//trade_states: Stocks<TradeState>,
coin: f32,
}
impl fmt::Display for Site {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{:?}", self.kind)?;
writeln!(f, "- population: {}", self.population.floor() as u32)?;
writeln!(f, "- coin: {}", self.coin.floor() as u32)?;
writeln!(f, "Stocks")?;
for (stock, q) in self.stocks.iter() {
writeln!(f, "- {:?}: {}", stock, q.floor())?;
}
writeln!(f, "Values")?;
for stock in TRADE_STOCKS.iter() {
writeln!(
f,
"- {:?}: {}",
stock,
self.values[*stock]
.map(|x| x.to_string())
.unwrap_or_else(|| "N/A".to_string())
)?;
}
writeln!(f, "Laborers")?;
for (labor, n) in self.labors.iter() {
writeln!(
f,
"- {:?}: {}",
labor,
(*n * self.population).floor() as u32
)?;
}
writeln!(f, "Export targets")?;
for (stock, n) in self.export_targets.iter() {
writeln!(f, "- {:?}: {}", stock, n)?;
}
Ok(())
}
@ -908,236 +717,9 @@ pub enum SiteKind {
}
impl Site {
#[allow(clippy::let_and_return)] // TODO: Pending review in #587
pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) {
// Insert natural resources into the economy
if self.stocks[Fish] < nat_res.river {
self.stocks[Fish] = nat_res.river;
}
if self.stocks[Wheat] < nat_res.farmland {
self.stocks[Wheat] = nat_res.farmland;
}
if self.stocks[Logs] < nat_res.wood {
self.stocks[Logs] = nat_res.wood;
}
if self.stocks[Game] < nat_res.wood {
self.stocks[Game] = nat_res.wood;
}
if self.stocks[Rock] < nat_res.rock {
self.stocks[Rock] = nat_res.rock;
}
// We use this hasher (FxHasher32) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 1-byte keys (for which FxHash is supposedly fastest).
let orders = vec![
(None, vec![(Food, 0.5)]),
(Some(Cook), vec![(Flour, 16.0), (Meat, 4.0), (Wood, 3.0)]),
(Some(Lumberjack), vec![(Logs, 4.5)]),
(Some(Miner), vec![(Rock, 7.5)]),
(Some(Fisher), vec![(Fish, 4.0)]),
(Some(Hunter), vec![(Game, 4.0)]),
(Some(Farmer), vec![(Wheat, 4.0)]),
]
.into_iter()
.collect::<HashMap<_, Vec<(Stock, f32)>, BuildHasherDefault<FxHasher32>>>();
// Per labourer, per year
let production = MapVec::from_list(
&[
(Farmer, (Flour, 2.0)),
(Lumberjack, (Wood, 1.5)),
(Miner, (Stone, 0.6)),
(Fisher, (Meat, 3.0)),
(Hunter, (Meat, 0.25)),
(Cook, (Food, 20.0)),
],
(Rock, 0.0),
);
let mut demand = Stocks::from_default(0.0);
for (labor, orders) in &orders {
let scale = if let Some(labor) = labor {
self.labors[*labor]
} else {
1.0
} * self.population;
for (stock, amount) in orders {
demand[*stock] += *amount * scale;
}
}
let mut supply = Stocks::from_default(0.0);
for (labor, (output_stock, _)) in production.iter() {
supply[*output_stock] += self.yields[labor] * self.labors[labor] * self.population;
}
let last_exports = &self.last_exports;
let stocks = &self.stocks;
self.surplus = demand
.clone()
.map(|stock, _| supply[stock] + stocks[stock] - demand[stock] - last_exports[stock]);
// Update values according to the surplus of each stock
let values = &mut self.values;
self.surplus.iter().for_each(|(stock, surplus)| {
let val = 3.5f32.powf(1.0 - *surplus / demand[stock]);
values[stock] = if val > 0.001 && val < 1000.0 {
Some(val)
} else {
None
};
});
// Update export targets based on relative values
let value_avg = values
.iter()
.map(|(_, v)| (*v).unwrap_or(0.0))
.sum::<f32>()
.max(0.01)
/ values.iter().filter(|(_, v)| v.is_some()).count() as f32;
let export_targets = &mut self.export_targets;
let last_exports = &self.last_exports;
self.values.iter().for_each(|(stock, value)| {
let rvalue = (*value).map(|v| v - value_avg).unwrap_or(0.0);
//let factor = if export_targets[stock] > 0.0 { 1.0 / rvalue } else { rvalue };
export_targets[stock] = last_exports[stock] - rvalue * 0.1; // + (trade_states[stock].sell_belief.price - trade_states[stock].buy_belief.price) * 0.025;
});
let population = self.population;
// Redistribute workforce according to relative good values
let labor_ratios = production.clone().map(|labor, (output_stock, _)| {
self.productivity[labor] * demand[output_stock] / supply[output_stock].max(0.001)
});
let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::<f32>().max(0.01);
production.iter().for_each(|(labor, _)| {
let smooth = 0.8;
self.labors[labor] = smooth * self.labors[labor]
+ (1.0 - smooth)
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum);
});
// Production
let stocks_before = self.stocks.clone();
for (labor, orders) in orders.iter() {
let scale = if let Some(labor) = labor {
self.labors[*labor]
} else {
1.0
} * population;
// For each order, we try to find the minimum satisfaction rate - this limits
// how much we can produce! For example, if we need 0.25 fish and
// 0.75 oats to make 1 unit of food, but only 0.5 units of oats are
// available then we only need to consume 2/3rds
// of other ingredients and leave the rest in stock
// In effect, this is the productivity
let productivity = orders
.iter()
.map(|(stock, amount)| {
// What quantity is this order requesting?
let _quantity = *amount * scale;
// What proportion of this order is the economy able to satisfy?
let satisfaction = (stocks_before[*stock] / demand[*stock]).min(1.0);
satisfaction
})
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or_else(|| {
panic!("Industry {:?} requires at least one input order", labor)
});
for (stock, amount) in orders {
// What quantity is this order requesting?
let quantity = *amount * scale;
// What amount gets actually used in production?
let used = quantity * productivity;
// Deplete stocks accordingly
self.stocks[*stock] = (self.stocks[*stock] - used).max(0.0);
}
// Industries produce things
if let Some(labor) = labor {
let (stock, rate) = production[*labor];
let workers = self.labors[*labor] * population;
let final_rate = rate;
let yield_per_worker = productivity * final_rate;
self.yields[*labor] = yield_per_worker;
self.productivity[*labor] = productivity;
self.stocks[stock] += yield_per_worker * workers.powf(1.1);
}
}
// Denature stocks
self.stocks.iter_mut().for_each(|(_, v)| *v *= 0.9);
// Births/deaths
const NATURAL_BIRTH_RATE: f32 = 0.15;
const DEATH_RATE: f32 = 0.05;
let birth_rate = if self.surplus[Food] > 0.0 {
NATURAL_BIRTH_RATE
} else {
0.0
};
self.population += years * self.population * (birth_rate - DEATH_RATE);
}
pub fn is_dungeon(&self) -> bool { matches!(self.kind, SiteKind::Dungeon) }
pub fn is_settlement(&self) -> bool { matches!(self.kind, SiteKind::Settlement) }
pub fn is_castle(&self) -> bool { matches!(self.kind, SiteKind::Castle) }
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Occupation {
Farmer = 0,
Lumberjack = 1,
Miner = 2,
Fisher = 3,
Hunter = 4,
Cook = 5,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Stock {
Wheat = 0,
Flour = 1,
Meat = 2,
Fish = 3,
Game = 4,
Food = 5,
Logs = 6,
Wood = 7,
Rock = 8,
Stone = 9,
}
const TRADE_STOCKS: [Stock; 5] = [Flour, Meat, Food, Wood, Stone];
#[derive(Debug, Clone)]
struct TradeState {
buy_belief: econ::Belief,
sell_belief: econ::Belief,
}
impl Default for TradeState {
fn default() -> Self {
Self {
buy_belief: econ::Belief {
price: 1.0,
confidence: 0.25,
},
sell_belief: econ::Belief {
price: 1.0,
confidence: 0.25,
},
}
}
}
pub type Stocks<T> = MapVec<Stock, T>;

View File

@ -1,4 +1,7 @@
use crate::{site::Site, Colors};
use crate::{
site::{economy::TradeInformation, Site},
Colors,
};
use common::{
assets::{AssetExt, AssetHandle},
store::Store,
@ -14,6 +17,7 @@ pub struct Index {
pub time: f32,
pub noise: Noise,
pub sites: Store<Site>,
pub trade: TradeInformation,
colors: AssetHandle<Arc<Colors>>,
}
@ -59,6 +63,7 @@ impl Index {
time: 0.0,
noise: Noise::new(seed),
sites: Store::default(),
trade: Default::default(),
colors,
}
}

View File

@ -107,6 +107,9 @@ impl World {
}
pub fn get_map_data(&self, index: IndexRef) -> WorldMapMsg {
// we need these numbers to create unique ids for cave ends
let num_sites = self.civs().sites().count() as u64;
let num_caves = self.civs().caves.values().count() as u64;
WorldMapMsg {
sites: self
.civs()
@ -114,6 +117,7 @@ impl World {
.iter()
.map(|(_, site)| {
world_msg::SiteInfo {
id: site.site_tmp.map(|i| i.id()).unwrap_or_default(),
name: site.site_tmp.map(|id| index.sites[id].name().to_string()),
// TODO: Probably unify these, at some point
kind: match &site.kind {
@ -135,13 +139,15 @@ impl World {
self.civs()
.caves
.iter()
.map(|(_, info)| {
.map(|(id, info)| {
// separate the two locations, combine with name
std::iter::once((info.name.clone(), info.location.0))
.chain(std::iter::once((info.name.clone(), info.location.1)))
std::iter::once((id.id()+num_sites, info.name.clone(), info.location.0))
// unfortunately we have to introduce a fake id (as it gets stored in a map in the client)
.chain(std::iter::once((id.id()+num_sites+num_caves, info.name.clone(), info.location.1)))
})
.flatten() // unwrap inner iteration
.map(|(name, pos)| world_msg::SiteInfo {
.map(|(id, name, pos)| world_msg::SiteInfo {
id,
name: Some(name),
kind: world_msg::SiteKind::Cave,
wpos: pos,
@ -359,6 +365,7 @@ impl World {
chunk_wpos2d,
sample_get,
&mut supplement,
site.id(),
)
});

File diff suppressed because it is too large Load Diff

68
world/src/site/economy.gv Normal file
View File

@ -0,0 +1,68 @@
digraph economy {
Farmland [color="green"];
Flour [color="orange"];
Meat [color="orange"];
Fish [color="green"];
Game [color="green"];
Food [color="orange"];
Logs [color="green"];
Wood [color="orange"];
Rock [color="green"];
Stone [color="orange"];
Tools [color="orange"];
Armor [color="orange"];
Ingredients [color="green"];
Potions [color="orange"];
ControlledArea [color="green", shape=doubleoctagon];
// Professions
Everyone [shape=doubleoctagon];
Farmer [shape=box];
Lumberjack [shape=box];
Miner [shape=box];
Fisher [shape=box];
Hunter [shape=box];
Cook [shape=box];
Brewer [shape=box];
Blacksmith [shape=box];
Bladesmith [shape=box];
Guard [shape=box];
// Orders
Food -> Everyone [label= "0.5", style=dashed, weight=4];
Flour -> Cook [label="12.0", penwidth=2.0];
Meat -> Cook [label="4.0", penwidth=1.5];
Wood -> Cook [label="1.5"];
Stone -> Cook [label="1.0"];
Logs -> Lumberjack [label="0.5", style=dashed, weight=4];
Tools -> Lumberjack [label="0.1", color="orange", style=dashed];
Rock -> Miner [label="0.5", style=dashed, weight=4];
Tools -> Miner [label="0.1", color="orange", style=dashed];
Fish -> Fisher [label="4.0", penwidth=1.5, weight=4];
Tools -> Fisher [label="0.02", color="orange", style=dotted];
Game -> Hunter [label="1.0", weight=4];
Tools -> Hunter [label="0.1", color="orange", style=dashed];
Farmland -> Farmer [label="2.0", weight=4];
Tools -> Farmer [label="0.05", color="orange", style=dotted];
Ingredients -> Brewer [label="2.0", penwidth=1.0];
Flour -> Brewer [label="2.0", penwidth=1.0];
Ingredients -> Blacksmith [label="8.0", penwidth=2.0];
Wood -> Blacksmith [label="2.0", penwidth=1.0];
Ingredients -> Bladesmith [label="4.0", penwidth=1.5];
Wood -> Bladesmith [label="1.0", penwidth=1.0];
Armor -> Guard [label="0.5", style=dashed];
Tools -> Guard [label="0.3", color="orange", style=dashed];
Potions -> Guard [label="3.0", penwidth=1.5];
// Products
Farmer -> Flour [label="2.0"];
Lumberjack -> Wood [label="0.5", style=dashed];
Miner -> Stone [label="0.5", style=dashed];
Fisher -> Meat [label="4.0", penwidth=1.5];
Hunter -> Meat [label="1.0"];
Cook -> Food [label="16.0", penwidth=2.0];
Blacksmith -> Armor [label="4.0"];
Bladesmith -> Tools [label="2.0", color="orange"];
Brewer -> Potions [label="6.0"];
Guard -> ControlledArea [label="50.0", penwidth=2.0];
}

View File

@ -1,155 +1,401 @@
use crate::util::{DHashMap, MapVec};
use crate::{
assets::{self, AssetExt, AssetHandle},
sim::SimChunk,
site::Site,
util::{DHashMap, MapVec},
};
use common::{
store::Id,
terrain::BiomeKind,
trade::{Good, SitePrices},
};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::{fmt, marker::PhantomData, sync::Once};
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Good {
Wheat = 0,
Flour = 1,
Meat = 2,
Fish = 3,
Game = 4,
Food = 5,
Logs = 6,
Wood = 7,
Rock = 8,
Stone = 9,
}
use Good::*;
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Labor {
Farmer = 0,
Lumberjack = 1,
Miner = 2,
Fisher = 3,
Hunter = 4,
Cook = 5,
#[derive(Debug, Serialize, Deserialize)]
pub struct Profession {
pub name: String,
pub orders: Vec<(Good, f32)>,
pub products: Vec<(Good, f32)>,
}
use Labor::*;
// reference to profession
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Labor(u8, PhantomData<Profession>);
#[derive(Debug)]
pub struct AreaResources {
pub resource_sum: MapVec<Good, f32>,
pub resource_chunks: MapVec<Good, u32>,
pub chunks: u32,
}
impl Default for AreaResources {
fn default() -> Self {
Self {
resource_sum: MapVec::default(),
resource_chunks: MapVec::default(),
chunks: 0,
}
}
}
#[derive(Debug)]
pub struct NaturalResources {
// resources per distance, we should increase labor cost for far resources
pub per_area: Vec<AreaResources>,
// computation simplifying cached values
pub chunks_per_resource: MapVec<Good, u32>,
pub average_yield_per_chunk: MapVec<Good, f32>,
}
impl Default for NaturalResources {
fn default() -> Self {
Self {
per_area: Vec::new(),
chunks_per_resource: MapVec::default(),
average_yield_per_chunk: MapVec::default(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct RawProfessions(Vec<Profession>);
impl assets::Asset for RawProfessions {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
pub fn default_professions() -> AssetHandle<RawProfessions> {
RawProfessions::load_expect("common.professions")
}
lazy_static! {
static ref LABOR: AssetHandle<RawProfessions> = default_professions();
// used to define resources needed by every person
static ref DUMMY_LABOR: Labor = Labor(
LABOR
.read()
.0
.iter()
.position(|a| a.name == "_")
.unwrap_or(0) as u8,
PhantomData
);
}
impl fmt::Debug for Labor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if (self.0 as usize) < LABOR.read().0.len() {
f.write_str(&LABOR.read().0[self.0 as usize].name)
} else {
f.write_str("?")
}
}
}
#[derive(Debug)]
pub struct TradeOrder {
pub customer: Id<Site>,
pub amount: MapVec<Good, f32>, // positive for orders, negative for exchange
}
#[derive(Debug)]
pub struct TradeDelivery {
pub supplier: Id<Site>,
pub amount: MapVec<Good, f32>, // positive for orders, negative for exchange
pub prices: MapVec<Good, f32>, // at the time of interaction
pub supply: MapVec<Good, f32>, // maximum amount available, at the time of interaction
}
#[derive(Debug)]
pub struct TradeInformation {
pub orders: DHashMap<Id<Site>, Vec<TradeOrder>>, // per provider
pub deliveries: DHashMap<Id<Site>, Vec<TradeDelivery>>, // per receiver
}
impl Default for TradeInformation {
fn default() -> Self {
Self {
orders: Default::default(),
deliveries: Default::default(),
}
}
}
#[derive(Debug)]
pub struct NeighborInformation {
pub id: Id<Site>,
pub travel_distance: usize,
// remembered from last interaction
pub last_values: MapVec<Good, f32>,
pub last_supplies: MapVec<Good, f32>,
}
#[derive(Debug)]
pub struct Economy {
// Population
pub pop: f32,
/// Total available amount of each good
pub stocks: MapVec<Good, f32>,
/// Surplus stock compared to demand orders
pub surplus: MapVec<Good, f32>,
/// change rate (derivative) of stock in the current situation
pub marginal_surplus: MapVec<Good, f32>,
/// amount of wares not needed by the economy (helps with trade planning)
pub unconsumed_stock: MapVec<Good, f32>,
// For some goods, such a goods without any supply, it doesn't make sense to talk about value
pub values: MapVec<Good, Option<f32>>,
pub last_exports: MapVec<Good, f32>,
pub active_exports: MapVec<Good, f32>, // unfinished trade (amount unconfirmed)
//pub export_targets: MapVec<Good, f32>,
pub labor_values: MapVec<Good, Option<f32>>,
pub material_costs: MapVec<Good, f32>,
// Proportion of individuals dedicated to an industry
pub labors: MapVec<Labor, f32>,
// Per worker, per year, of their output good
pub yields: MapVec<Labor, f32>,
pub productivity: MapVec<Labor, f32>,
pub natural_resources: NaturalResources,
// usize is distance
pub neighbors: Vec<NeighborInformation>,
}
static INIT: Once = Once::new();
impl Default for Economy {
fn default() -> Self {
INIT.call_once(|| {
LABOR.read();
});
Self {
pop: 32.0,
stocks: Default::default(),
stocks: MapVec::from_list(&[(Coin, Economy::STARTING_COIN)], 100.0),
surplus: Default::default(),
marginal_surplus: Default::default(),
values: Default::default(),
values: MapVec::from_list(&[(Coin, Some(2.0))], None),
last_exports: Default::default(),
active_exports: Default::default(),
labor_values: Default::default(),
material_costs: Default::default(),
labors: Default::default(),
yields: Default::default(),
productivity: Default::default(),
labors: MapVec::from_default(0.01),
yields: MapVec::from_default(1.0),
productivity: MapVec::from_default(1.0),
natural_resources: Default::default(),
neighbors: Default::default(),
unconsumed_stock: Default::default(),
}
}
}
impl Economy {
pub const MINIMUM_PRICE: f32 = 0.1;
pub const STARTING_COIN: f32 = 1000.0;
const _NATURAL_RESOURCE_SCALE: f32 = 1.0 / 9.0;
pub fn cache_economy(&mut self) {
for &g in good_list() {
let amount: f32 = self
.natural_resources
.per_area
.iter()
.map(|a| a.resource_sum[g])
.sum();
let chunks = self
.natural_resources
.per_area
.iter()
.map(|a| a.resource_chunks[g])
.sum();
if chunks != 0 {
self.natural_resources.chunks_per_resource[g] = chunks;
self.natural_resources.average_yield_per_chunk[g] = amount / (chunks as f32);
}
}
}
pub fn get_orders(&self) -> DHashMap<Option<Labor>, Vec<(Good, f32)>> {
vec![
(None, vec![(Food, 0.5)]),
(Some(Cook), vec![
(Flour, 12.0),
(Meat, 4.0),
(Wood, 1.5),
(Stone, 1.0),
]),
(Some(Lumberjack), vec![(Logs, 0.5)]),
(Some(Miner), vec![(Rock, 0.5)]),
(Some(Fisher), vec![(Fish, 4.0)]),
(Some(Hunter), vec![(Game, 1.0)]),
(Some(Farmer), vec![(Wheat, 2.0)]),
]
.into_iter()
.collect()
LABOR
.read()
.0
.iter()
.enumerate()
.map(|(i, p)| {
(
if p.name == "_" {
None
} else {
Some(Labor(i as u8, PhantomData))
},
p.orders.clone(),
)
})
.collect()
}
pub fn get_productivity(&self) -> MapVec<Labor, (Good, f32)> {
// Per labourer, per year
MapVec::from_list(
&[
(Farmer, (Flour, 2.0)),
(Lumberjack, (Wood, 0.5)),
(Miner, (Stone, 0.5)),
(Fisher, (Meat, 4.0)),
(Hunter, (Meat, 1.0)),
(Cook, (Food, 16.0)),
],
(Rock, 0.0),
)
.map(|l, (good, v)| (good, v * (1.0 + self.labors[l])))
pub fn get_productivity(&self) -> MapVec<Labor, Vec<(Good, f32)>> {
let products: MapVec<Labor, Vec<(Good, f32)>> = MapVec::from_iter(
LABOR
.read()
.0
.iter()
.enumerate()
.filter(|(_, p)| !p.products.is_empty())
.map(|(i, p)| (Labor(i as u8, PhantomData), p.products.clone())),
vec![(Good::Terrain(BiomeKind::Void), 0.0)],
);
products.map(|l, vec| {
let labor_ratio = self.labors[l];
let total_workers = labor_ratio * self.pop;
// apply economy of scale (workers get more productive in numbers)
let relative_scale = 1.0 + labor_ratio;
let absolute_scale = (1.0 + total_workers / 100.0).min(3.0);
let scale = relative_scale * absolute_scale;
vec.iter()
.map(|(good, amount)| (*good, amount * scale))
.collect()
})
}
pub fn replenish(&mut self, time: f32) {
//use rand::Rng;
for (i, (g, v)) in [
(Wheat, 50.0),
(Logs, 20.0),
(Rock, 120.0),
(Game, 12.0),
(Fish, 10.0),
]
.iter()
.enumerate()
{
self.stocks[*g] = (*v
* (1.25 + (((time * 0.0001 + i as f32).sin() + 1.0) % 1.0) * 0.5)
- self.stocks[*g])
* 0.075; //rand::thread_rng().gen_range(0.05, 0.1);
pub fn replenish(&mut self, _time: f32) {
for (good, &ch) in self.natural_resources.chunks_per_resource.iter() {
let per_year = self.natural_resources.average_yield_per_chunk[good] * (ch as f32);
self.stocks[good] = self.stocks[good].max(per_year);
}
// info!("resources {:?}", self.stocks);
}
pub fn add_chunk(&mut self, ch: &SimChunk, distance_squared: i32) {
let biome = ch.get_biome();
// we don't scale by pi, although that would be correct
let distance_bin = (distance_squared >> 16).min(64) as usize;
if self.natural_resources.per_area.len() <= distance_bin {
self.natural_resources
.per_area
.resize_with(distance_bin + 1, Default::default);
}
self.natural_resources.per_area[distance_bin].chunks += 1;
self.natural_resources.per_area[distance_bin].resource_sum[Terrain(biome)] += 1.0;
self.natural_resources.per_area[distance_bin].resource_chunks[Terrain(biome)] += 1;
// TODO: Scale resources by rockiness or tree_density?
}
pub fn add_neighbor(&mut self, id: Id<Site>, distance: usize) {
self.neighbors.push(NeighborInformation {
id,
travel_distance: distance,
last_values: MapVec::from_default(Economy::MINIMUM_PRICE),
last_supplies: Default::default(),
});
}
pub fn get_site_prices(&self) -> SitePrices {
SitePrices {
values: self
.values
.iter()
.map(|(g, v)| (g, v.unwrap_or(Economy::MINIMUM_PRICE)))
.collect(),
}
}
}
impl Default for Good {
fn default() -> Self {
Good::Rock // Arbitrary
pub fn good_list() -> &'static [Good] {
static GOODS: [Good; 23] = [
// controlled resources
Territory(BiomeKind::Grassland),
Territory(BiomeKind::Forest),
Territory(BiomeKind::Lake),
Territory(BiomeKind::Ocean),
Territory(BiomeKind::Mountain),
RoadSecurity,
Ingredients,
// produced goods
Flour,
Meat,
Wood,
Stone,
Food,
Tools,
Armor,
Potions,
Transportation,
// exchange currency
Coin,
// uncontrolled resources
Terrain(BiomeKind::Lake),
Terrain(BiomeKind::Mountain),
Terrain(BiomeKind::Grassland),
Terrain(BiomeKind::Forest),
Terrain(BiomeKind::Desert),
Terrain(BiomeKind::Ocean),
];
&GOODS
}
pub fn transportation_effort(g: Good) -> f32 {
match g {
Terrain(_) | Territory(_) | RoadSecurity => 0.0,
Coin => 0.01,
Potions => 0.1,
Armor => 2.0,
Stone => 4.0,
_ => 1.0,
}
}
impl Good {
pub fn list() -> &'static [Self] {
static GOODS: [Good; 10] = [
Wheat, Flour, Meat, Fish, Game, Food, Logs, Wood, Rock, Stone,
];
&GOODS
pub fn decay_rate(g: Good) -> f32 {
match g {
Food => 0.2,
Flour => 0.1,
Meat => 0.25,
Ingredients => 0.1,
_ => 0.0,
}
}
pub fn decay_rate(&self) -> f32 {
match self {
Food => 0.2,
Wheat => 0.1,
Meat => 0.25,
Fish => 0.2,
_ => 0.0,
}
}
/** you can't accumulate or save these options/resources for later */
pub fn direct_use_goods() -> &'static [Good] {
static DIRECT_USE: [Good; 13] = [
Transportation,
Territory(BiomeKind::Grassland),
Territory(BiomeKind::Forest),
Territory(BiomeKind::Lake),
Territory(BiomeKind::Ocean),
Territory(BiomeKind::Mountain),
RoadSecurity,
Terrain(BiomeKind::Grassland),
Terrain(BiomeKind::Forest),
Terrain(BiomeKind::Lake),
Terrain(BiomeKind::Ocean),
Terrain(BiomeKind::Mountain),
Terrain(BiomeKind::Desert),
];
&DIRECT_USE
}
impl Labor {
pub fn list() -> &'static [Self] {
static LABORS: [Labor; 6] = [Farmer, Lumberjack, Miner, Fisher, Hunter, Cook];
&LABORS
pub fn list() -> impl Iterator<Item = Self> {
(0..LABOR.read().0.len())
.filter(|&i| i != (DUMMY_LABOR.0 as usize))
.map(|i| Self(i as u8, PhantomData))
}
}

View File

@ -141,10 +141,20 @@ impl Site {
wpos2d: Vec2<i32>,
get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
supplement: &mut ChunkSupplement,
site_id: common::trade::SiteId,
) {
match &self.kind {
SiteKind::Settlement(s) => {
s.apply_supplement(dynamic_rng, wpos2d, get_column, supplement)
let economy = common::trade::SiteInformation {
id: site_id,
unconsumed_stock: self
.economy
.unconsumed_stock
.iter()
.map(|(g, a)| (g, *a))
.collect(),
};
s.apply_supplement(dynamic_rng, wpos2d, get_column, supplement, economy)
},
SiteKind::Dungeon(d) => d.apply_supplement(dynamic_rng, wpos2d, get_column, supplement),
SiteKind::Castle(c) => c.apply_supplement(dynamic_rng, wpos2d, get_column, supplement),
@ -152,4 +162,6 @@ impl Site {
SiteKind::Tree(_) => {},
}
}
pub fn do_economic_simulation(&self) -> bool { matches!(self.kind, SiteKind::Settlement(_)) }
}

View File

@ -23,6 +23,7 @@ use common::{
spiral::Spiral2d,
store::{Id, Store},
terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
trade::SiteInformation,
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, WriteVol},
};
use fxhash::FxHasher64;
@ -855,7 +856,21 @@ impl Settlement {
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
supplement: &mut ChunkSupplement,
economy: SiteInformation,
) {
// let economy: HashMap<Good, (f32, f32)> = SiteInformation::economy
// .values
// .iter()
// .map(|(g, v)| {
// (
// g,
// (
// v.unwrap_or(Economy::MINIMUM_PRICE),
// economy.stocks[g] + economy.surplus[g],
// ),
// )
// })
// .collect();
for y in 0..TerrainChunkSize::RECT_SIZE.y as i32 {
for x in 0..TerrainChunkSize::RECT_SIZE.x as i32 {
let offs = Vec2::new(x, y);
@ -938,6 +953,17 @@ impl Settlement {
.with_skillset_config(
common::skillset_builder::SkillSetConfig::Guard,
),
1 => entity
.with_main_tool(Item::new_from_asset_expect(
"common.items.weapons.bow.eldwood-0",
))
.with_name("Merchant")
.with_level(dynamic_rng.gen_range(10..15))
.with_loadout_config(loadout_builder::LoadoutConfig::Merchant)
.with_skillset_config(
common::skillset_builder::SkillSetConfig::Merchant,
)
.with_economy(&economy),
_ => entity
.with_main_tool(Item::new_from_asset_expect(
match dynamic_rng.gen_range(0..7) {

View File

@ -1,6 +1,15 @@
use crate::util::DHashMap;
use std::hash::Hash;
/** A static table of known values where any key always maps to a single value.
It's not really intended to be a "collection" in the same way that, say, HashMap or Vec is
Since it's not intended to have a way of expressing that a value is not present (hence the default behaviour)
It's really quite specifically tailored to its application in the economy code where it wouldn't make sense to not have certain entries
Store is a bit different in that it is the one to generate an index, and so it can hold as many things as you like
Whereas with MapVec, we always know the index ahead of time.
**/
#[derive(Clone, Debug)]
pub struct MapVec<K, T> {
/// We use this hasher (FxHasher32) because
@ -33,6 +42,13 @@ impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
}
}
pub fn from_iter(i: impl Iterator<Item = (K, T)>, default: T) -> Self {
Self {
entries: i.collect(),
default,
}
}
pub fn from_default(default: T) -> Self {
Self {
entries: DHashMap::default(),