mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'christof/trading_MVP' into 'master'
Economy rework, inter site trading, merchants in towns trading with players See merge request veloren/veloren!1744
This commit is contained in:
commit
72ca632a2c
@ -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