mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'master' of https://gitlab.com/veloren/veloren into xvar/wgpu-egui
This commit is contained in:
commit
b4dd476318
@ -17,11 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Entity-entity pushback is no longer applied in forced movement states like rolling and leaping.
|
||||
- Updated audio library (rodio 0.13 -> 0.14).
|
||||
- Improve entity-terrain physics performance by reducing the number of voxel lookups.
|
||||
- Clay Golem uses shockwave only after specific fraction of health and other difficulty adjustments.
|
||||
|
||||
### Removed
|
||||
- Enemies no more spawn in dungeon boss room
|
||||
|
||||
### Fixed
|
||||
|
||||
- Crafting Stations aren't exploadable anymore
|
||||
- Cases where no audio output could be produced before.
|
||||
- Significantly improved the performance of playing sound effects
|
||||
|
||||
|
@ -15,5 +15,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Cultist,
|
||||
)
|
||||
)
|
||||
|
@ -10,5 +10,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
)
|
||||
|
@ -10,5 +10,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
)
|
||||
|
@ -2,13 +2,19 @@ BasicBeam(
|
||||
buildup_duration: 0.5,
|
||||
recover_duration: 0.4,
|
||||
beam_duration: 0.25,
|
||||
damage: 100,
|
||||
damage: 70,
|
||||
tick_rate: 2.0,
|
||||
range: 40.0,
|
||||
max_angle: 1.0,
|
||||
damage_effect: None,
|
||||
damage_effect: Some(Buff((
|
||||
kind: Burning,
|
||||
dur_secs: 5.0,
|
||||
strength: DamageFraction(0.75),
|
||||
chance: 0.75,
|
||||
))),
|
||||
energy_regen: 50,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: FromOri,
|
||||
ori_rate: 0.07,
|
||||
specifier: ClayGolem,
|
||||
)
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
BasicRanged(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.5,
|
||||
recover_duration: 0.8,
|
||||
buildup_duration: 0.8,
|
||||
recover_duration: 0.5,
|
||||
projectile: ClayRocket(
|
||||
damage: 500.0,
|
||||
knockback: 25.0,
|
||||
radius: 10.0,
|
||||
radius: 5.0,
|
||||
),
|
||||
projectile_body: Object(ClayRocket),
|
||||
projectile_light: None,
|
||||
|
@ -1,6 +1,6 @@
|
||||
Shockwave(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.6,
|
||||
buildup_duration: 1.5,
|
||||
swing_duration: 0.12,
|
||||
recover_duration: 1.2,
|
||||
damage: 500,
|
||||
@ -9,9 +9,9 @@ Shockwave(
|
||||
shockwave_angle: 180.0,
|
||||
shockwave_vertical_angle: 90.0,
|
||||
shockwave_speed: 15.0,
|
||||
shockwave_duration: 2.5,
|
||||
shockwave_duration: 3.5,
|
||||
requires_ground: true,
|
||||
move_efficiency: 0.0,
|
||||
damage_kind: Crushing,
|
||||
specifier: Ground,
|
||||
)
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
BasicMelee(
|
||||
energy_cost: 0,
|
||||
buildup_duration: 0.8,
|
||||
swing_duration: 0.2,
|
||||
swing_duration: 0.1,
|
||||
recover_duration: 0.5,
|
||||
base_damage: 200,
|
||||
base_poise_damage: 50,
|
||||
|
@ -15,5 +15,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Cultist,
|
||||
)
|
||||
)
|
||||
|
@ -10,5 +10,6 @@ BasicBeam(
|
||||
energy_regen: 25,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: HealingBeam,
|
||||
)
|
||||
)
|
||||
|
@ -10,5 +10,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
)
|
||||
|
@ -15,5 +15,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Bubbles,
|
||||
)
|
||||
|
@ -15,5 +15,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.3,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
)
|
||||
|
@ -15,5 +15,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Frost,
|
||||
)
|
||||
)
|
||||
|
@ -10,5 +10,6 @@ BasicBeam(
|
||||
energy_regen: 50,
|
||||
energy_drain: 0,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: LifestealBeam
|
||||
)
|
||||
)
|
||||
|
@ -15,5 +15,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 350,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
)
|
||||
|
@ -10,5 +10,6 @@ BasicBeam(
|
||||
energy_regen: 0,
|
||||
energy_drain: 350,
|
||||
orientation_behavior: Normal,
|
||||
ori_rate: 0.6,
|
||||
specifier: Flamethrower,
|
||||
)
|
||||
|
@ -3,8 +3,8 @@
|
||||
(250, ShortGrass),
|
||||
(50, CaveMushroom),
|
||||
(50, Mushroom),
|
||||
(30, AmethystSmall),
|
||||
(15, TopazSmall),
|
||||
(5, AmethystSmall),
|
||||
(5, TopazSmall),
|
||||
(15, Tin),
|
||||
(12, Copper),
|
||||
(15, Iron),
|
||||
|
@ -2,16 +2,16 @@
|
||||
loot_tables: [
|
||||
// balance the loot tables against each other (higher= more common= smaller price)
|
||||
// Weapons
|
||||
(16.0, true, "common.loot_tables.weapons.starter"),
|
||||
(12.0, true, "common.loot_tables.weapons.tier-0"),
|
||||
(6.0, true, "common.loot_tables.weapons.tier-1"),
|
||||
(32.0, true, "common.loot_tables.weapons.starter"),
|
||||
(16.0, true, "common.loot_tables.weapons.tier-0"),
|
||||
(8.0, true, "common.loot_tables.weapons.tier-1"),
|
||||
(4.0, true, "common.loot_tables.weapons.tier-2"),
|
||||
(2.0, true, "common.loot_tables.weapons.tier-3"),
|
||||
(1.0, false, "common.loot_tables.weapons.tier-4"),
|
||||
(0.5, false, "common.loot_tables.weapons.tier-5"),
|
||||
(0.05, false, "common.loot_tables.weapons.cultist"),
|
||||
(0.05, false, "common.loot_tables.weapons.cave"),
|
||||
(0.04, false, "common.loot_tables.weapons.legendary"),
|
||||
(0.025, false, "common.loot_tables.weapons.cultist"),
|
||||
(0.025, false, "common.loot_tables.weapons.cave"),
|
||||
(0.02, false, "common.loot_tables.weapons.legendary"),
|
||||
// Armor
|
||||
(20.0, true, "common.loot_tables.armor.cloth"),
|
||||
(1.0, true, "common.loot_tables.armor.twigs"),
|
||||
@ -25,7 +25,14 @@ loot_tables: [
|
||||
(0.4, true, "common.loot_tables.food.wild_ingredients"),
|
||||
(0.2, true, "common.loot_tables.food.prepared"),
|
||||
// Potions
|
||||
(0.2, true, "common.loot_tables.consumable.potion"),
|
||||
//
|
||||
// crafted from food, no need to duplicate it here.
|
||||
// Big potions aren't crafted, but our potions
|
||||
// from merchants are already abused
|
||||
//
|
||||
// Place them back we will have better situation with potions
|
||||
// and economy.
|
||||
//
|
||||
// Misc
|
||||
(0.1, true, "common.loot_tables.consumable.throwable"),
|
||||
(0.7, true, "common.loot_tables.consumable.misc"),
|
||||
@ -37,7 +44,7 @@ good_scaling: [
|
||||
(Potions, 0.0075), // common.items.consumable.potion_minor
|
||||
(Food, 0.1), // common.items.food.mushroom
|
||||
(Coin, 1.0), // common.items.utility.coins
|
||||
(Armor, 0.05), // common.items.armor.misc.pants.worker_blue
|
||||
(Tools, 0.10), // common.items.weapons.staff.starter_staff
|
||||
(Ingredients, 0.15), // common.items.crafting_ing.leather_scraps
|
||||
(Armor, 0.5), // common.items.armor.misc.pants.worker_blue
|
||||
(Tools, 0.25), // common.items.weapons.staff.starter_staff
|
||||
(Ingredients, 0.25), // common.items.crafting_ing.leather_scraps
|
||||
])
|
||||
|
@ -1,8 +1,8 @@
|
||||
[
|
||||
(3.0, Item("common.items.mineral.gem.topaz")),
|
||||
(3.0, Item("common.items.mineral.gem.amethyst")),
|
||||
(6.0, Item("common.items.mineral.gem.topaz")),
|
||||
(8.0, Item("common.items.mineral.gem.amethyst")),
|
||||
(1.6, Item("common.items.mineral.gem.sapphire")),
|
||||
(1.2, Item("common.items.mineral.gem.emerald")),
|
||||
(0.8, Item("common.items.mineral.gem.ruby")),
|
||||
(0.4, Item("common.items.mineral.gem.diamond")),
|
||||
]
|
||||
]
|
||||
|
@ -2,5 +2,5 @@
|
||||
(2.0, Item("common.items.crafting_ing.stones")),
|
||||
(0.5, Item("common.items.mineral.ore.veloritefrag")),
|
||||
(0.25, Item("common.items.mineral.ore.velorite")),
|
||||
(0.25, LootTable("common.loot_tables.materials.gems")),
|
||||
]
|
||||
(0.15, LootTable("common.loot_tables.materials.gems")),
|
||||
]
|
||||
|
@ -1,38 +1,37 @@
|
||||
// Loot table that exists purely for price rationalisation
|
||||
// Please keep it sorting by rarity so it's easier to reason about things
|
||||
[
|
||||
(1.0, Item("common.items.crafting_ing.honey")),
|
||||
(1.5, Item("common.items.crafting_ing.leather.leather_strips")),
|
||||
(0.08, Item("common.items.crafting_ing.leather.rigid_leather")),
|
||||
(1.0, Item("common.items.crafting_ing.leather.simple_leather")),
|
||||
(0.4, Item("common.items.crafting_ing.leather.thick_leather")),
|
||||
(1.0, Item("common.items.crafting_ing.hide.animal_hide")),
|
||||
(0.5, Item("common.items.crafting_ing.hide.tough_hide")),
|
||||
(0.2, Item("common.items.crafting_ing.hide.scales")),
|
||||
(0.8, Item("common.items.crafting_ing.animal_misc.fur")),
|
||||
(0.15, Item("common.items.crafting_ing.animal_misc.grim_eyeball")),
|
||||
(0.1, Item("common.items.crafting_ing.animal_misc.icy_fang")),
|
||||
(0.08, Item("common.items.crafting_ing.animal_misc.large_horn")),
|
||||
(0.15, Item("common.items.crafting_ing.animal_misc.lively_vine")),
|
||||
(0.08, Item("common.items.crafting_ing.animal_misc.phoenix_feather")),
|
||||
(1.0, Item("common.items.food.meat.beast_small_raw")),
|
||||
(0.6, Item("common.items.food.meat.beast_large_raw")),
|
||||
(1.3, Item("common.items.food.meat.bird_raw")),
|
||||
(1.2, Item("common.items.food.meat.fish_raw")),
|
||||
(0.8, Item("common.items.food.meat.tough_raw")),
|
||||
(0.2, Item("common.items.mineral.ore.bloodstone")),
|
||||
(1.0, Item("common.items.mineral.ore.coal")),
|
||||
(0.4, Item("common.items.mineral.ore.cobalt")),
|
||||
(1.5, Item("common.items.mineral.ore.tin")),
|
||||
(1.5, Item("common.items.mineral.ore.copper")),
|
||||
(0.03, Item("common.items.mineral.ore.gold")),
|
||||
(0.8, Item("common.items.mineral.ore.iron")),
|
||||
(0.05, Item("common.items.mineral.ore.silver")),
|
||||
(1.2, Item("common.items.mineral.ore.velorite")),
|
||||
// Ores
|
||||
// Uncomment when bug with crafting doesn't propagating can_sell will be fixed
|
||||
// (0.03, Item("common.items.mineral.ore.gold")),
|
||||
// (0.045, Item("common.items.mineral.ore.silver")),
|
||||
// (0.1, Item("common.items.mineral.ore.bloodstone")),
|
||||
// (0.2, Item("common.items.mineral.ore.cobalt")),
|
||||
(0.25, Item("common.items.mineral.ore.coal")),
|
||||
(0.3, Item("common.items.mineral.ore.iron")),
|
||||
(0.5, Item("common.items.mineral.ore.velorite")),
|
||||
(0.6, Item("common.items.mineral.ore.veloritefrag")),
|
||||
(0.8, Item("common.items.mineral.gem.amethyst")),
|
||||
(0.2, Item("common.items.mineral.gem.diamond")),
|
||||
(0.6, Item("common.items.mineral.gem.emerald")),
|
||||
(0.4, Item("common.items.mineral.gem.ruby")),
|
||||
(0.4, Item("common.items.mineral.gem.sapphire")),
|
||||
(0.9, Item("common.items.mineral.gem.topaz")),
|
||||
(1.5, Item("common.items.mineral.ore.copper")),
|
||||
(1.5, Item("common.items.mineral.ore.tin")),
|
||||
// Animal Hide
|
||||
(0.1, Item("common.items.crafting_ing.leather.rigid_leather")),
|
||||
(0.2, Item("common.items.crafting_ing.hide.scales")),
|
||||
(0.7, Item("common.items.crafting_ing.hide.tough_hide")),
|
||||
(1.0, Item("common.items.crafting_ing.hide.animal_hide")),
|
||||
(1.5, Item("common.items.crafting_ing.leather.leather_strips")),
|
||||
// Mob Drops
|
||||
(0.01, Item("common.items.crafting_ing.animal_misc.phoenix_feather")),
|
||||
(0.08, Item("common.items.crafting_ing.animal_misc.large_horn")),
|
||||
(0.1, Item("common.items.crafting_ing.animal_misc.icy_fang")),
|
||||
(0.15, Item("common.items.crafting_ing.animal_misc.grim_eyeball")),
|
||||
(0.15, Item("common.items.crafting_ing.animal_misc.lively_vine")),
|
||||
(1.2, Item("common.items.crafting_ing.animal_misc.fur")),
|
||||
// Meats
|
||||
(0.6, Item("common.items.food.meat.beast_large_raw")),
|
||||
(0.8, Item("common.items.food.meat.tough_raw")),
|
||||
(1.0, Item("common.items.food.meat.beast_small_raw")),
|
||||
(1.2, Item("common.items.food.meat.fish_raw")),
|
||||
(1.3, Item("common.items.food.meat.bird_raw")),
|
||||
// Others
|
||||
(1.0, Item("common.items.crafting_ing.honey")),
|
||||
]
|
||||
|
25
assets/world/dungeon/difficulty_distribution.ron
Normal file
25
assets/world/dungeon/difficulty_distribution.ron
Normal file
@ -0,0 +1,25 @@
|
||||
/// Distribution of different dungeon levels.
|
||||
///
|
||||
/// first number is dungeon level, integer
|
||||
/// second number is weight, any normal positive float (not a NaN, for example)
|
||||
///
|
||||
/// Values are relative to each other,
|
||||
/// lesser weight means there will be less dungeons of that tier.
|
||||
///
|
||||
/// General rules:
|
||||
/// 1) Weight should not be less then zero
|
||||
/// 2) At least some of weights shouldn't be a zero
|
||||
/// 3) Keep it synced with number of dungeon levels
|
||||
/// 4) Keep these pairs sorted from lowest to highest tier
|
||||
///
|
||||
/// Tips:
|
||||
/// 1) Set every probability to 0.0 and left one with any other number
|
||||
/// and you will have map full of dungeons of same level
|
||||
([
|
||||
(0, 5.0),
|
||||
(1, 4.0),
|
||||
(2, 4.0),
|
||||
(3, 2.0),
|
||||
(4, 2.0),
|
||||
(5, 1.0),
|
||||
])
|
@ -247,6 +247,7 @@ pub enum CharacterAbility {
|
||||
energy_regen: f32,
|
||||
energy_drain: f32,
|
||||
orientation_behavior: basic_beam::OrientationBehavior,
|
||||
ori_rate: f32,
|
||||
specifier: beam::FrontendSpecifier,
|
||||
},
|
||||
BasicAura {
|
||||
@ -1651,6 +1652,7 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
|
||||
energy_regen,
|
||||
energy_drain,
|
||||
orientation_behavior,
|
||||
ori_rate,
|
||||
specifier,
|
||||
} => CharacterState::BasicBeam(basic_beam::Data {
|
||||
static_data: basic_beam::StaticData {
|
||||
@ -1666,6 +1668,7 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState {
|
||||
energy_drain: *energy_drain,
|
||||
ability_info,
|
||||
orientation_behavior: *orientation_behavior,
|
||||
ori_rate: *ori_rate,
|
||||
specifier: *specifier,
|
||||
},
|
||||
timer: Duration::default(),
|
||||
|
@ -512,7 +512,7 @@ impl Body {
|
||||
_ => 10000,
|
||||
},
|
||||
Body::Golem(golem) => match golem.species {
|
||||
golem::Species::ClayGolem => 7500,
|
||||
golem::Species::ClayGolem => 4500,
|
||||
_ => 10000,
|
||||
},
|
||||
Body::Theropod(theropod) => match theropod.species {
|
||||
@ -677,7 +677,7 @@ impl Body {
|
||||
_ => 1.0,
|
||||
},
|
||||
Body::Golem(g) => match g.species {
|
||||
golem::Species::ClayGolem => 1.2,
|
||||
golem::Species::ClayGolem => 2.0,
|
||||
_ => 1.0,
|
||||
},
|
||||
_ => 1.0,
|
||||
|
@ -1,3 +1,6 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
//#![warn(clippy::nursery)]
|
||||
|
||||
use crate::{
|
||||
assets::{self, AssetExt},
|
||||
lottery::{LootSpec, Lottery},
|
||||
@ -10,19 +13,19 @@ use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use tracing::{info, warn};
|
||||
|
||||
type Entry = (String, f32, bool);
|
||||
|
||||
type Entries = Vec<Entry>;
|
||||
const PRICING_DEBUG: bool = false;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TradePricing {
|
||||
// items of different good kinds
|
||||
tools: Entries,
|
||||
armor: Entries,
|
||||
potions: Entries,
|
||||
food: Entries,
|
||||
ingredients: Entries,
|
||||
other: Entries,
|
||||
|
||||
// good_scaling of coins
|
||||
coin_scale: f32,
|
||||
// rng: ChaChaRng,
|
||||
|
||||
@ -31,6 +34,46 @@ pub struct TradePricing {
|
||||
equality_set: EqualitySet,
|
||||
}
|
||||
|
||||
// item asset specifier, probability, whether it's sellable by merchants
|
||||
type Entry = (String, f32, bool);
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Entries {
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
impl Entries {
|
||||
fn add(&mut self, eqset: &EqualitySet, item_name: &str, probability: f32, can_sell: bool) {
|
||||
let canonical_itemname = eqset
|
||||
.equivalence_class
|
||||
.get(item_name)
|
||||
.map_or(item_name, |i| &**i);
|
||||
|
||||
let old = self
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|(name, _, _)| *name == *canonical_itemname);
|
||||
|
||||
// Increase probability if already in entries, or add new entry
|
||||
if let Some((asset, ref mut old_probability, _)) = old {
|
||||
if PRICING_DEBUG {
|
||||
info!("Update {} {}+{}", asset, old_probability, probability);
|
||||
}
|
||||
*old_probability += probability;
|
||||
} else {
|
||||
if PRICING_DEBUG {
|
||||
info!("New {} {}", item_name, probability);
|
||||
}
|
||||
self.entries
|
||||
.push((canonical_itemname.to_owned(), probability, can_sell));
|
||||
if canonical_itemname != item_name {
|
||||
// Add the non-canonical item so that it'll show up in merchant inventories
|
||||
self.entries.push((item_name.to_owned(), 0.0, can_sell));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref TRADE_PRICING: TradePricing = TradePricing::read();
|
||||
}
|
||||
@ -47,21 +90,24 @@ impl assets::Asset for ProbabilityFile {
|
||||
}
|
||||
|
||||
impl From<Vec<(f32, LootSpec)>> for ProbabilityFile {
|
||||
fn from(content: Vec<(f32, LootSpec)>) -> ProbabilityFile {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn from(content: Vec<(f32, LootSpec)>) -> Self {
|
||||
Self {
|
||||
content: content
|
||||
.into_iter()
|
||||
.flat_map(|(a, b)| match b {
|
||||
LootSpec::Item(c) => vec![(a, c)].into_iter(),
|
||||
LootSpec::ItemQuantity(c, d, e) => {
|
||||
vec![(a * (d + e) as f32 / 2.0, c)].into_iter()
|
||||
.flat_map(|(p0, loot)| match loot {
|
||||
LootSpec::Item(asset) => vec![(p0, asset)].into_iter(),
|
||||
LootSpec::ItemQuantity(asset, a, b) => {
|
||||
vec![(p0 * (a + b) as f32 / 2.0, asset)].into_iter()
|
||||
},
|
||||
LootSpec::LootTable(c) => {
|
||||
let total = Lottery::<LootSpec>::load_expect(&c).read().total();
|
||||
ProbabilityFile::load_expect_cloned(&c)
|
||||
LootSpec::LootTable(table_asset) => {
|
||||
let total = Lottery::<LootSpec>::load_expect(&table_asset)
|
||||
.read()
|
||||
.total();
|
||||
Self::load_expect_cloned(&table_asset)
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|(d, e)| (a * d / total, e))
|
||||
.map(|(p1, asset)| (p0 * p1 / total, asset))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
},
|
||||
@ -74,11 +120,12 @@ impl From<Vec<(f32, LootSpec)>> for ProbabilityFile {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TradingPriceFile {
|
||||
pub loot_tables: Vec<(f32, bool, String)>,
|
||||
pub good_scaling: Vec<(Good, f32)>, // the amount of Good equivalent to the most common item
|
||||
// the amount of Good equivalent to the most common item
|
||||
pub good_scaling: Vec<(Good, f32)>,
|
||||
}
|
||||
|
||||
impl assets::Asset for TradingPriceFile {
|
||||
type Loader = assets::LoadFrom<TradingPriceFile, assets::RonLoader>;
|
||||
type Loader = assets::RonLoader;
|
||||
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
||||
@ -95,10 +142,10 @@ impl assets::Compound for EqualitySet {
|
||||
id: &str,
|
||||
) -> Result<Self, assets::Error> {
|
||||
let manifest = cache.load::<assets::Ron<Vec<Vec<String>>>>(id)?;
|
||||
let mut ret = EqualitySet {
|
||||
let mut ret = Self {
|
||||
equivalence_class: HashMap::new(),
|
||||
};
|
||||
for set in manifest.read().0.iter() {
|
||||
for set in &manifest.read().0 {
|
||||
let mut iter = set.iter();
|
||||
if let Some(first) = iter.next() {
|
||||
let first = first.to_string();
|
||||
@ -121,151 +168,179 @@ struct RememberedRecipe {
|
||||
input: Vec<(String, u32)>,
|
||||
}
|
||||
|
||||
fn sort_and_normalize(entryvec: &mut [Entry], scale: f32) {
|
||||
if !entryvec.is_empty() {
|
||||
entryvec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
if let Some((_, max_scale, _)) = entryvec.last() {
|
||||
// most common item has frequency max_scale. avoid NaN
|
||||
let rescale = scale / max_scale;
|
||||
for i in entryvec.iter_mut() {
|
||||
i.1 *= rescale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scaling(contents: &AssetGuard<TradingPriceFile>, good: Good) -> f32 {
|
||||
contents
|
||||
.good_scaling
|
||||
.iter()
|
||||
.find(|(good_kind, _)| *good_kind == good)
|
||||
.map_or(1.0, |(_, scaling)| *scaling)
|
||||
}
|
||||
|
||||
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;
|
||||
const UNAVAILABLE_PRICE: f32 = 1_000_000.0;
|
||||
|
||||
// add this much of a non-consumed crafting tool price
|
||||
|
||||
fn get_list(&self, good: Good) -> &[Entry] {
|
||||
match good {
|
||||
Good::Armor => &self.armor,
|
||||
Good::Tools => &self.tools,
|
||||
Good::Potions => &self.potions,
|
||||
Good::Food => &self.food,
|
||||
Good::Ingredients => &self.ingredients,
|
||||
Good::Armor => &self.armor.entries,
|
||||
Good::Tools => &self.tools.entries,
|
||||
Good::Potions => &self.potions.entries,
|
||||
Good::Food => &self.food.entries,
|
||||
Good::Ingredients => &self.ingredients.entries,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_list_mut(&mut self, good: Good) -> &mut [Entry] {
|
||||
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,
|
||||
Good::Armor => &mut self.armor.entries,
|
||||
Good::Tools => &mut self.tools.entries,
|
||||
Good::Potions => &mut self.potions.entries,
|
||||
Good::Food => &mut self.food.entries,
|
||||
Good::Ingredients => &mut self.ingredients.entries,
|
||||
_ => &mut [],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_list_by_path(&self, name: &str) -> &[Entry] {
|
||||
match name {
|
||||
"common.items.crafting_ing.mindflayer_bag_damaged" => &self.armor,
|
||||
_ 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.mineral.") => &self.ingredients,
|
||||
_ if name.starts_with("common.items.flowers.") => &self.ingredients,
|
||||
_ if name.starts_with("common.items.crafting_tools.") => &self.other,
|
||||
_ if name.starts_with("common.items.lantern.") => &self.other,
|
||||
_ if name.starts_with("common.items.tool.") => &self.tools,
|
||||
// Armor
|
||||
// TODO: balance mindflayer bag price so this isn't needed
|
||||
"common.items.crafting_ing.mindflayer_bag_damaged" => &self.armor.entries,
|
||||
_ if name.starts_with("common.items.armor.") => &self.armor.entries,
|
||||
// Tools
|
||||
_ if name.starts_with("common.items.weapons.") => &self.tools.entries,
|
||||
_ if name.starts_with("common.items.tool.") => &self.tools.entries,
|
||||
// Ingredients
|
||||
_ if name.starts_with("common.items.crafting_ing.") => &self.ingredients.entries,
|
||||
_ if name.starts_with("common.items.mineral.") => &self.ingredients.entries,
|
||||
_ if name.starts_with("common.items.flowers.") => &self.ingredients.entries,
|
||||
// Potions
|
||||
_ if name.starts_with("common.items.consumable.") => &self.potions.entries,
|
||||
// Food
|
||||
_ if name.starts_with("common.items.food.") => &self.food.entries,
|
||||
// Other
|
||||
_ if name.starts_with("common.items.glider.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.utility.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.boss_drops.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.crafting_tools.") => &self.other.entries,
|
||||
_ if name.starts_with("common.items.lantern.") => &self.other.entries,
|
||||
_ => {
|
||||
info!("unknown loot item {}", name);
|
||||
&self.other
|
||||
warn!("unknown loot item {}", name);
|
||||
&self.other.entries
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_list_by_path_mut(&mut self, name: &str) -> &mut Entries {
|
||||
match name {
|
||||
// Armor
|
||||
// TODO: balance mindflayer bag price so this isn't needed
|
||||
"common.items.crafting_ing.mindflayer_bag_damaged" => &mut self.armor,
|
||||
_ 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,
|
||||
// Tools
|
||||
_ 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.tool.") => &mut self.tools,
|
||||
// Ingredients
|
||||
_ if name.starts_with("common.items.crafting_ing.") => &mut self.ingredients,
|
||||
_ if name.starts_with("common.items.mineral.") => &mut self.ingredients,
|
||||
_ if name.starts_with("common.items.flowers.") => &mut self.ingredients,
|
||||
// Potions
|
||||
_ if name.starts_with("common.items.consumable.") => &mut self.potions,
|
||||
// Food
|
||||
_ if name.starts_with("common.items.food.") => &mut self.food,
|
||||
// Other
|
||||
_ if name.starts_with("common.items.glider.") => &mut self.other,
|
||||
_ 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.crafting_tools.") => &mut self.other,
|
||||
_ if name.starts_with("common.items.lantern.") => &mut self.other,
|
||||
_ if name.starts_with("common.items.tool.") => &mut self.tools,
|
||||
_ => {
|
||||
info!("unknown loot item {}", name);
|
||||
warn!("unknown loot item {}", name);
|
||||
&mut self.other
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn read() -> Self {
|
||||
fn add(
|
||||
entryvec: &mut Entries,
|
||||
eqset: &EqualitySet,
|
||||
itemname: &str,
|
||||
probability: f32,
|
||||
can_sell: bool,
|
||||
) {
|
||||
let canonical_itemname = eqset
|
||||
.equivalence_class
|
||||
.get(itemname)
|
||||
.map(|i| &**i)
|
||||
.unwrap_or(itemname);
|
||||
let val = entryvec.iter_mut().find(|j| *j.0 == *canonical_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((canonical_itemname.to_string(), probability, can_sell));
|
||||
if canonical_itemname != itemname {
|
||||
// Add the non-canonical item so that it'll show up in merchant inventories
|
||||
entryvec.push((itemname.to_string(), 0.0, can_sell));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn sort_and_normalize(entryvec: &mut [Entry], scale: f32) {
|
||||
if !entryvec.is_empty() {
|
||||
entryvec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
if let Some((_, max_scale, _)) = entryvec.last() {
|
||||
// most common item has frequency max_scale. avoid NaN
|
||||
let rescale = scale / max_scale;
|
||||
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)
|
||||
}
|
||||
// look up price (inverse frequency) of an item
|
||||
fn price_lookup(&self, eqset: &EqualitySet, requested_name: &str) -> f32 {
|
||||
let canonical_name = eqset
|
||||
.equivalence_class
|
||||
.get(requested_name)
|
||||
.map_or(requested_name, |name| &**name);
|
||||
|
||||
let mut result = TradePricing::default();
|
||||
let files = TradingPriceFile::load_expect("common.item_price_calculation");
|
||||
let eqset = EqualitySet::load_expect("common.item_price_equality");
|
||||
result.equality_set = eqset.read().clone();
|
||||
let contents = files.read();
|
||||
for i in contents.loot_tables.iter() {
|
||||
let goods = self.get_list_by_path(canonical_name);
|
||||
// even if we multiply by INVEST_FACTOR we need to remain
|
||||
// above UNAVAILABLE_PRICE (add 1.0 to compensate rounding errors)
|
||||
goods
|
||||
.iter()
|
||||
.find(|(name, _, _)| name == canonical_name)
|
||||
.map_or(
|
||||
Self::UNAVAILABLE_PRICE / Self::INVEST_FACTOR + 1.0,
|
||||
|(_, freq, _)| 1.0 / freq,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn calculate_material_cost(&self, r: &RememberedRecipe, eqset: &EqualitySet) -> f32 {
|
||||
r.input
|
||||
.iter()
|
||||
.map(|(name, amount)| {
|
||||
self.price_lookup(eqset, name) * (*amount as f32).max(Self::INVEST_FACTOR)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
// re-look up prices and sort the vector by ascending material cost, return
|
||||
// whether first cost is finite
|
||||
fn sort_by_price(&self, recipes: &mut Vec<RememberedRecipe>, eqset: &EqualitySet) -> bool {
|
||||
for recipe in recipes.iter_mut() {
|
||||
recipe.material_cost = self.calculate_material_cost(recipe, eqset);
|
||||
}
|
||||
recipes.sort_by(|a, b| a.material_cost.partial_cmp(&b.material_cost).unwrap());
|
||||
//info!(?recipes);
|
||||
recipes
|
||||
.first()
|
||||
.filter(|recipe| recipe.material_cost < Self::UNAVAILABLE_PRICE)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn read() -> Self {
|
||||
let mut result = Self::default();
|
||||
let price_config = TradingPriceFile::load_expect("common.item_price_calculation").read();
|
||||
let eqset = EqualitySet::load_expect("common.item_price_equality").read();
|
||||
result.equality_set = eqset.clone();
|
||||
for table in &price_config.loot_tables {
|
||||
if PRICING_DEBUG {
|
||||
info!(?i);
|
||||
info!(?table);
|
||||
}
|
||||
let loot = ProbabilityFile::load_expect(&i.2);
|
||||
for j in loot.read().content.iter() {
|
||||
add(
|
||||
&mut result.get_list_by_path_mut(&j.1),
|
||||
&eqset.read(),
|
||||
&j.1,
|
||||
i.0 * j.0,
|
||||
i.1,
|
||||
let (frequency, can_sell, asset_path) = table;
|
||||
let loot = ProbabilityFile::load_expect(asset_path);
|
||||
for (p, item_asset) in &loot.read().content {
|
||||
result.get_list_by_path_mut(item_asset).add(
|
||||
&eqset,
|
||||
item_asset,
|
||||
frequency * p,
|
||||
*can_sell,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -273,12 +348,13 @@ impl TradePricing {
|
||||
// Apply recipe book
|
||||
let book = default_recipe_book().read();
|
||||
let mut ordered_recipes: Vec<RememberedRecipe> = Vec::new();
|
||||
for (_, r) in book.iter() {
|
||||
for (_, recipe) in book.iter() {
|
||||
let (ref asset_path, amount) = recipe.output;
|
||||
ordered_recipes.push(RememberedRecipe {
|
||||
output: r.output.0.id().into(),
|
||||
amount: r.output.1,
|
||||
material_cost: TradePricing::UNAVAILABLE_PRICE,
|
||||
input: r
|
||||
output: asset_path.id().into(),
|
||||
amount,
|
||||
material_cost: Self::UNAVAILABLE_PRICE,
|
||||
input: recipe
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|&(ref recipe_input, count)| {
|
||||
@ -291,61 +367,19 @@ impl TradePricing {
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
// look up price (inverse frequency) of an item
|
||||
fn price_lookup(s: &TradePricing, eqset: &EqualitySet, name: &str) -> f32 {
|
||||
let name = eqset
|
||||
.equivalence_class
|
||||
.get(name)
|
||||
.map(|i| &**i)
|
||||
.unwrap_or(name);
|
||||
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,
|
||||
eqset: &EqualitySet,
|
||||
r: &RememberedRecipe,
|
||||
) -> f32 {
|
||||
r.input
|
||||
.iter()
|
||||
.map(|(name, amount)| {
|
||||
price_lookup(s, eqset, 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,
|
||||
eqset: &EqualitySet,
|
||||
vec: &mut Vec<RememberedRecipe>,
|
||||
) -> bool {
|
||||
for e in vec.iter_mut() {
|
||||
e.material_cost = calculate_material_cost(s, eqset, e);
|
||||
}
|
||||
vec.sort_by(|a, b| a.material_cost.partial_cmp(&b.material_cost).unwrap());
|
||||
//info!(?vec);
|
||||
vec.first()
|
||||
.filter(|recipe| recipe.material_cost < TradePricing::UNAVAILABLE_PRICE)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
// re-evaluate prices based on crafting tables
|
||||
// (start with cheap ones to avoid changing material prices after evaluation)
|
||||
while price_sort(&result, &eqset.read(), &mut ordered_recipes) {
|
||||
ordered_recipes.retain(|e| {
|
||||
if e.material_cost < 1e-5 {
|
||||
while result.sort_by_price(&mut ordered_recipes, &eqset) {
|
||||
ordered_recipes.retain(|recipe| {
|
||||
if recipe.material_cost < 1e-5 {
|
||||
false
|
||||
} else if e.material_cost < TradePricing::UNAVAILABLE_PRICE {
|
||||
let actual_cost = calculate_material_cost(&result, &eqset.read(), e);
|
||||
add(
|
||||
&mut result.get_list_by_path_mut(&e.output),
|
||||
&eqset.read(),
|
||||
&e.output,
|
||||
(e.amount as f32) / actual_cost * TradePricing::CRAFTING_FACTOR,
|
||||
} else if recipe.material_cost < Self::UNAVAILABLE_PRICE {
|
||||
let actual_cost = result.calculate_material_cost(recipe, &eqset);
|
||||
result.get_list_by_path_mut(&recipe.output).add(
|
||||
&eqset,
|
||||
&recipe.output,
|
||||
(recipe.amount as f32) / actual_cost * Self::CRAFTING_FACTOR,
|
||||
true,
|
||||
);
|
||||
false
|
||||
@ -363,22 +397,31 @@ impl TradePricing {
|
||||
Good::Food,
|
||||
Good::Ingredients,
|
||||
];
|
||||
for &g in good_list.iter() {
|
||||
sort_and_normalize(result.get_list_mut(g), get_scaling(&contents, g));
|
||||
|
||||
for good in &good_list {
|
||||
sort_and_normalize(
|
||||
result.get_list_mut(*good),
|
||||
get_scaling(&price_config, *good),
|
||||
);
|
||||
let mut materials = result
|
||||
.get_list(g)
|
||||
.get_list(*good)
|
||||
.iter()
|
||||
.map(|i| (i.0.clone(), (g, 1.0 / i.1)))
|
||||
.map(|i| (i.0.clone(), (*good, 1.0 / i.1)))
|
||||
.collect::<Vec<_>>();
|
||||
result.material_cache.extend(materials.drain(..));
|
||||
}
|
||||
result.coin_scale = get_scaling(&contents, Good::Coin);
|
||||
result.coin_scale = get_scaling(&price_config, Good::Coin);
|
||||
result
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss
|
||||
)]
|
||||
fn random_item_impl(&self, good: Good, amount: f32, selling: bool) -> Option<String> {
|
||||
if good == Good::Coin {
|
||||
Some(TradePricing::COIN_ITEM.into())
|
||||
Some(Self::COIN_ITEM.into())
|
||||
} else {
|
||||
let table = self.get_list(good);
|
||||
if table.is_empty() {
|
||||
@ -390,8 +433,7 @@ impl TradePricing {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|i| i.1.1 * amount >= 1.0)
|
||||
.map(|i| i.0)
|
||||
.unwrap_or(upper - 1);
|
||||
.map_or(upper - 1, |i| i.0);
|
||||
loop {
|
||||
let index =
|
||||
(rand::random::<f32>() * ((upper - lower) as f32)).floor() as usize + lower;
|
||||
@ -403,21 +445,23 @@ impl TradePricing {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn random_item(good: Good, amount: f32, selling: bool) -> Option<String> {
|
||||
TRADE_PRICING.random_item_impl(good, amount, selling)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_material(item: &str) -> (Good, f32) {
|
||||
if item == TradePricing::COIN_ITEM {
|
||||
if item == Self::COIN_ITEM {
|
||||
(Good::Coin, 1.0)
|
||||
} else {
|
||||
let item = TRADE_PRICING
|
||||
.equality_set
|
||||
.equivalence_class
|
||||
.get(item)
|
||||
.map(|i| &**i)
|
||||
.unwrap_or(item);
|
||||
TRADE_PRICING.material_cache.get(item).cloned().map_or(
|
||||
.map_or(item, |i| &**i);
|
||||
|
||||
TRADE_PRICING.material_cache.get(item).copied().map_or(
|
||||
(Good::Terrain(crate::terrain::BiomeKind::Void), 0.0),
|
||||
|(a, b)| (a, b * TRADE_PRICING.coin_scale),
|
||||
)
|
||||
@ -436,61 +480,87 @@ impl TradePricing {
|
||||
where
|
||||
F: Fn(&Item, f32) -> String,
|
||||
{
|
||||
println!("{}", x);
|
||||
println!("\n======{:^15}======", x);
|
||||
for i in e.iter() {
|
||||
let it = Item::new_from_asset_expect(&i.0);
|
||||
let price = 1.0 / i.1;
|
||||
println!("{} {:.2} {:?} {}", i.0, price, it.quality, f(&it, i.1));
|
||||
println!(
|
||||
"<{}>\n{:>4.2} {:?} {}",
|
||||
i.0,
|
||||
price,
|
||||
it.quality,
|
||||
f(&it, i.1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
printvec("Armor", &self.armor, |i, p| match &i.kind {
|
||||
ItemKind::Armor(a) => match a.protection() {
|
||||
armor::Protection::Invincible => "Invincible".into(),
|
||||
armor::Protection::Normal(x) => format!("{:.4} prot/val", x * p),
|
||||
},
|
||||
_ => format!("{:?}", i.kind),
|
||||
printvec("Armor", &self.armor.entries, |i, p| {
|
||||
if let ItemKind::Armor(a) = &i.kind {
|
||||
match a.protection() {
|
||||
armor::Protection::Invincible => "Invincible".into(),
|
||||
armor::Protection::Normal(x) => format!("{:.4} prot/val", x * p),
|
||||
}
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
});
|
||||
printvec("Tools", &self.tools, |i, p| match &i.kind {
|
||||
ItemKind::Tool(t) => match &t.stats {
|
||||
tool::StatKind::Direct(d) => {
|
||||
format!("{:.4} dps/val", d.power * d.speed * p)
|
||||
},
|
||||
tool::StatKind::Modular => "Modular".into(),
|
||||
},
|
||||
_ => format!("{:?}", i.kind),
|
||||
});
|
||||
printvec("Potions", &self.potions, |i, p| match &i.kind {
|
||||
ItemKind::Consumable { kind: _, effect } => effect
|
||||
.iter()
|
||||
.map(|e| match e {
|
||||
crate::effect::Effect::Buff(b) => {
|
||||
format!("{:.2} str/val", b.data.strength * p)
|
||||
printvec("Tools", &self.tools.entries, |i, p| {
|
||||
if let ItemKind::Tool(t) = &i.kind {
|
||||
match &t.stats {
|
||||
tool::StatKind::Direct(d) => {
|
||||
format!("{:.4} dps/val", d.power * d.speed * p)
|
||||
},
|
||||
_ => format!("{:?}", e),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
_ => format!("{:?}", i.kind),
|
||||
tool::StatKind::Modular => "Modular".into(),
|
||||
}
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
});
|
||||
printvec("Food", &self.food, |i, p| match &i.kind {
|
||||
ItemKind::Consumable { kind: _, effect } => effect
|
||||
.iter()
|
||||
.map(|e| match e {
|
||||
crate::effect::Effect::Buff(b) => {
|
||||
format!("{:.2} str/val", b.data.strength * p)
|
||||
},
|
||||
_ => format!("{:?}", e),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" "),
|
||||
_ => format!("{:?}", i.kind),
|
||||
printvec("Potions", &self.potions.entries, |i, p| {
|
||||
if let ItemKind::Consumable { kind: _, effect } = &i.kind {
|
||||
effect
|
||||
.iter()
|
||||
.map(|e| {
|
||||
if let crate::effect::Effect::Buff(b) = e {
|
||||
format!("{:.2} str/val", b.data.strength * p)
|
||||
} else {
|
||||
format!("{:?}", e)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
});
|
||||
printvec("Ingredients", &self.ingredients, |i, _p| match &i.kind {
|
||||
ItemKind::Ingredient { kind } => kind.clone(),
|
||||
_ => format!("{:?}", i.kind),
|
||||
printvec("Food", &self.food.entries, |i, p| {
|
||||
if let ItemKind::Consumable { kind: _, effect } = &i.kind {
|
||||
effect
|
||||
.iter()
|
||||
.map(|e| {
|
||||
if let crate::effect::Effect::Buff(b) = e {
|
||||
format!("{:.2} str/val", b.data.strength * p)
|
||||
} else {
|
||||
format!("{:?}", e)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
});
|
||||
println!("{} {}", TradePricing::COIN_ITEM, self.coin_scale);
|
||||
printvec("Ingredients", &self.ingredients.entries, |i, _p| {
|
||||
if let ItemKind::Ingredient { kind } = &i.kind {
|
||||
kind.clone()
|
||||
} else {
|
||||
format!("{:?}", i.kind)
|
||||
}
|
||||
});
|
||||
printvec("Other", &self.other.entries, |i, _p| {
|
||||
format!("{:?}", i.kind)
|
||||
});
|
||||
println!("<{}>\n{}", Self::COIN_ITEM, self.coin_scale);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,8 @@ pub struct StaticData {
|
||||
pub energy_drain: f32,
|
||||
/// Used to dictate how orientation functions in this state
|
||||
pub orientation_behavior: OrientationBehavior,
|
||||
/// How fast enemy can rotate with beam
|
||||
pub ori_rate: f32,
|
||||
/// What key is used to press ability
|
||||
pub ability_info: AbilityInfo,
|
||||
/// Used to specify the beam to the frontend
|
||||
@ -61,14 +63,10 @@ impl CharacterBehavior for Data {
|
||||
fn behavior(&self, data: &JoinData) -> StateUpdate {
|
||||
let mut update = StateUpdate::from(data);
|
||||
|
||||
let ori_rate = match self.static_data.orientation_behavior {
|
||||
OrientationBehavior::Normal => 0.6,
|
||||
OrientationBehavior::Turret => {
|
||||
update.ori = Ori::from(data.inputs.look_dir);
|
||||
0.6
|
||||
},
|
||||
OrientationBehavior::FromOri => 0.1,
|
||||
};
|
||||
let ori_rate = self.static_data.ori_rate;
|
||||
if self.static_data.orientation_behavior == OrientationBehavior::Turret {
|
||||
update.ori = Ori::from(data.inputs.look_dir);
|
||||
}
|
||||
|
||||
handle_orientation(data, &mut update, ori_rate);
|
||||
handle_move(data, &mut update, 0.4);
|
||||
|
@ -225,15 +225,24 @@ impl Block {
|
||||
/// arbitrary and only important when compared to one-another.
|
||||
#[inline]
|
||||
pub fn explode_power(&self) -> Option<f32> {
|
||||
// Explodable means that the terrain sprite will get removed anyway,
|
||||
// so all is good for empty fluids.
|
||||
match self.kind() {
|
||||
BlockKind::Leaves => Some(0.25),
|
||||
BlockKind::Grass => Some(0.5),
|
||||
BlockKind::WeakRock => Some(0.75),
|
||||
BlockKind::Snow => Some(0.1),
|
||||
// Explodable means that the terrain sprite will get removed anyway, so all is good for
|
||||
// empty fluids.
|
||||
// TODO: Handle the case of terrain sprites we don't want to have explode
|
||||
_ => self.get_sprite().map(|_| 0.25),
|
||||
_ => self.get_sprite().and_then(|sprite| match sprite {
|
||||
SpriteKind::Anvil
|
||||
| SpriteKind::Cauldron
|
||||
| SpriteKind::CookingPot
|
||||
| SpriteKind::CraftingBench
|
||||
| SpriteKind::Forge
|
||||
| SpriteKind::Loom
|
||||
| SpriteKind::SpinningWheel
|
||||
| SpriteKind::TanningRack => None,
|
||||
_ => Some(0.25),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::{client::Client, Server};
|
||||
use common::trade::Good;
|
||||
use common_net::msg::{world_msg::EconomyInfo, ServerGeneral};
|
||||
use specs::{Entity as EcsEntity, WorldExt};
|
||||
use std::collections::HashMap;
|
||||
@ -32,25 +33,30 @@ pub fn handle_site_info(server: &Server, entity: EcsEntity, id: u64) {
|
||||
EconomyInfo {
|
||||
id,
|
||||
population: site.economy.pop.floor() as u32,
|
||||
stock: site.economy.stocks.iter().map(|(g, a)| (g, *a)).collect(),
|
||||
stock: site
|
||||
.economy
|
||||
.stocks
|
||||
.iter()
|
||||
.map(|(g, a)| (Good::from(g), *a))
|
||||
.collect(),
|
||||
labor_values: site
|
||||
.economy
|
||||
.labor_values
|
||||
.iter()
|
||||
.filter_map(|(g, a)| a.map(|a| (g, a)))
|
||||
.filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
|
||||
.collect(),
|
||||
values: site
|
||||
.economy
|
||||
.values
|
||||
.iter()
|
||||
.filter_map(|(g, a)| a.map(|a| (g, a)))
|
||||
.filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
|
||||
.collect(),
|
||||
labors: site.economy.labors.iter().map(|(_, a)| (*a)).collect(),
|
||||
last_exports: site
|
||||
.economy
|
||||
.last_exports
|
||||
.iter()
|
||||
.map(|(g, a)| (g, *a))
|
||||
.map(|(g, a)| (Good::from(g), *a))
|
||||
.collect(),
|
||||
resources: site
|
||||
.economy
|
||||
@ -59,7 +65,7 @@ pub fn handle_site_info(server: &Server, entity: EcsEntity, id: u64) {
|
||||
.iter()
|
||||
.map(|(g, a)| {
|
||||
(
|
||||
g,
|
||||
Good::from(g),
|
||||
((*a) as f32) * site.economy.natural_resources.average_yield_per_chunk[g],
|
||||
)
|
||||
})
|
||||
|
@ -3477,6 +3477,9 @@ impl<'a> AgentData<'a> {
|
||||
const GOLEM_LONG_RANGE: f32 = 50.0;
|
||||
const GOLEM_TARGET_SPEED: f32 = 8.0;
|
||||
let golem_melee_range = self.body.map_or(0.0, |b| b.radius()) + GOLEM_MELEE_RANGE;
|
||||
// Fraction of health, used for activation of shockwave
|
||||
// If golem don't have health for some reason, assume it's full
|
||||
let health_fraction = self.health.map_or(1.0, |h| h.fraction());
|
||||
// Magnitude squared of cross product of target velocity with golem orientation
|
||||
let target_speed_cross_sqd = agent
|
||||
.target
|
||||
@ -3502,7 +3505,7 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
}
|
||||
} else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) {
|
||||
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(10))
|
||||
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
|
||||
|| target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
|
||||
&& can_see_tgt(
|
||||
&*read_data.terrain,
|
||||
@ -3512,13 +3515,14 @@ impl<'a> AgentData<'a> {
|
||||
)
|
||||
&& attack_data.angle < 45.0
|
||||
{
|
||||
// If target in range threshold and haven't been lasering for more than 10
|
||||
// If target in range threshold and haven't been lasering for more than 5
|
||||
// seconds already or if target is moving slow-ish, laser them
|
||||
controller
|
||||
.actions
|
||||
.push(ControlAction::basic_input(InputKind::Secondary));
|
||||
} else {
|
||||
// Else target moving too fast for laser, shockwave time
|
||||
} else if health_fraction < 0.7 {
|
||||
// Else target moving too fast for laser, shockwave time.
|
||||
// But only if damaged enough
|
||||
controller
|
||||
.actions
|
||||
.push(ControlAction::basic_input(InputKind::Ability(0)));
|
||||
@ -3536,8 +3540,9 @@ impl<'a> AgentData<'a> {
|
||||
controller
|
||||
.actions
|
||||
.push(ControlAction::basic_input(InputKind::Ability(1)));
|
||||
} else {
|
||||
// Else target moving too fast for laser, shockwave time
|
||||
} else if health_fraction < 0.7 {
|
||||
// Else target moving too fast for laser, shockwave time.
|
||||
// But only if damaged enough
|
||||
controller
|
||||
.actions
|
||||
.push(ControlAction::basic_input(InputKind::Ability(0)));
|
||||
|
@ -135,7 +135,7 @@ impl PlayState for MainMenuState {
|
||||
},
|
||||
Some(InitMsg::Done(Err(e))) => {
|
||||
self.init = InitState::None;
|
||||
tracing::trace!(?e, "raw Client Init error");
|
||||
error!(?e, "Client Init failed raw error");
|
||||
let e = get_client_msg_error(e, &global_state.i18n);
|
||||
// Log error for possible additional use later or incase that the error
|
||||
// displayed is cut of.
|
||||
@ -339,21 +339,26 @@ impl PlayState for MainMenuState {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_client_msg_error(e: client_init::Error, localized_strings: &LocalizationHandle) -> String {
|
||||
fn get_client_msg_error(
|
||||
error: client_init::Error,
|
||||
localized_strings: &LocalizationHandle,
|
||||
) -> String {
|
||||
let localization = localized_strings.read();
|
||||
|
||||
// When a network error is received and there is a mismatch between the client
|
||||
// and server version it is almost definitely due to this mismatch rather than
|
||||
// a true networking error.
|
||||
let net_e = |error: String, mismatched_server_info: Option<ServerInfo>| -> String {
|
||||
let net_error = |error: String, mismatched_server_info: Option<ServerInfo>| -> String {
|
||||
if let Some(server_info) = mismatched_server_info {
|
||||
format!(
|
||||
"{} {}: {} {}: {}",
|
||||
"{} {}: {} ({}) {}: {} ({})",
|
||||
localization.get("main.login.network_wrong_version"),
|
||||
localization.get("main.login.client_version"),
|
||||
common::util::GIT_HASH.to_string(),
|
||||
&*common::util::GIT_HASH,
|
||||
&*common::util::GIT_DATE,
|
||||
localization.get("main.login.server_version"),
|
||||
server_info.git_hash
|
||||
server_info.git_hash,
|
||||
server_info.git_date,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
@ -365,7 +370,7 @@ fn get_client_msg_error(e: client_init::Error, localized_strings: &LocalizationH
|
||||
};
|
||||
|
||||
use client::Error;
|
||||
match e {
|
||||
match error {
|
||||
InitError::ClientError {
|
||||
error,
|
||||
mismatched_server_info,
|
||||
@ -392,15 +397,15 @@ fn get_client_msg_error(e: client_init::Error, localized_strings: &LocalizationH
|
||||
Error::InvalidCharacter => localization.get("main.login.invalid_character").into(),
|
||||
Error::NetworkErr(NetworkError::ConnectFailed(NetworkConnectError::Handshake(
|
||||
InitProtocolError::WrongVersion(_),
|
||||
))) => net_e(
|
||||
))) => net_error(
|
||||
localization
|
||||
.get("main.login.network_wrong_version")
|
||||
.to_owned(),
|
||||
mismatched_server_info,
|
||||
),
|
||||
Error::NetworkErr(e) => net_e(e.to_string(), mismatched_server_info),
|
||||
Error::ParticipantErr(e) => net_e(e.to_string(), mismatched_server_info),
|
||||
Error::StreamErr(e) => net_e(e.to_string(), mismatched_server_info),
|
||||
Error::NetworkErr(e) => net_error(e.to_string(), mismatched_server_info),
|
||||
Error::ParticipantErr(e) => net_error(e.to_string(), mismatched_server_info),
|
||||
Error::StreamErr(e) => net_error(e.to_string(), mismatched_server_info),
|
||||
Error::HostnameLookupFailed(e) => {
|
||||
format!("{}: {}", localization.get("main.login.server_not_found"), e)
|
||||
},
|
||||
@ -422,9 +427,7 @@ fn get_client_msg_error(e: client_init::Error, localized_strings: &LocalizationH
|
||||
client::AuthClientError::InsecureSchema => {
|
||||
localization.get("main.login.insecure_auth_scheme").into()
|
||||
},
|
||||
client::AuthClientError::ServerError(_, e) => {
|
||||
String::from_utf8_lossy(&e).to_string()
|
||||
},
|
||||
client::AuthClientError::ServerError(_, e) => String::from_utf8_lossy(&e).into(),
|
||||
},
|
||||
Error::AuthServerUrlInvalid(e) => {
|
||||
format!(
|
||||
|
45
world/economy_testinput2.ron
Normal file
45
world/economy_testinput2.ron
Normal file
@ -0,0 +1,45 @@
|
||||
[
|
||||
(
|
||||
name: "Forest Settlement",
|
||||
position: (1, 1),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(1, 10),
|
||||
(2, 10),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Forest),
|
||||
amount: 1000,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Moutain Peak",
|
||||
position: (10, 10),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(0, 10),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Mountain),
|
||||
amount: 1000,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Farmer Village",
|
||||
position: (20, 10),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(0, 10),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Grassland),
|
||||
amount: 1000,
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
@ -21,13 +21,13 @@ fn main() -> Result<(), std::io::Error> {
|
||||
|
||||
let mut f = std::fs::File::create("economy.gv")?;
|
||||
writeln!(f, "digraph economy {{")?;
|
||||
for i in good_list().iter() {
|
||||
let color = if economy::direct_use_goods().contains(i) {
|
||||
for i in good_list() {
|
||||
let color = if economy::direct_use_goods().contains(&i) {
|
||||
"green"
|
||||
} else {
|
||||
"orange"
|
||||
};
|
||||
writeln!(f, "{:?} [color=\"{}\"];", good_name(*i), color)?; // shape doubleoctagon ?
|
||||
writeln!(f, "{:?} [color=\"{}\"];", good_name(i.into()), color)?; // shape doubleoctagon ?
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
@ -42,9 +42,9 @@ fn main() -> Result<(), std::io::Error> {
|
||||
for i in o.iter() {
|
||||
for j in i.1.iter() {
|
||||
if i.0.is_some() {
|
||||
let style = if matches!(j.0, Good::Tools)
|
||||
|| matches!(j.0, Good::Armor)
|
||||
|| matches!(j.0, Good::Potions)
|
||||
let style = if matches!(j.0.into(), Good::Tools)
|
||||
|| matches!(j.0.into(), Good::Armor)
|
||||
|| matches!(j.0.into(), Good::Potions)
|
||||
{
|
||||
", style=dashed, color=orange"
|
||||
} else {
|
||||
@ -53,7 +53,7 @@ fn main() -> Result<(), std::io::Error> {
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> {:?} [label=\"{:.1}\"{}];",
|
||||
good_name(j.0),
|
||||
good_name(j.0.into()),
|
||||
labor_name(i.0.unwrap()),
|
||||
j.1,
|
||||
style
|
||||
@ -62,7 +62,7 @@ fn main() -> Result<(), std::io::Error> {
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> Everyone [label=\"{:.1}\"];",
|
||||
good_name(j.0),
|
||||
good_name(j.0.into()),
|
||||
j.1
|
||||
)?;
|
||||
}
|
||||
@ -72,15 +72,13 @@ fn main() -> Result<(), std::io::Error> {
|
||||
writeln!(f)?;
|
||||
writeln!(f, "// Products")?;
|
||||
for i in p.iter() {
|
||||
for j in i.1.iter() {
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> {:?} [label=\"{:.1}\"];",
|
||||
labor_name(i.0),
|
||||
good_name(j.0),
|
||||
j.1
|
||||
)?;
|
||||
}
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> {:?} [label=\"{:.1}\"];",
|
||||
labor_name(i.0),
|
||||
good_name(i.1.0.into()),
|
||||
i.1.1
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(f, "}}")?;
|
||||
|
@ -2,12 +2,12 @@ use crate::{
|
||||
sim::WorldSim,
|
||||
site::{
|
||||
economy::{
|
||||
decay_rate, direct_use_goods, good_list, transportation_effort, Economy, Labor,
|
||||
TradeDelivery, TradeOrder,
|
||||
decay_rate, direct_use_goods, good_list, transportation_effort, Economy, GoodIndex,
|
||||
GoodMap, LaborIndex, LaborMap, TradeDelivery, TradeOrder,
|
||||
},
|
||||
Site, SiteKind,
|
||||
},
|
||||
util::{DHashMap, DHashSet, MapVec},
|
||||
util::{DHashMap, DHashSet},
|
||||
Index,
|
||||
};
|
||||
use common::{
|
||||
@ -17,7 +17,8 @@ use common::{
|
||||
Good::{Coin, Transportation},
|
||||
},
|
||||
};
|
||||
use std::cmp::Ordering::Less;
|
||||
use lazy_static::lazy_static;
|
||||
use std::{cmp::Ordering::Less, convert::TryInto};
|
||||
use tracing::{debug, info};
|
||||
|
||||
const MONTH: f32 = 30.0;
|
||||
@ -28,6 +29,32 @@ const HISTORY_DAYS: f32 = 500.0 * YEAR; // 500 years
|
||||
const GENERATE_CSV: bool = false;
|
||||
const INTER_SITE_TRADE: bool = true;
|
||||
|
||||
// this is an empty replacement for https://github.com/cpetig/vergleich
|
||||
// which can be used to compare values acros runs
|
||||
mod vergleich {
|
||||
pub struct Error {}
|
||||
impl Error {
|
||||
pub fn to_string(&self) -> &'static str { "" }
|
||||
}
|
||||
pub struct ProgramRun {}
|
||||
impl ProgramRun {
|
||||
pub fn new(_: &str) -> Result<Self, Error> { Ok(Self {}) }
|
||||
|
||||
pub fn set_epsilon(&mut self, _: f32) {}
|
||||
|
||||
pub fn context(&mut self, _: &str) -> Context { Context {} }
|
||||
|
||||
//pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
|
||||
}
|
||||
pub struct Context {}
|
||||
impl Context {
|
||||
pub fn context(&mut self, _: &str) -> Context { Context {} }
|
||||
|
||||
pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics collector (min, max, avg)
|
||||
#[derive(Debug)]
|
||||
struct EconStatistics {
|
||||
pub count: u32,
|
||||
@ -41,12 +68,16 @@ impl Default for EconStatistics {
|
||||
Self {
|
||||
count: 0,
|
||||
sum: 0.0,
|
||||
min: 1e30,
|
||||
max: 0.0,
|
||||
min: f32::INFINITY,
|
||||
max: -f32::INFINITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::AddAssign<f32> for EconStatistics {
|
||||
fn add_assign(&mut self, rhs: f32) { self.collect(rhs); }
|
||||
}
|
||||
|
||||
impl EconStatistics {
|
||||
fn collect(&mut self, value: f32) {
|
||||
self.count += 1;
|
||||
@ -58,6 +89,8 @@ impl EconStatistics {
|
||||
self.min = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn valid(&self) -> bool { self.min.is_finite() }
|
||||
}
|
||||
|
||||
pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
|
||||
@ -71,24 +104,24 @@ pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Erro
|
||||
site.economy.pop
|
||||
)?;
|
||||
for g in good_list() {
|
||||
write!(*f, "{:?},", site.economy.values[*g].unwrap_or(-1.0))?;
|
||||
write!(*f, "{:?},", site.economy.values[g].unwrap_or(-1.0))?;
|
||||
}
|
||||
for g in good_list() {
|
||||
write!(f, "{:?},", site.economy.labor_values[*g].unwrap_or(-1.0))?;
|
||||
write!(f, "{:?},", site.economy.labor_values[g].unwrap_or(-1.0))?;
|
||||
}
|
||||
for g in good_list() {
|
||||
write!(f, "{:?},", site.economy.stocks[*g])?;
|
||||
write!(f, "{:?},", site.economy.stocks[g])?;
|
||||
}
|
||||
for g in good_list() {
|
||||
write!(f, "{:?},", site.economy.marginal_surplus[*g])?;
|
||||
write!(f, "{:?},", site.economy.marginal_surplus[g])?;
|
||||
}
|
||||
for l in Labor::list() {
|
||||
for l in LaborIndex::list() {
|
||||
write!(f, "{:?},", site.economy.labors[l] * site.economy.pop)?;
|
||||
}
|
||||
for l in Labor::list() {
|
||||
for l in LaborIndex::list() {
|
||||
write!(f, "{:?},", site.economy.productivity[l])?;
|
||||
}
|
||||
for l in Labor::list() {
|
||||
for l in LaborIndex::list() {
|
||||
write!(f, "{:?},", site.economy.yields[l])?;
|
||||
}
|
||||
writeln!(f)
|
||||
@ -113,13 +146,13 @@ fn simulate_return(index: &mut Index, world: &mut WorldSim) -> Result<(), std::i
|
||||
for g in good_list() {
|
||||
write!(f, "{:?} Surplus,", g)?;
|
||||
}
|
||||
for l in Labor::list() {
|
||||
for l in LaborIndex::list() {
|
||||
write!(f, "{:?} Labor,", l)?;
|
||||
}
|
||||
for l in Labor::list() {
|
||||
for l in LaborIndex::list() {
|
||||
write!(f, "{:?} Productivity,", l)?;
|
||||
}
|
||||
for l in Labor::list() {
|
||||
for l in LaborIndex::list() {
|
||||
write!(f, "{:?} Yields,", l)?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
@ -129,12 +162,15 @@ fn simulate_return(index: &mut Index, world: &mut WorldSim) -> Result<(), std::i
|
||||
};
|
||||
|
||||
tracing::info!("economy simulation start");
|
||||
let mut vr = vergleich::ProgramRun::new("economy_compare.sqlite")
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||
vr.set_epsilon(0.1);
|
||||
for i in 0..(HISTORY_DAYS / TICK_PERIOD) as i32 {
|
||||
if (index.time / YEAR) as i32 % 50 == 0 && (index.time % YEAR) as i32 == 0 {
|
||||
debug!("Year {}", (index.time / YEAR) as i32);
|
||||
}
|
||||
|
||||
tick(index, world, TICK_PERIOD);
|
||||
tick(index, world, TICK_PERIOD, vr.context(&i.to_string()));
|
||||
|
||||
if let Some(f) = f.as_mut() {
|
||||
if i % 5 == 0 {
|
||||
@ -165,33 +201,40 @@ fn simulate_return(index: &mut Index, world: &mut WorldSim) -> Result<(), std::i
|
||||
for site in index.sites.ids() {
|
||||
let site = &index.sites[site];
|
||||
match site.kind {
|
||||
SiteKind::Dungeon(_) => dungeons.collect(site.economy.pop),
|
||||
SiteKind::Settlement(_) => towns.collect(site.economy.pop),
|
||||
SiteKind::Castle(_) => castles.collect(site.economy.pop),
|
||||
SiteKind::Dungeon(_) => dungeons += site.economy.pop,
|
||||
SiteKind::Settlement(_) => towns += site.economy.pop,
|
||||
SiteKind::Castle(_) => castles += site.economy.pop,
|
||||
SiteKind::Tree(_) => (),
|
||||
SiteKind::Refactor(_) => (),
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"Towns {:.0}-{:.0} avg {:.0} inhabitants",
|
||||
towns.min,
|
||||
towns.max,
|
||||
towns.sum / (towns.count as f32)
|
||||
);
|
||||
info!(
|
||||
"Castles {:.0}-{:.0} avg {:.0}",
|
||||
castles.min,
|
||||
castles.max,
|
||||
castles.sum / (castles.count as f32)
|
||||
);
|
||||
info!(
|
||||
"Dungeons {:.0}-{:.0} avg {:.0}",
|
||||
dungeons.min,
|
||||
dungeons.max,
|
||||
dungeons.sum / (dungeons.count as f32)
|
||||
);
|
||||
if towns.valid() {
|
||||
info!(
|
||||
"Towns {:.0}-{:.0} avg {:.0} inhabitants",
|
||||
towns.min,
|
||||
towns.max,
|
||||
towns.sum / (towns.count as f32)
|
||||
);
|
||||
}
|
||||
if castles.valid() {
|
||||
info!(
|
||||
"Castles {:.0}-{:.0} avg {:.0}",
|
||||
castles.min,
|
||||
castles.max,
|
||||
castles.sum / (castles.count as f32)
|
||||
);
|
||||
}
|
||||
if dungeons.valid() {
|
||||
info!(
|
||||
"Dungeons {:.0}-{:.0} avg {:.0}",
|
||||
dungeons.min,
|
||||
dungeons.max,
|
||||
dungeons.sum / (dungeons.count as f32)
|
||||
);
|
||||
}
|
||||
check_money(index);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -203,12 +246,12 @@ pub fn simulate(index: &mut Index, world: &mut WorldSim) {
|
||||
fn check_money(index: &mut Index) {
|
||||
let mut sum_stock: f32 = 0.0;
|
||||
for site in index.sites.values() {
|
||||
sum_stock += site.economy.stocks[Coin];
|
||||
sum_stock += site.economy.stocks[*COIN_INDEX];
|
||||
}
|
||||
let mut sum_del: f32 = 0.0;
|
||||
for v in index.trade.deliveries.values() {
|
||||
for del in v.iter() {
|
||||
sum_del += del.amount[Coin];
|
||||
sum_del += del.amount[*COIN_INDEX];
|
||||
}
|
||||
}
|
||||
info!(
|
||||
@ -219,15 +262,16 @@ fn check_money(index: &mut Index) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32) {
|
||||
pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32, mut vc: vergleich::Context) {
|
||||
let site_ids = index.sites.ids().collect::<Vec<_>>();
|
||||
for site in site_ids {
|
||||
tick_site_economy(index, site, dt);
|
||||
tick_site_economy(index, site, dt, vc.context(&site.id().to_string()));
|
||||
}
|
||||
if INTER_SITE_TRADE {
|
||||
for (&site, orders) in index.trade.orders.iter_mut() {
|
||||
let siteinfo = index.sites.get_mut(site);
|
||||
if siteinfo.do_economic_simulation() {
|
||||
// let name: String = siteinfo.name().into();
|
||||
trade_at_site(
|
||||
site,
|
||||
orders,
|
||||
@ -242,6 +286,12 @@ pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32) {
|
||||
index.time += dt;
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref COIN_INDEX: GoodIndex = Coin.try_into().unwrap_or_default();
|
||||
static ref FOOD_INDEX: GoodIndex = Good::Food.try_into().unwrap_or_default();
|
||||
static ref TRANSPORTATION_INDEX: GoodIndex = Transportation.try_into().unwrap_or_default();
|
||||
}
|
||||
|
||||
/// plan the trading according to missing goods and prices at neighboring sites
|
||||
/// (1st step of trading)
|
||||
// returns wares spent (-) and procured (+)
|
||||
@ -251,8 +301,8 @@ fn plan_trade_for_site(
|
||||
site_id: &Id<Site>,
|
||||
transportation_capacity: f32,
|
||||
external_orders: &mut DHashMap<Id<Site>, Vec<TradeOrder>>,
|
||||
potential_trade: &mut MapVec<Good, f32>,
|
||||
) -> MapVec<Good, f32> {
|
||||
potential_trade: &mut GoodMap<f32>,
|
||||
) -> GoodMap<f32> {
|
||||
// TODO: Do we have some latency of information here (using last years
|
||||
// capacity?)
|
||||
//let total_transport_capacity = site.economy.stocks[Transportation];
|
||||
@ -264,14 +314,14 @@ fn plan_trade_for_site(
|
||||
let mut collect_capacity = transportation_capacity;
|
||||
let mut missing_dispatch: f32 = 0.0;
|
||||
let mut missing_collect: f32 = 0.0;
|
||||
let mut result = MapVec::from_default(0.0);
|
||||
let mut result = GoodMap::default();
|
||||
const MIN_SELL_PRICE: f32 = 1.0;
|
||||
// value+amount per good
|
||||
let mut missing_goods: Vec<(Good, (f32, f32))> = site
|
||||
let mut missing_goods: Vec<(GoodIndex, (f32, f32))> = site
|
||||
.economy
|
||||
.surplus
|
||||
.iter()
|
||||
.filter(|(g, a)| (**a < 0.0 && *g != Transportation))
|
||||
.filter(|(g, a)| (**a < 0.0 && *g != *TRANSPORTATION_INDEX))
|
||||
.map(|(g, a)| {
|
||||
(
|
||||
g,
|
||||
@ -283,17 +333,20 @@ fn plan_trade_for_site(
|
||||
})
|
||||
.collect();
|
||||
missing_goods.sort_by(|a, b| b.1.0.partial_cmp(&a.1.0).unwrap_or(Less));
|
||||
let mut extra_goods: MapVec<Good, f32> = MapVec::from_iter(
|
||||
let mut extra_goods: GoodMap<f32> = GoodMap::from_iter(
|
||||
site.economy
|
||||
.surplus
|
||||
.iter()
|
||||
.chain(core::iter::once((Coin, &site.economy.stocks[Coin])))
|
||||
.filter(|(g, a)| (**a > 0.0 && *g != Transportation))
|
||||
.chain(core::iter::once((
|
||||
*COIN_INDEX,
|
||||
&site.economy.stocks[*COIN_INDEX],
|
||||
)))
|
||||
.filter(|(g, a)| (**a > 0.0 && *g != *TRANSPORTATION_INDEX))
|
||||
.map(|(g, a)| (g, *a)),
|
||||
0.0,
|
||||
);
|
||||
// ratio+price per good and site
|
||||
type GoodRatioPrice = Vec<(Good, (f32, f32))>;
|
||||
type GoodRatioPrice = Vec<(GoodIndex, (f32, f32))>;
|
||||
let good_payment: DHashMap<Id<Site>, GoodRatioPrice> = site
|
||||
.economy
|
||||
.neighbors
|
||||
@ -322,7 +375,7 @@ fn plan_trade_for_site(
|
||||
.collect();
|
||||
// price+stock per site and good
|
||||
type SitePriceStock = Vec<(Id<Site>, (f32, f32))>;
|
||||
let mut good_price: DHashMap<Good, SitePriceStock> = missing_goods
|
||||
let mut good_price: DHashMap<GoodIndex, SitePriceStock> = missing_goods
|
||||
.iter()
|
||||
.map(|(g, _)| {
|
||||
(*g, {
|
||||
@ -340,11 +393,11 @@ fn plan_trade_for_site(
|
||||
.collect();
|
||||
// TODO: we need to introduce priority (according to available transportation
|
||||
// capacity)
|
||||
let mut neighbor_orders: DHashMap<Id<Site>, MapVec<Good, f32>> = site
|
||||
let mut neighbor_orders: DHashMap<Id<Site>, GoodMap<f32>> = site
|
||||
.economy
|
||||
.neighbors
|
||||
.iter()
|
||||
.map(|n| (n.id, MapVec::default()))
|
||||
.map(|n| (n.id, GoodMap::default()))
|
||||
.collect();
|
||||
if site_id.id() == 1 {
|
||||
// cut down number of lines printed
|
||||
@ -450,7 +503,7 @@ fn plan_trade_for_site(
|
||||
}
|
||||
let to = TradeOrder {
|
||||
customer: *site_id,
|
||||
amount: orders.clone(),
|
||||
amount: *orders,
|
||||
};
|
||||
if let Some(o) = external_orders.get_mut(&n.id) {
|
||||
// this is just to catch unbound growth (happened in development)
|
||||
@ -474,7 +527,8 @@ fn plan_trade_for_site(
|
||||
missing_collect,
|
||||
missing_dispatch,
|
||||
);
|
||||
result[Transportation] = -(transportation_capacity - collect_capacity.min(dispatch_capacity)
|
||||
result[*TRANSPORTATION_INDEX] = -(transportation_capacity
|
||||
- collect_capacity.min(dispatch_capacity)
|
||||
+ missing_collect.max(missing_dispatch));
|
||||
if site_id.id() == 1 {
|
||||
debug!("Trade {:?}", result);
|
||||
@ -493,7 +547,7 @@ fn trade_at_site(
|
||||
// TODO: rework using economy.unconsumed_stock
|
||||
|
||||
let internal_orders = economy.get_orders();
|
||||
let mut next_demand = MapVec::from_default(0.0);
|
||||
let mut next_demand = GoodMap::from_default(0.0);
|
||||
for (labor, orders) in &internal_orders {
|
||||
let workers = if let Some(labor) = labor {
|
||||
economy.labors[*labor]
|
||||
@ -506,13 +560,13 @@ fn trade_at_site(
|
||||
}
|
||||
}
|
||||
//info!("Trade {} {}", site.id(), orders.len());
|
||||
let mut total_orders: MapVec<Good, f32> = MapVec::from_default(0.0);
|
||||
let mut total_orders: GoodMap<f32> = GoodMap::from_default(0.0);
|
||||
for i in orders.iter() {
|
||||
for (g, &a) in i.amount.iter().filter(|(_, a)| **a > 0.0) {
|
||||
total_orders[g] += a;
|
||||
}
|
||||
}
|
||||
let order_stock_ratio: MapVec<Good, Option<f32>> = MapVec::from_iter(
|
||||
let order_stock_ratio: GoodMap<Option<f32>> = GoodMap::from_iter(
|
||||
economy
|
||||
.stocks
|
||||
.iter()
|
||||
@ -522,7 +576,7 @@ fn trade_at_site(
|
||||
None,
|
||||
);
|
||||
debug!("trade {} {:?}", site.id(), order_stock_ratio);
|
||||
let prices = MapVec::from_iter(
|
||||
let prices = GoodMap::from_iter(
|
||||
economy
|
||||
.values
|
||||
.iter()
|
||||
@ -532,14 +586,14 @@ fn trade_at_site(
|
||||
for o in orders.drain(..) {
|
||||
// amount, local value (sell low value, buy high value goods first (trading
|
||||
// town's interest))
|
||||
let mut sorted_sell: Vec<(Good, f32, f32)> = o
|
||||
let mut sorted_sell: Vec<(GoodIndex, f32, f32)> = o
|
||||
.amount
|
||||
.iter()
|
||||
.filter(|(_, &a)| a > 0.0)
|
||||
.map(|(g, a)| (g, *a, prices[g]))
|
||||
.collect();
|
||||
sorted_sell.sort_by(|a, b| (a.2.partial_cmp(&b.2).unwrap_or(Less)));
|
||||
let mut sorted_buy: Vec<(Good, f32, f32)> = o
|
||||
let mut sorted_buy: Vec<(GoodIndex, f32, f32)> = o
|
||||
.amount
|
||||
.iter()
|
||||
.filter(|(_, &a)| a < 0.0)
|
||||
@ -552,7 +606,7 @@ fn trade_at_site(
|
||||
sorted_sell,
|
||||
sorted_buy
|
||||
);
|
||||
let mut good_delivery = MapVec::from_default(0.0);
|
||||
let mut good_delivery = GoodMap::from_default(0.0);
|
||||
for (g, amount, price) in sorted_sell.iter() {
|
||||
if let Some(order_stock_ratio) = order_stock_ratio[*g] {
|
||||
let allocated_amount = *amount / order_stock_ratio.max(1.0);
|
||||
@ -605,8 +659,8 @@ fn trade_at_site(
|
||||
}
|
||||
let delivery = TradeDelivery {
|
||||
supplier: site,
|
||||
prices: prices.clone(),
|
||||
supply: MapVec::from_iter(
|
||||
prices,
|
||||
supply: GoodMap::from_iter(
|
||||
economy.stocks.iter().map(|(g, a)| {
|
||||
(g, {
|
||||
(a - next_demand[g] - total_orders[g]).max(0.0) + good_delivery[g]
|
||||
@ -630,9 +684,13 @@ fn trade_at_site(
|
||||
}
|
||||
|
||||
/// 3rd step of trading
|
||||
fn collect_deliveries(site: &mut Site, deliveries: &mut Vec<TradeDelivery>) {
|
||||
fn collect_deliveries(
|
||||
site: &mut Site,
|
||||
deliveries: &mut Vec<TradeDelivery>,
|
||||
ctx: &mut vergleich::Context,
|
||||
) {
|
||||
// collect all the goods we shipped
|
||||
let mut last_exports = MapVec::from_iter(
|
||||
let mut last_exports = GoodMap::from_iter(
|
||||
site.economy
|
||||
.active_exports
|
||||
.iter()
|
||||
@ -642,8 +700,9 @@ fn collect_deliveries(site: &mut Site, deliveries: &mut Vec<TradeDelivery>) {
|
||||
);
|
||||
// TODO: properly rate benefits created by merchants (done below?)
|
||||
for mut d in deliveries.drain(..) {
|
||||
let mut ictx = ctx.context(&format!("suppl {}", d.supplier.id()));
|
||||
for i in d.amount.iter() {
|
||||
last_exports[i.0] -= *i.1;
|
||||
last_exports[i.0] -= ictx.value(&format!("{:?}", i.0), *i.1);
|
||||
}
|
||||
// remember price
|
||||
if let Some(n) = site
|
||||
@ -691,7 +750,12 @@ fn collect_deliveries(site: &mut Site, deliveries: &mut Vec<TradeDelivery>) {
|
||||
/// dynamically react to environmental changes. If a product becomes available
|
||||
/// through a mechanism such as trade, an entire arm of the economy may
|
||||
/// materialise to take advantage of this.
|
||||
pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
pub fn tick_site_economy(
|
||||
index: &mut Index,
|
||||
site_id: Id<Site>,
|
||||
dt: f32,
|
||||
mut vc: vergleich::Context,
|
||||
) {
|
||||
let site = &mut index.sites[site_id];
|
||||
if !site.do_economic_simulation() {
|
||||
return;
|
||||
@ -701,14 +765,19 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
if INTER_SITE_TRADE {
|
||||
let deliveries = index.trade.deliveries.get_mut(&site_id);
|
||||
if let Some(deliveries) = deliveries {
|
||||
collect_deliveries(site, deliveries);
|
||||
collect_deliveries(site, deliveries, &mut vc);
|
||||
}
|
||||
}
|
||||
|
||||
let orders = site.economy.get_orders();
|
||||
let productivity = site.economy.get_productivity();
|
||||
|
||||
let mut demand = MapVec::from_default(0.0);
|
||||
for i in productivity.iter() {
|
||||
vc.context("productivity")
|
||||
.value(&std::format!("{:?}{:?}", i.0, Good::from(i.1.0)), i.1.1);
|
||||
}
|
||||
|
||||
let mut demand = GoodMap::from_default(0.0);
|
||||
for (labor, orders) in &orders {
|
||||
let workers = if let Some(labor) = labor {
|
||||
site.economy.labors[*labor]
|
||||
@ -719,33 +788,49 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
demand[*good] += *amount * workers;
|
||||
}
|
||||
}
|
||||
if INTER_SITE_TRADE {
|
||||
demand[*COIN_INDEX] += Economy::STARTING_COIN; // if we spend coin value increases
|
||||
}
|
||||
|
||||
// which labor is the merchant
|
||||
let merchant_labor = productivity
|
||||
.iter()
|
||||
.find(|(_, v)| (**v).iter().any(|(g, _)| *g == Transportation))
|
||||
.find(|(_, v)| v.0 == *TRANSPORTATION_INDEX)
|
||||
.map(|(l, _)| l);
|
||||
|
||||
let mut supply = site.economy.stocks.clone(); //MapVec::from_default(0.0);
|
||||
let mut supply = site.economy.stocks; //GoodMap::from_default(0.0);
|
||||
for (labor, goodvec) in productivity.iter() {
|
||||
for (output_good, _) in goodvec.iter() {
|
||||
supply[*output_good] +=
|
||||
site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop;
|
||||
}
|
||||
//for (output_good, _) in goodvec.iter() {
|
||||
//info!("{} supply{:?}+={}", site_id.id(), Good::from(goodvec.0),
|
||||
// site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop);
|
||||
supply[goodvec.0] +=
|
||||
site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop;
|
||||
vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0), labor))
|
||||
.value("yields", site.economy.yields[labor]);
|
||||
vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0), labor))
|
||||
.value("labors", site.economy.labors[labor]);
|
||||
//}
|
||||
}
|
||||
|
||||
for i in supply.iter() {
|
||||
vc.context("supply")
|
||||
.value(&std::format!("{:?}", Good::from(i.0)), *i.1);
|
||||
}
|
||||
|
||||
let stocks = &site.economy.stocks;
|
||||
site.economy.surplus = demand
|
||||
.clone()
|
||||
.map(|g, demand| supply[g] + stocks[g] - demand);
|
||||
site.economy.marginal_surplus = demand.clone().map(|g, demand| supply[g] - demand);
|
||||
for i in stocks.iter() {
|
||||
vc.context("stocks")
|
||||
.value(&std::format!("{:?}", Good::from(i.0)), *i.1);
|
||||
}
|
||||
site.economy.surplus = demand.map(|g, demand| supply[g] + stocks[g] - demand);
|
||||
site.economy.marginal_surplus = demand.map(|g, demand| supply[g] - demand);
|
||||
|
||||
// plan trading with other sites
|
||||
let mut external_orders = &mut index.trade.orders;
|
||||
let mut potential_trade = MapVec::from_default(0.0);
|
||||
let mut potential_trade = GoodMap::from_default(0.0);
|
||||
// use last year's generated transportation for merchants (could we do better?
|
||||
// this is in line with the other professions)
|
||||
let transportation_capacity = site.economy.stocks[Transportation];
|
||||
let transportation_capacity = site.economy.stocks[*TRANSPORTATION_INDEX];
|
||||
let trade = if INTER_SITE_TRADE {
|
||||
let trade = plan_trade_for_site(
|
||||
site,
|
||||
@ -754,10 +839,12 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
&mut external_orders,
|
||||
&mut potential_trade,
|
||||
);
|
||||
site.economy.active_exports = MapVec::from_iter(trade.iter().map(|(g, a)| (g, -*a)), 0.0); // TODO: check for availability?
|
||||
site.economy.active_exports = GoodMap::from_iter(trade.iter().map(|(g, a)| (g, -*a)), 0.0); // TODO: check for availability?
|
||||
|
||||
// add the wares to sell to demand and the goods to buy to supply
|
||||
for (g, a) in trade.iter() {
|
||||
vc.context("trade")
|
||||
.value(&std::format!("{:?}", Good::from(g)), *a);
|
||||
if *a > 0.0 {
|
||||
supply[g] += *a;
|
||||
assert!(supply[g] >= 0.0);
|
||||
@ -766,72 +853,87 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
assert!(demand[g] >= 0.0);
|
||||
}
|
||||
}
|
||||
demand[Coin] += Economy::STARTING_COIN; // if we spend coin value increases
|
||||
trade
|
||||
} else {
|
||||
MapVec::default()
|
||||
GoodMap::default()
|
||||
};
|
||||
|
||||
// Update values according to the surplus of each stock
|
||||
// Note that values are used for workforce allocation and are not the same thing
|
||||
// as price
|
||||
// fall back to old (less wrong than other goods) coin logic
|
||||
let old_coin_surplus = site.economy.stocks[*COIN_INDEX] - demand[*COIN_INDEX];
|
||||
let values = &mut site.economy.values;
|
||||
site.economy
|
||||
.surplus
|
||||
.iter()
|
||||
.chain(std::iter::once((
|
||||
Coin,
|
||||
&(site.economy.stocks[Coin] - demand[Coin]),
|
||||
)))
|
||||
.for_each(|(good, surplus)| {
|
||||
// Value rationalisation
|
||||
let val = 2.0f32.powf(1.0 - *surplus / demand[good]);
|
||||
let smooth = 0.8;
|
||||
values[good] = if val > 0.001 && val < 1000.0 {
|
||||
Some(smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
|
||||
let all_trade_goods: DHashSet<Good> = trade
|
||||
site.economy.surplus.iter().for_each(|(good, surplus)| {
|
||||
let old_surplus = if good == *COIN_INDEX {
|
||||
old_coin_surplus
|
||||
} else {
|
||||
*surplus
|
||||
};
|
||||
// Value rationalisation
|
||||
let goodname = std::format!("{:?}", Good::from(good));
|
||||
vc.context("old_surplus").value(&goodname, old_surplus);
|
||||
vc.context("demand").value(&goodname, demand[good]);
|
||||
let val = 2.0f32.powf(1.0 - old_surplus / demand[good]);
|
||||
let smooth = 0.8;
|
||||
values[good] = if val > 0.001 && val < 1000.0 {
|
||||
Some(vc.context("values").value(
|
||||
&goodname,
|
||||
smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
|
||||
let all_trade_goods: DHashSet<GoodIndex> = trade
|
||||
.iter()
|
||||
.filter(|(_, a)| **a > 0.0)
|
||||
.chain(potential_trade.iter())
|
||||
.filter(|(_, a)| **a > 0.0)
|
||||
.map(|(g, _)| g)
|
||||
.collect();
|
||||
let empty_goods: DHashSet<Good> = DHashSet::default();
|
||||
//let empty_goods: DHashSet<GoodIndex> = DHashSet::default();
|
||||
// TODO: Does avg/max/sum make most sense for labors creating more than one good
|
||||
// summing favors merchants too much (as they will provide multiple
|
||||
// goods, so we use max instead)
|
||||
let labor_ratios: MapVec<Labor, f32> = productivity.clone().map(|labor, goodvec| {
|
||||
let trade_boost = if Some(labor) == merchant_labor {
|
||||
all_trade_goods.iter()
|
||||
} else {
|
||||
empty_goods.iter()
|
||||
};
|
||||
goodvec
|
||||
.iter()
|
||||
.map(|(g, _)| g)
|
||||
.chain(trade_boost)
|
||||
.map(|output_good| site.economy.values[*output_good].unwrap_or(0.0))
|
||||
.max_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap_or(Less))
|
||||
.unwrap_or(0.0)
|
||||
* site.economy.productivity[labor]
|
||||
});
|
||||
let labor_ratios: LaborMap<f32> = LaborMap::from_iter(
|
||||
productivity.iter().map(|(labor, goodvec)| {
|
||||
(
|
||||
labor,
|
||||
if Some(labor) == merchant_labor {
|
||||
all_trade_goods
|
||||
.iter()
|
||||
.chain(std::iter::once(&goodvec.0))
|
||||
.map(|&output_good| site.economy.values[output_good].unwrap_or(0.0))
|
||||
.max_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap_or(Less))
|
||||
} else {
|
||||
site.economy.values[goodvec.0]
|
||||
}
|
||||
.unwrap_or(0.0)
|
||||
* site.economy.productivity[labor],
|
||||
)
|
||||
}),
|
||||
0.0,
|
||||
);
|
||||
debug!(?labor_ratios);
|
||||
|
||||
let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::<f32>().max(0.01);
|
||||
let mut labor_context = vc.context("labor");
|
||||
productivity.iter().for_each(|(labor, _)| {
|
||||
let smooth = 0.8;
|
||||
site.economy.labors[labor] = smooth * site.economy.labors[labor]
|
||||
+ (1.0 - smooth)
|
||||
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum);
|
||||
site.economy.labors[labor] = labor_context.value(
|
||||
&format!("{:?}", labor),
|
||||
smooth * site.economy.labors[labor]
|
||||
+ (1.0 - smooth)
|
||||
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum),
|
||||
);
|
||||
assert!(site.economy.labors[labor] >= 0.0);
|
||||
});
|
||||
|
||||
// Production
|
||||
let stocks_before = site.economy.stocks.clone();
|
||||
let stocks_before = site.economy.stocks;
|
||||
// TODO: Should we recalculate demand after labor reassignment?
|
||||
|
||||
let direct_use = direct_use_goods();
|
||||
// Handle the stocks you can't pile (decay)
|
||||
@ -839,9 +941,9 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
site.economy.stocks[*g] = 0.0;
|
||||
}
|
||||
|
||||
let mut total_labor_values = MapVec::<_, f32>::default();
|
||||
let mut total_labor_values = GoodMap::<f32>::default();
|
||||
// TODO: trade
|
||||
let mut total_outputs = MapVec::<_, f32>::default();
|
||||
let mut total_outputs = GoodMap::<f32>::default();
|
||||
for (labor, orders) in orders.iter() {
|
||||
let workers = if let Some(labor) = labor {
|
||||
site.economy.labors[*labor]
|
||||
@ -886,7 +988,7 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
site.economy.stocks[*good] = (site.economy.stocks[*good] - used).max(0.0);
|
||||
}
|
||||
}
|
||||
let mut produced_goods: MapVec<Good, f32> = MapVec::from_default(0.0);
|
||||
let mut produced_goods: GoodMap<f32> = GoodMap::from_default(0.0);
|
||||
if INTER_SITE_TRADE && is_merchant {
|
||||
// TODO: replan for missing merchant productivity???
|
||||
for (g, a) in trade.iter() {
|
||||
@ -944,16 +1046,14 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
//let workers = site.economy.labors[*labor] * site.economy.pop;
|
||||
//let final_rate = rate;
|
||||
//let yield_per_worker = labor_productivity;
|
||||
site.economy.yields[*labor] =
|
||||
labor_productivity * work_products.iter().map(|(_, r)| r).sum::<f32>();
|
||||
site.economy.yields[*labor] = labor_productivity * work_products.1;
|
||||
site.economy.productivity[*labor] = labor_productivity;
|
||||
//let total_product_rate: f32 = work_products.iter().map(|(_, r)| *r).sum();
|
||||
for (stock, rate) in work_products {
|
||||
let total_output = labor_productivity * *rate * workers;
|
||||
assert!(total_output >= 0.0);
|
||||
site.economy.stocks[*stock] += total_output;
|
||||
produced_goods[*stock] += total_output;
|
||||
}
|
||||
let (stock, rate) = work_products;
|
||||
let total_output = labor_productivity * *rate * workers;
|
||||
assert!(total_output >= 0.0);
|
||||
site.economy.stocks[*stock] += total_output;
|
||||
produced_goods[*stock] += total_output;
|
||||
|
||||
let produced_amount: f32 = produced_goods.iter().map(|(_, a)| *a).sum();
|
||||
for (stock, amount) in produced_goods.iter() {
|
||||
@ -997,17 +1097,20 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
// Births/deaths
|
||||
const NATURAL_BIRTH_RATE: f32 = 0.05;
|
||||
const DEATH_RATE: f32 = 0.005;
|
||||
let birth_rate = if site.economy.surplus[Good::Food] > 0.0 {
|
||||
let birth_rate = if site.economy.surplus[*FOOD_INDEX] > 0.0 {
|
||||
NATURAL_BIRTH_RATE
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
site.economy.pop += dt / YEAR * site.economy.pop * (birth_rate - DEATH_RATE);
|
||||
site.economy.pop += vc.value(
|
||||
"pop",
|
||||
dt / YEAR * site.economy.pop * (birth_rate - DEATH_RATE),
|
||||
);
|
||||
|
||||
// calculate the new unclaimed stock
|
||||
//let next_orders = site.economy.get_orders();
|
||||
// orders are static
|
||||
let mut next_demand = MapVec::from_default(0.0);
|
||||
let mut next_demand = GoodMap::from_default(0.0);
|
||||
for (labor, orders) in orders.iter() {
|
||||
let workers = if let Some(labor) = labor {
|
||||
site.economy.labors[*labor]
|
||||
@ -1019,25 +1122,26 @@ pub fn tick_site_economy(index: &mut Index, site_id: Id<Site>, dt: f32) {
|
||||
assert!(next_demand[*good] >= 0.0);
|
||||
}
|
||||
}
|
||||
site.economy.unconsumed_stock = MapVec::from_iter(
|
||||
site.economy
|
||||
.stocks
|
||||
.iter()
|
||||
.map(|(g, a)| (g, *a - next_demand[g])),
|
||||
let mut us = vc.context("unconsumed");
|
||||
site.economy.unconsumed_stock = GoodMap::from_iter(
|
||||
site.economy.stocks.iter().map(|(g, a)| {
|
||||
(
|
||||
g,
|
||||
us.value(&format!("{:?}", Good::from(g)), *a - next_demand[g]),
|
||||
)
|
||||
}),
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
sim,
|
||||
util::{seed_expan, MapVec},
|
||||
};
|
||||
use crate::{sim, site::economy::GoodMap, util::seed_expan};
|
||||
use common::trade::Good;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use tracing::{info, Level};
|
||||
use tracing_subscriber::{
|
||||
filter::{EnvFilter, LevelFilter},
|
||||
@ -1095,7 +1199,7 @@ mod tests {
|
||||
.chunks_per_resource
|
||||
.iter()
|
||||
.map(|(good, a)| ResourcesSetup {
|
||||
good,
|
||||
good: good.into(),
|
||||
amount: *a * i.economy.natural_resources.average_yield_per_chunk[good],
|
||||
})
|
||||
.collect();
|
||||
@ -1131,10 +1235,10 @@ mod tests {
|
||||
}
|
||||
} else {
|
||||
let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
|
||||
let ron_file = std::fs::File::open("economy_testinput.ron")
|
||||
.expect("economy_testinput.ron not found");
|
||||
let ron_file = std::fs::File::open("economy_testinput2.ron")
|
||||
.expect("economy_testinput2.ron not found");
|
||||
let econ_testinput: Vec<EconomySetup> =
|
||||
ron::de::from_reader(ron_file).expect("economy_testinput.ron parse error");
|
||||
ron::de::from_reader(ron_file).expect("economy_testinput2.ron parse error");
|
||||
for i in econ_testinput.iter() {
|
||||
let wpos = Vec2 {
|
||||
x: i.position.0,
|
||||
@ -1159,8 +1263,10 @@ mod tests {
|
||||
//let c = sim::SimChunk::new();
|
||||
//settlement.economy.add_chunk(ch, distance_squared)
|
||||
// bypass the API for now
|
||||
settlement.economy.natural_resources.chunks_per_resource[g.good] = g.amount;
|
||||
settlement.economy.natural_resources.average_yield_per_chunk[g.good] = 1.0;
|
||||
settlement.economy.natural_resources.chunks_per_resource
|
||||
[g.good.try_into().unwrap_or_default()] = g.amount;
|
||||
settlement.economy.natural_resources.average_yield_per_chunk
|
||||
[g.good.try_into().unwrap_or_default()] = 1.0;
|
||||
}
|
||||
index.sites.insert(settlement);
|
||||
}
|
||||
@ -1176,8 +1282,8 @@ mod tests {
|
||||
.map(|(nid, dist)| crate::site::economy::NeighborInformation {
|
||||
id: nid,
|
||||
travel_distance: *dist,
|
||||
last_values: MapVec::from_default(0.0),
|
||||
last_supplies: MapVec::from_default(0.0),
|
||||
last_values: GoodMap::from_default(0.0),
|
||||
last_supplies: GoodMap::from_default(0.0),
|
||||
})
|
||||
.collect();
|
||||
index
|
||||
|
@ -9,7 +9,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use common::{
|
||||
assets::AssetHandle,
|
||||
assets::{self, AssetExt, AssetHandle},
|
||||
astar::Astar,
|
||||
comp::{self},
|
||||
generation::{ChunkSupplement, EntityInfo},
|
||||
@ -20,7 +20,7 @@ use common::{
|
||||
use core::{f32, hash::BuildHasherDefault};
|
||||
use fxhash::FxHasher64;
|
||||
use lazy_static::lazy_static;
|
||||
use rand::prelude::*;
|
||||
use rand::{prelude::*, seq::SliceRandom};
|
||||
use serde::Deserialize;
|
||||
use vek::*;
|
||||
|
||||
@ -47,13 +47,39 @@ pub struct Colors {
|
||||
|
||||
const ALT_OFFSET: i32 = -2;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DungeonDistribution(Vec<(u32, f32)>);
|
||||
impl assets::Asset for DungeonDistribution {
|
||||
type Loader = assets::RonLoader;
|
||||
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref DUNGEON_DISTRIBUTION: Vec<(u32, f32)> =
|
||||
DungeonDistribution::load_expect("world.dungeon.difficulty_distribution")
|
||||
.read()
|
||||
.0
|
||||
.clone();
|
||||
}
|
||||
|
||||
impl Dungeon {
|
||||
#[allow(clippy::let_and_return)] // TODO: Pending review in #587
|
||||
pub fn generate(wpos: Vec2<i32>, sim: Option<&WorldSim>, rng: &mut impl Rng) -> Self {
|
||||
let mut ctx = GenCtx { sim, rng };
|
||||
let difficulty = ctx.rng.gen_range(0..6);
|
||||
|
||||
let difficulty = DUNGEON_DISTRIBUTION
|
||||
.choose_weighted(&mut ctx.rng, |pair| pair.1)
|
||||
.map(|(difficulty, _)| *difficulty)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Failed to choose difficulty (check instruction in config). Error: {}",
|
||||
err
|
||||
)
|
||||
});
|
||||
|
||||
let floors = 3 + difficulty / 2;
|
||||
let this = Self {
|
||||
|
||||
Self {
|
||||
name: {
|
||||
let name = NameGen::location(ctx.rng).generate();
|
||||
match ctx.rng.gen_range(0..5) {
|
||||
@ -81,9 +107,7 @@ impl Dungeon {
|
||||
})
|
||||
.collect(),
|
||||
difficulty,
|
||||
};
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str { &self.name }
|
||||
@ -541,7 +565,7 @@ impl Floor {
|
||||
.map(|density| dynamic_rng.gen_range(0..density.recip() as usize) == 0)
|
||||
.unwrap_or(false)
|
||||
&& !tile_is_pillar
|
||||
&& !(room.boss && room.difficulty == 5)
|
||||
&& !room.boss
|
||||
{
|
||||
// Randomly displace them a little
|
||||
let raw_entity = EntityInfo::at(
|
||||
|
@ -1,8 +1,11 @@
|
||||
use crate::{
|
||||
assets::{self, AssetExt, AssetHandle},
|
||||
assets::{self, AssetExt},
|
||||
sim::SimChunk,
|
||||
site::Site,
|
||||
util::{DHashMap, MapVec},
|
||||
util::{
|
||||
map_array::{enum_from_index, index_from_enum, GenericIndex, NotFound},
|
||||
DHashMap,
|
||||
},
|
||||
};
|
||||
use common::{
|
||||
store::Id,
|
||||
@ -11,347 +14,24 @@ use common::{
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, marker::PhantomData, sync::Once};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt::{self, Write},
|
||||
marker::PhantomData,
|
||||
ops::{Index, IndexMut},
|
||||
};
|
||||
|
||||
use Good::*;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Profession {
|
||||
pub name: String,
|
||||
pub orders: Vec<(Good, f32)>,
|
||||
pub products: Vec<(Good, f32)>,
|
||||
// the opaque index type into the "map" of Goods
|
||||
#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GoodIndex {
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
// 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, f32>,
|
||||
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, f32>,
|
||||
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: MapVec::from_list(&[(Coin, Economy::STARTING_COIN)], 100.0),
|
||||
surplus: Default::default(),
|
||||
marginal_surplus: 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: 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.001 {
|
||||
self.natural_resources.chunks_per_resource[g] = chunks;
|
||||
self.natural_resources.average_yield_per_chunk[g] = amount / chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_orders(&self) -> DHashMap<Option<Labor>, Vec<(Good, f32)>> {
|
||||
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, 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) {
|
||||
for (good, &ch) in self.natural_resources.chunks_per_resource.iter() {
|
||||
let per_year = self.natural_resources.average_yield_per_chunk[good] * ch;
|
||||
self.stocks[good] = self.stocks[good].max(per_year);
|
||||
}
|
||||
// info!("resources {:?}", self.stocks);
|
||||
}
|
||||
|
||||
pub fn add_chunk(&mut self, ch: &SimChunk, distance_squared: i64) {
|
||||
// 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.0; TODO: Scale resources by
|
||||
// rockiness or tree_density?
|
||||
|
||||
let mut add_biome = |biome, amount| {
|
||||
self.natural_resources.per_area[distance_bin].resource_sum[Terrain(biome)] += amount;
|
||||
self.natural_resources.per_area[distance_bin].resource_chunks[Terrain(biome)] += amount;
|
||||
};
|
||||
if ch.river.is_ocean() {
|
||||
add_biome(BiomeKind::Ocean, 1.0);
|
||||
} else if ch.river.is_lake() {
|
||||
add_biome(BiomeKind::Lake, 1.0);
|
||||
} else {
|
||||
add_biome(BiomeKind::Forest, 0.5 + ch.tree_density);
|
||||
add_biome(BiomeKind::Grassland, 0.5 + ch.humidity);
|
||||
add_biome(BiomeKind::Jungle, 0.5 + ch.humidity * ch.temp.max(0.0));
|
||||
add_biome(BiomeKind::Mountain, 0.5 + (ch.alt / 4000.0).max(0.0));
|
||||
add_biome(
|
||||
BiomeKind::Desert,
|
||||
0.5 + (1.0 - ch.humidity) * ch.temp.max(0.0),
|
||||
);
|
||||
add_biome(BiomeKind::Snowland, 0.5 + (-ch.temp).max(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let normalize = |xs: MapVec<Good, Option<f32>>| {
|
||||
let sum = xs
|
||||
.iter()
|
||||
.map(|(_, x)| (*x).unwrap_or(0.0))
|
||||
.sum::<f32>()
|
||||
.max(0.001);
|
||||
xs.map(|_, x| Some(x? / sum))
|
||||
};
|
||||
|
||||
SitePrices {
|
||||
values: {
|
||||
let labor_values = normalize(self.labor_values.clone());
|
||||
// Use labor values as prices. Not correct (doesn't care about exchange value)
|
||||
let prices = normalize(self.values.clone()).map(|good, value| {
|
||||
(labor_values[good].unwrap_or(Economy::MINIMUM_PRICE)
|
||||
+ value.unwrap_or(Economy::MINIMUM_PRICE))
|
||||
* 0.5
|
||||
});
|
||||
prices.iter().map(|(g, v)| (g, *v)).collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn good_list() -> &'static [Good] {
|
||||
static GOODS: [Good; 23] = [
|
||||
impl GenericIndex<Good, 23> for GoodIndex {
|
||||
// static list of all Goods traded
|
||||
const VALUES: [Good; GoodIndex::LENGTH] = [
|
||||
// controlled resources
|
||||
Territory(BiomeKind::Grassland),
|
||||
Territory(BiomeKind::Forest),
|
||||
@ -381,11 +61,563 @@ pub fn good_list() -> &'static [Good] {
|
||||
Terrain(BiomeKind::Ocean),
|
||||
];
|
||||
|
||||
&GOODS
|
||||
fn from_usize(idx: usize) -> Self { Self { idx } }
|
||||
|
||||
fn into_usize(self) -> usize { self.idx }
|
||||
}
|
||||
|
||||
pub fn transportation_effort(g: Good) -> f32 {
|
||||
match g {
|
||||
impl TryFrom<Good> for GoodIndex {
|
||||
type Error = NotFound;
|
||||
|
||||
fn try_from(e: Good) -> Result<Self, NotFound> { index_from_enum(e) }
|
||||
}
|
||||
|
||||
impl From<GoodIndex> for Good {
|
||||
fn from(gi: GoodIndex) -> Good { enum_from_index(gi) }
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GoodIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
GoodIndex::VALUES[self.idx].fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
// the "map" itself
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct GoodMap<V> {
|
||||
data: [V; GoodIndex::LENGTH],
|
||||
}
|
||||
|
||||
impl<V: Default + Copy> Default for GoodMap<V> {
|
||||
fn default() -> Self {
|
||||
GoodMap {
|
||||
data: [V::default(); GoodIndex::LENGTH],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Default + Copy + PartialEq + fmt::Debug> fmt::Debug for GoodMap<V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("{ ")?;
|
||||
for i in self.iter() {
|
||||
if *i.1 != V::default() {
|
||||
Good::from(i.0).fmt(f)?;
|
||||
f.write_char(':')?;
|
||||
i.1.fmt(f)?;
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
}
|
||||
f.write_char('}')
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Index<GoodIndex> for GoodMap<V> {
|
||||
type Output = V;
|
||||
|
||||
fn index(&self, index: GoodIndex) -> &Self::Output { &self.data[index.idx] }
|
||||
}
|
||||
|
||||
impl<V> IndexMut<GoodIndex> for GoodMap<V> {
|
||||
fn index_mut(&mut self, index: GoodIndex) -> &mut Self::Output { &mut self.data[index.idx] }
|
||||
}
|
||||
|
||||
impl<V> GoodMap<V> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (GoodIndex, &V)> + '_ {
|
||||
(&self.data)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, v)| (GoodIndex { idx }, v))
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (GoodIndex, &mut V)> + '_ {
|
||||
(&mut self.data)
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.map(|(idx, v)| (GoodIndex { idx }, v))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Copy> GoodMap<V> {
|
||||
pub fn from_default(default: V) -> Self {
|
||||
GoodMap {
|
||||
data: [default; GoodIndex::LENGTH],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_iter(i: impl Iterator<Item = (GoodIndex, V)>, default: V) -> Self {
|
||||
let mut result = Self::from_default(default);
|
||||
for j in i {
|
||||
result.data[j.0.idx] = j.1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn map<U: Default + Copy>(self, mut f: impl FnMut(GoodIndex, V) -> U) -> GoodMap<U> {
|
||||
let mut result = GoodMap::<U>::from_default(U::default());
|
||||
for j in self.data.iter().enumerate() {
|
||||
result.data[j.0] = f(GoodIndex::from_usize(j.0), *j.1);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (GoodIndex, V)>, default: V) -> Self
|
||||
where
|
||||
V: 'a,
|
||||
{
|
||||
let mut result = Self::from_default(default);
|
||||
for j in i {
|
||||
result.data[j.0.idx] = j.1;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct RawProfession {
|
||||
pub name: String,
|
||||
pub orders: Vec<(Good, f32)>,
|
||||
pub products: Vec<(Good, f32)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Profession {
|
||||
pub name: String,
|
||||
pub orders: Vec<(GoodIndex, f32)>,
|
||||
pub products: (GoodIndex, f32),
|
||||
}
|
||||
|
||||
// reference to profession
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct Labor(u8, PhantomData<Profession>);
|
||||
|
||||
// the opaque index type into the "map" of Labors (as Labor already contains a
|
||||
// monotonous index we reuse it)
|
||||
pub type LaborIndex = Labor;
|
||||
|
||||
impl LaborIndex {
|
||||
fn from_usize(idx: usize) -> Self { Self(idx as u8, PhantomData) }
|
||||
|
||||
fn into_usize(self) -> usize { self.0 as usize }
|
||||
}
|
||||
|
||||
// the "map" itself
|
||||
#[derive(Clone)]
|
||||
pub struct LaborMap<V> {
|
||||
data: Vec<V>,
|
||||
}
|
||||
|
||||
impl<V: Default + Copy> Default for LaborMap<V> {
|
||||
fn default() -> Self {
|
||||
LaborMap {
|
||||
data: std::iter::repeat(V::default()).take(*LABOR_COUNT).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Default + Copy + PartialEq + fmt::Debug> fmt::Debug for LaborMap<V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("{ ")?;
|
||||
for i in self.iter() {
|
||||
if *i.1 != V::default() {
|
||||
i.0.fmt(f)?;
|
||||
f.write_char(':')?;
|
||||
(*i.1).fmt(f)?;
|
||||
f.write_char(' ')?;
|
||||
}
|
||||
}
|
||||
f.write_char('}')
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Index<LaborIndex> for LaborMap<V> {
|
||||
type Output = V;
|
||||
|
||||
fn index(&self, index: LaborIndex) -> &Self::Output { &self.data[index.into_usize()] }
|
||||
}
|
||||
|
||||
impl<V> IndexMut<LaborIndex> for LaborMap<V> {
|
||||
fn index_mut(&mut self, index: LaborIndex) -> &mut Self::Output {
|
||||
&mut self.data[index.into_usize()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> LaborMap<V> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (LaborIndex, &V)> + '_ {
|
||||
(&self.data)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, v)| (LaborIndex::from_usize(idx), v))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Copy + Default> LaborMap<V> {
|
||||
pub fn from_default(default: V) -> Self {
|
||||
LaborMap {
|
||||
data: std::iter::repeat(default).take(*LABOR_COUNT).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Copy + Default> LaborMap<V> {
|
||||
pub fn from_iter(i: impl Iterator<Item = (LaborIndex, V)>, default: V) -> Self {
|
||||
let mut result = Self::from_default(default);
|
||||
for j in i {
|
||||
result.data[j.0.into_usize()] = j.1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn map<U: Default + Copy>(&self, f: impl Fn(LaborIndex, &V) -> U) -> LaborMap<U> {
|
||||
LaborMap {
|
||||
data: self.iter().map(|i| f(i.0, i.1)).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AreaResources {
|
||||
pub resource_sum: GoodMap<f32>,
|
||||
pub resource_chunks: GoodMap<f32>,
|
||||
pub chunks: u32,
|
||||
}
|
||||
|
||||
impl Default for AreaResources {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
resource_sum: GoodMap::default(),
|
||||
resource_chunks: GoodMap::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: GoodMap<f32>,
|
||||
pub average_yield_per_chunk: GoodMap<f32>,
|
||||
}
|
||||
|
||||
impl Default for NaturalResources {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
per_area: Vec::new(),
|
||||
chunks_per_resource: GoodMap::default(),
|
||||
average_yield_per_chunk: GoodMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RawProfessions(Vec<RawProfession>);
|
||||
|
||||
impl assets::Asset for RawProfessions {
|
||||
type Loader = assets::RonLoader;
|
||||
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
||||
|
||||
pub fn default_professions() -> Vec<Profession> {
|
||||
RawProfessions::load_expect("common.professions")
|
||||
.read()
|
||||
.0
|
||||
.iter()
|
||||
.map(|r| Profession {
|
||||
name: r.name.clone(),
|
||||
orders: r
|
||||
.orders
|
||||
.iter()
|
||||
.map(|i| (i.0.try_into().unwrap_or_default(), i.1))
|
||||
.collect(),
|
||||
products: r
|
||||
.products
|
||||
.first()
|
||||
.map(|p| (p.0.try_into().unwrap_or_default(), p.1))
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LABOR: Vec<Profession> = default_professions();
|
||||
// used to define resources needed by every person
|
||||
static ref DUMMY_LABOR: Labor = Labor(
|
||||
LABOR
|
||||
.iter()
|
||||
.position(|a| a.name == "_")
|
||||
.unwrap_or(0) as u8,
|
||||
PhantomData
|
||||
);
|
||||
// do not count the DUMMY_LABOR (has to be last entry)
|
||||
static ref LABOR_COUNT: usize = LABOR.len()-1;
|
||||
}
|
||||
|
||||
impl fmt::Debug for Labor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if (self.0 as usize) < *LABOR_COUNT {
|
||||
f.write_str(&LABOR[self.0 as usize].name)
|
||||
} else {
|
||||
f.write_str("?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Labor {
|
||||
fn default() -> Self { *DUMMY_LABOR }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TradeOrder {
|
||||
pub customer: Id<Site>,
|
||||
pub amount: GoodMap<f32>, // positive for orders, negative for exchange
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TradeDelivery {
|
||||
pub supplier: Id<Site>,
|
||||
pub amount: GoodMap<f32>, // positive for orders, negative for exchange
|
||||
pub prices: GoodMap<f32>, // at the time of interaction
|
||||
pub supply: GoodMap<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: GoodMap<f32>,
|
||||
pub last_supplies: GoodMap<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Economy {
|
||||
// Population
|
||||
pub pop: f32,
|
||||
|
||||
/// Total available amount of each good
|
||||
pub stocks: GoodMap<f32>,
|
||||
/// Surplus stock compared to demand orders
|
||||
pub surplus: GoodMap<f32>,
|
||||
/// change rate (derivative) of stock in the current situation
|
||||
pub marginal_surplus: GoodMap<f32>,
|
||||
/// amount of wares not needed by the economy (helps with trade planning)
|
||||
pub unconsumed_stock: GoodMap<f32>,
|
||||
// For some goods, such a goods without any supply, it doesn't make sense to talk about value
|
||||
pub values: GoodMap<Option<f32>>,
|
||||
pub last_exports: GoodMap<f32>,
|
||||
pub active_exports: GoodMap<f32>, // unfinished trade (amount unconfirmed)
|
||||
//pub export_targets: GoodMap<f32>,
|
||||
pub labor_values: GoodMap<Option<f32>>,
|
||||
pub material_costs: GoodMap<f32>,
|
||||
|
||||
// Proportion of individuals dedicated to an industry
|
||||
pub labors: LaborMap<f32>,
|
||||
// Per worker, per year, of their output good
|
||||
pub yields: LaborMap<f32>,
|
||||
pub productivity: LaborMap<f32>,
|
||||
|
||||
pub natural_resources: NaturalResources,
|
||||
// usize is distance
|
||||
pub neighbors: Vec<NeighborInformation>,
|
||||
}
|
||||
|
||||
impl Default for Economy {
|
||||
fn default() -> Self {
|
||||
let coin_index: GoodIndex = GoodIndex::try_from(Coin).unwrap_or_default();
|
||||
Self {
|
||||
pop: 32.0,
|
||||
|
||||
stocks: GoodMap::from_list(&[(coin_index, Economy::STARTING_COIN)], 100.0),
|
||||
surplus: Default::default(),
|
||||
marginal_surplus: Default::default(),
|
||||
values: GoodMap::from_list(&[(coin_index, Some(2.0))], None),
|
||||
last_exports: Default::default(),
|
||||
active_exports: Default::default(),
|
||||
|
||||
labor_values: Default::default(),
|
||||
material_costs: Default::default(),
|
||||
|
||||
labors: LaborMap::from_default(0.01),
|
||||
yields: LaborMap::from_default(1.0),
|
||||
productivity: LaborMap::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.001 {
|
||||
self.natural_resources.chunks_per_resource[g] = chunks;
|
||||
self.natural_resources.average_yield_per_chunk[g] = amount / chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_orders(&self) -> DHashMap<Option<LaborIndex>, Vec<(GoodIndex, f32)>> {
|
||||
LABOR
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
(
|
||||
if i == DUMMY_LABOR.0 as usize {
|
||||
None
|
||||
} else {
|
||||
Some(LaborIndex::from_usize(i))
|
||||
},
|
||||
p.orders.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_productivity(&self) -> LaborMap<(GoodIndex, f32)> {
|
||||
// cache the site independent part of production
|
||||
lazy_static! {
|
||||
static ref PRODUCTS: LaborMap<(GoodIndex, f32)> = LaborMap::from_iter(
|
||||
LABOR
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.products.1 > 0.0)
|
||||
.map(|(i, p)| { (LaborIndex::from_usize(i), p.products,) }),
|
||||
(GoodIndex::default(), 0.0),
|
||||
);
|
||||
}
|
||||
PRODUCTS.map(|l, vec| {
|
||||
//dbg!((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.0, vec.1 * scale)
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
self.stocks[good] = self.stocks[good].max(per_year);
|
||||
}
|
||||
// info!("resources {:?}", self.stocks);
|
||||
}
|
||||
|
||||
pub fn add_chunk(&mut self, ch: &SimChunk, distance_squared: i64) {
|
||||
// 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;
|
||||
|
||||
let mut add_biome = |biome, amount| {
|
||||
if let Ok(idx) = GoodIndex::try_from(Terrain(biome)) {
|
||||
self.natural_resources.per_area[distance_bin].resource_sum[idx] += amount;
|
||||
self.natural_resources.per_area[distance_bin].resource_chunks[idx] += amount;
|
||||
}
|
||||
};
|
||||
if ch.river.is_ocean() {
|
||||
add_biome(BiomeKind::Ocean, 1.0);
|
||||
} else if ch.river.is_lake() {
|
||||
add_biome(BiomeKind::Lake, 1.0);
|
||||
} else {
|
||||
add_biome(BiomeKind::Forest, 0.5 + ch.tree_density);
|
||||
add_biome(BiomeKind::Grassland, 0.5 + ch.humidity);
|
||||
add_biome(BiomeKind::Jungle, 0.5 + ch.humidity * ch.temp.max(0.0));
|
||||
add_biome(BiomeKind::Mountain, 0.5 + (ch.alt / 4000.0).max(0.0));
|
||||
add_biome(
|
||||
BiomeKind::Desert,
|
||||
0.5 + (1.0 - ch.humidity) * ch.temp.max(0.0),
|
||||
);
|
||||
add_biome(BiomeKind::Snowland, 0.5 + (-ch.temp).max(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_neighbor(&mut self, id: Id<Site>, distance: usize) {
|
||||
self.neighbors.push(NeighborInformation {
|
||||
id,
|
||||
travel_distance: distance,
|
||||
|
||||
last_values: GoodMap::from_default(Economy::MINIMUM_PRICE),
|
||||
last_supplies: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_site_prices(&self) -> SitePrices {
|
||||
let normalize = |xs: GoodMap<Option<f32>>| {
|
||||
let sum = xs
|
||||
.iter()
|
||||
.map(|(_, x)| (*x).unwrap_or(0.0))
|
||||
.sum::<f32>()
|
||||
.max(0.001);
|
||||
xs.map(|_, x| Some(x? / sum))
|
||||
};
|
||||
|
||||
SitePrices {
|
||||
values: {
|
||||
let labor_values = normalize(self.labor_values);
|
||||
// Use labor values as prices. Not correct (doesn't care about exchange value)
|
||||
let prices = normalize(self.values).map(|good, value| {
|
||||
(labor_values[good].unwrap_or(Economy::MINIMUM_PRICE)
|
||||
+ value.unwrap_or(Economy::MINIMUM_PRICE))
|
||||
* 0.5
|
||||
});
|
||||
prices.iter().map(|(g, v)| (Good::from(g), *v)).collect()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn good_list() -> impl Iterator<Item = GoodIndex> {
|
||||
(0..GoodIndex::LENGTH).map(GoodIndex::from_usize)
|
||||
}
|
||||
|
||||
// cache in GoodMap ?
|
||||
pub fn transportation_effort(g: GoodIndex) -> f32 {
|
||||
match Good::from(g) {
|
||||
Terrain(_) | Territory(_) | RoadSecurity => 0.0,
|
||||
Coin => 0.01,
|
||||
Potions => 0.1,
|
||||
@ -397,8 +629,8 @@ pub fn transportation_effort(g: Good) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decay_rate(g: Good) -> f32 {
|
||||
match g {
|
||||
pub fn decay_rate(g: GoodIndex) -> f32 {
|
||||
match Good::from(g) {
|
||||
Food => 0.2,
|
||||
Flour => 0.1,
|
||||
Meat => 0.25,
|
||||
@ -408,28 +640,30 @@ pub fn decay_rate(g: Good) -> f32 {
|
||||
}
|
||||
|
||||
/** 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
|
||||
pub fn direct_use_goods() -> &'static [GoodIndex] {
|
||||
lazy_static! {
|
||||
static ref DIRECT_USE: [GoodIndex; 13] = [
|
||||
GoodIndex::try_from(Transportation).unwrap_or_default(),
|
||||
GoodIndex::try_from(Territory(BiomeKind::Grassland)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Territory(BiomeKind::Forest)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Territory(BiomeKind::Lake)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Territory(BiomeKind::Ocean)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Territory(BiomeKind::Mountain)).unwrap_or_default(),
|
||||
GoodIndex::try_from(RoadSecurity).unwrap_or_default(),
|
||||
GoodIndex::try_from(Terrain(BiomeKind::Grassland)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Terrain(BiomeKind::Forest)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Terrain(BiomeKind::Lake)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Terrain(BiomeKind::Ocean)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Terrain(BiomeKind::Mountain)).unwrap_or_default(),
|
||||
GoodIndex::try_from(Terrain(BiomeKind::Desert)).unwrap_or_default(),
|
||||
];
|
||||
}
|
||||
&*DIRECT_USE
|
||||
}
|
||||
|
||||
impl Labor {
|
||||
pub fn list() -> impl Iterator<Item = Self> {
|
||||
(0..LABOR.read().0.len())
|
||||
(0..LABOR.len())
|
||||
.filter(|&i| i != (DUMMY_LABOR.0 as usize))
|
||||
.map(|i| Self(i as u8, PhantomData))
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ impl Site {
|
||||
.economy
|
||||
.unconsumed_stock
|
||||
.iter()
|
||||
.map(|(g, a)| (g, *a))
|
||||
.map(|(g, a)| (g.into(), *a))
|
||||
.collect(),
|
||||
};
|
||||
s.apply_supplement(dynamic_rng, wpos2d, get_column, supplement, economy)
|
||||
|
110
world/src/util/map_array.rs
Normal file
110
world/src/util/map_array.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use std::cmp::PartialEq;
|
||||
|
||||
pub trait GenericIndex<V: Clone, const N: usize> {
|
||||
const LENGTH: usize = N;
|
||||
const VALUES: [V; N];
|
||||
|
||||
fn from_usize(n: usize) -> Self;
|
||||
fn into_usize(self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotFound();
|
||||
|
||||
pub fn index_from_enum<E: Clone + PartialEq, I: GenericIndex<E, N>, const N: usize>(
|
||||
val: E,
|
||||
) -> Result<I, NotFound> {
|
||||
I::VALUES
|
||||
.iter()
|
||||
.position(|v| val == *v)
|
||||
.ok_or(NotFound {})
|
||||
.map(I::from_usize)
|
||||
}
|
||||
|
||||
pub fn enum_from_index<E: Clone, I: GenericIndex<E, N>, const N: usize>(idx: I) -> E {
|
||||
I::VALUES[idx.into_usize()].clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::util::map_array::{enum_from_index, index_from_enum, GenericIndex, NotFound};
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::{Index, IndexMut},
|
||||
};
|
||||
|
||||
// the Values we want to generate an Index for
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum MyEnum0 {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum MyEnum {
|
||||
C(MyEnum0),
|
||||
D,
|
||||
}
|
||||
|
||||
// the opaque index type into the "map"
|
||||
struct MyIndex(usize);
|
||||
|
||||
impl GenericIndex<MyEnum, 3> for MyIndex {
|
||||
const VALUES: [MyEnum; MyIndex::LENGTH] =
|
||||
[MyEnum::C(MyEnum0::B), MyEnum::C(MyEnum0::A), MyEnum::D];
|
||||
|
||||
fn from_usize(n: usize) -> Self { Self(n) }
|
||||
|
||||
fn into_usize(self) -> usize { self.0 }
|
||||
}
|
||||
|
||||
impl TryFrom<MyEnum> for MyIndex {
|
||||
type Error = NotFound;
|
||||
|
||||
fn try_from(e: MyEnum) -> Result<MyIndex, NotFound> { index_from_enum(e) }
|
||||
}
|
||||
|
||||
impl From<MyIndex> for MyEnum {
|
||||
fn from(idx: MyIndex) -> MyEnum { enum_from_index(idx) }
|
||||
}
|
||||
|
||||
// the "map" itself
|
||||
struct MyMap<V>([V; MyIndex::LENGTH]);
|
||||
|
||||
impl<V: Default + Copy> Default for MyMap<V> {
|
||||
fn default() -> Self { MyMap([V::default(); MyIndex::LENGTH]) }
|
||||
}
|
||||
|
||||
impl<V> Index<MyIndex> for MyMap<V> {
|
||||
type Output = V;
|
||||
|
||||
fn index(&self, index: MyIndex) -> &Self::Output { &self.0[index.0] }
|
||||
}
|
||||
|
||||
impl<V> IndexMut<MyIndex> for MyMap<V> {
|
||||
fn index_mut(&mut self, index: MyIndex) -> &mut Self::Output { &mut self.0[index.0] }
|
||||
}
|
||||
|
||||
impl<V> MyMap<V> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (MyIndex, &V)> + '_ {
|
||||
(&self.0).iter().enumerate().map(|(i, v)| (MyIndex(i), v))
|
||||
}
|
||||
}
|
||||
|
||||
// test: create a map, set some values and output it
|
||||
// Output: m[C(B)]=19 m[C(A)]=42 m[D]=0
|
||||
#[test]
|
||||
fn test_map_array() {
|
||||
let mut m = MyMap::default();
|
||||
if let Ok(i) = MyEnum::C(MyEnum0::A).try_into() {
|
||||
m[i] = 42.0;
|
||||
}
|
||||
if let Ok(i) = MyEnum::C(MyEnum0::B).try_into() {
|
||||
m[i] = 19.0;
|
||||
}
|
||||
for (k, v) in m.iter() {
|
||||
let k2: MyEnum = k.into();
|
||||
println!("m[{:?}]={}", k2, *v);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
pub mod fast_noise;
|
||||
pub mod map_array;
|
||||
pub mod map_vec;
|
||||
pub mod math;
|
||||
pub mod random;
|
||||
|
Loading…
Reference in New Issue
Block a user