mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
fcdd7f1c8b
commit
15b11d9154
@ -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
2
Cargo.lock
generated
@ -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",
|
||||
|
44
assets/common/item_price_calculation.ron
Normal file
44
assets/common/item_price_calculation.ron
Normal 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
|
||||
])
|
@ -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
|
||||
|
110
assets/common/professions.ron
Normal file
110
assets/common/professions.ron
Normal 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: [],
|
||||
),
|
||||
])
|
@ -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(())
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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;
|
||||
|
368
common/src/comp/inventory/trade_pricing.rs
Normal file
368
common/src/comp/inventory/trade_pricing.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -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> {
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
87
server/src/events/information.rs
Normal file
87
server/src/events/information.rs
Normal 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));
|
||||
}
|
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -114,6 +114,7 @@ impl<'a> System<'a> for Sys {
|
||||
agent: Some(comp::Agent::new(
|
||||
None,
|
||||
matches!(body, comp::Body::Humanoid(_)),
|
||||
None,
|
||||
&body,
|
||||
false,
|
||||
)),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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
220
world/economy_testinput.ron
Normal 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: [],
|
||||
),
|
||||
]
|
@ -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},
|
||||
|
@ -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>;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
68
world/src/site/economy.gv
Normal 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];
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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(_)) }
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user