diff --git a/CHANGELOG.md b/CHANGELOG.md index 30cc28e9cc..ee737abab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/assets/common/abilities/custom/basilisk/petrify.ron b/assets/common/abilities/custom/basilisk/petrify.ron index c8fb5b3021..b8691b25fa 100644 --- a/assets/common/abilities/custom/basilisk/petrify.ron +++ b/assets/common/abilities/custom/basilisk/petrify.ron @@ -15,5 +15,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Cultist, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/birdlargebreathe/flamethrower.ron b/assets/common/abilities/custom/birdlargebreathe/flamethrower.ron index 4bf507fa86..1cb6d4fdf8 100644 --- a/assets/common/abilities/custom/birdlargebreathe/flamethrower.ron +++ b/assets/common/abilities/custom/birdlargebreathe/flamethrower.ron @@ -10,5 +10,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Flamethrower, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/birdlargefire/flamethrower.ron b/assets/common/abilities/custom/birdlargefire/flamethrower.ron index 4bf507fa86..1cb6d4fdf8 100644 --- a/assets/common/abilities/custom/birdlargefire/flamethrower.ron +++ b/assets/common/abilities/custom/birdlargefire/flamethrower.ron @@ -10,5 +10,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Flamethrower, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/claygolem/laser.ron b/assets/common/abilities/custom/claygolem/laser.ron index 8e029678d4..c93f0f7ce8 100644 --- a/assets/common/abilities/custom/claygolem/laser.ron +++ b/assets/common/abilities/custom/claygolem/laser.ron @@ -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, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/claygolem/rocket.ron b/assets/common/abilities/custom/claygolem/rocket.ron index 852a4db918..c58011bfb1 100644 --- a/assets/common/abilities/custom/claygolem/rocket.ron +++ b/assets/common/abilities/custom/claygolem/rocket.ron @@ -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, diff --git a/assets/common/abilities/custom/claygolem/shockwave.ron b/assets/common/abilities/custom/claygolem/shockwave.ron index 5a88d9f395..ac4b9a8ae1 100644 --- a/assets/common/abilities/custom/claygolem/shockwave.ron +++ b/assets/common/abilities/custom/claygolem/shockwave.ron @@ -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, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/claygolem/strike.ron b/assets/common/abilities/custom/claygolem/strike.ron index 08e15584bc..9552e7f2bc 100644 --- a/assets/common/abilities/custom/claygolem/strike.ron +++ b/assets/common/abilities/custom/claygolem/strike.ron @@ -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, diff --git a/assets/common/abilities/custom/mindflayer/cursedflames.ron b/assets/common/abilities/custom/mindflayer/cursedflames.ron index 9d83b5a243..0cfddbd5f5 100644 --- a/assets/common/abilities/custom/mindflayer/cursedflames.ron +++ b/assets/common/abilities/custom/mindflayer/cursedflames.ron @@ -15,5 +15,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Cultist, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/quadlowbeam/healingbeam.ron b/assets/common/abilities/custom/quadlowbeam/healingbeam.ron index 6776a79944..2aba7a0d1b 100644 --- a/assets/common/abilities/custom/quadlowbeam/healingbeam.ron +++ b/assets/common/abilities/custom/quadlowbeam/healingbeam.ron @@ -10,5 +10,6 @@ BasicBeam( energy_regen: 25, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: HealingBeam, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/quadlowbreathe/flamethrower.ron b/assets/common/abilities/custom/quadlowbreathe/flamethrower.ron index ddc09e0ef6..5eba0f6226 100644 --- a/assets/common/abilities/custom/quadlowbreathe/flamethrower.ron +++ b/assets/common/abilities/custom/quadlowbreathe/flamethrower.ron @@ -10,5 +10,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Flamethrower, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/tidalwarrior/bubbles.ron b/assets/common/abilities/custom/tidalwarrior/bubbles.ron index 65cbadc67c..737e144574 100644 --- a/assets/common/abilities/custom/tidalwarrior/bubbles.ron +++ b/assets/common/abilities/custom/tidalwarrior/bubbles.ron @@ -15,5 +15,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Bubbles, ) diff --git a/assets/common/abilities/custom/turret/flamethrower.ron b/assets/common/abilities/custom/turret/flamethrower.ron index 3814d1f235..33f5dc51c9 100644 --- a/assets/common/abilities/custom/turret/flamethrower.ron +++ b/assets/common/abilities/custom/turret/flamethrower.ron @@ -15,5 +15,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.3, specifier: Flamethrower, -) \ No newline at end of file +) diff --git a/assets/common/abilities/custom/yeti/frostbreath.ron b/assets/common/abilities/custom/yeti/frostbreath.ron index 826f59bbcb..67e16b78ff 100644 --- a/assets/common/abilities/custom/yeti/frostbreath.ron +++ b/assets/common/abilities/custom/yeti/frostbreath.ron @@ -15,5 +15,6 @@ BasicBeam( energy_regen: 0, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Frost, -) \ No newline at end of file +) diff --git a/assets/common/abilities/sceptre/lifestealbeam.ron b/assets/common/abilities/sceptre/lifestealbeam.ron index 0484fef1c4..994e959b1d 100644 --- a/assets/common/abilities/sceptre/lifestealbeam.ron +++ b/assets/common/abilities/sceptre/lifestealbeam.ron @@ -10,5 +10,6 @@ BasicBeam( energy_regen: 50, energy_drain: 0, orientation_behavior: Normal, + ori_rate: 0.6, specifier: LifestealBeam -) \ No newline at end of file +) diff --git a/assets/common/abilities/staff/flamethrower.ron b/assets/common/abilities/staff/flamethrower.ron index c3c491b94a..82fa30d8f8 100644 --- a/assets/common/abilities/staff/flamethrower.ron +++ b/assets/common/abilities/staff/flamethrower.ron @@ -15,5 +15,6 @@ BasicBeam( energy_regen: 0, energy_drain: 350, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Flamethrower, -) \ No newline at end of file +) diff --git a/assets/common/abilities/staffsimple/flamethrower.ron b/assets/common/abilities/staffsimple/flamethrower.ron index 4842fdd20f..14eead7fa6 100644 --- a/assets/common/abilities/staffsimple/flamethrower.ron +++ b/assets/common/abilities/staffsimple/flamethrower.ron @@ -10,5 +10,6 @@ BasicBeam( energy_regen: 0, energy_drain: 350, orientation_behavior: Normal, + ori_rate: 0.6, specifier: Flamethrower, ) diff --git a/assets/common/cave_scatter/shallow_floor.ron b/assets/common/cave_scatter/shallow_floor.ron index f80601194a..894aa3d97b 100644 --- a/assets/common/cave_scatter/shallow_floor.ron +++ b/assets/common/cave_scatter/shallow_floor.ron @@ -3,8 +3,8 @@ (250, ShortGrass), (50, CaveMushroom), (50, Mushroom), - (30, AmethystSmall), - (15, TopazSmall), + (5, AmethystSmall), + (5, TopazSmall), (15, Tin), (12, Copper), (15, Iron), diff --git a/assets/common/item_price_calculation.ron b/assets/common/item_price_calculation.ron index 875321c988..8b4416054e 100644 --- a/assets/common/item_price_calculation.ron +++ b/assets/common/item_price_calculation.ron @@ -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 ]) diff --git a/assets/common/loot_tables/materials/gems.ron b/assets/common/loot_tables/materials/gems.ron index 226beed85a..572025896a 100644 --- a/assets/common/loot_tables/materials/gems.ron +++ b/assets/common/loot_tables/materials/gems.ron @@ -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")), -] \ No newline at end of file +] diff --git a/assets/common/loot_tables/materials/underground.ron b/assets/common/loot_tables/materials/underground.ron index 9147aa87c2..88a247006c 100644 --- a/assets/common/loot_tables/materials/underground.ron +++ b/assets/common/loot_tables/materials/underground.ron @@ -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")), -] \ No newline at end of file + (0.15, LootTable("common.loot_tables.materials.gems")), +] diff --git a/assets/common/loot_tables/trading.ron b/assets/common/loot_tables/trading.ron index 9db4df6cdb..559a023911 100644 --- a/assets/common/loot_tables/trading.ron +++ b/assets/common/loot_tables/trading.ron @@ -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")), ] diff --git a/assets/world/dungeon/difficulty_distribution.ron b/assets/world/dungeon/difficulty_distribution.ron new file mode 100644 index 0000000000..1460bfc6e6 --- /dev/null +++ b/assets/world/dungeon/difficulty_distribution.ron @@ -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), +]) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index f755fe58d5..4d5c4aaf50 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -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(), diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index ad7a499127..9fb53ae67d 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -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, diff --git a/common/src/comp/inventory/trade_pricing.rs b/common/src/comp/inventory/trade_pricing.rs index 12daa3b38b..78fc2b7dda 100644 --- a/common/src/comp/inventory/trade_pricing.rs +++ b/common/src/comp/inventory/trade_pricing.rs @@ -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; 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, +} + +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> 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::::load_expect(&c).read().total(); - ProbabilityFile::load_expect_cloned(&c) + LootSpec::LootTable(table_asset) => { + let total = Lottery::::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::>() .into_iter() }, @@ -74,11 +120,12 @@ impl From> 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; + type Loader = assets::RonLoader; const EXTENSION: &'static str = "ron"; } @@ -95,10 +142,10 @@ impl assets::Compound for EqualitySet { id: &str, ) -> Result { let manifest = cache.load::>>>(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, 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, 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, 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 = 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, - ) -> 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::>(); 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 { 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::() * ((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 { 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::>() - .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::>() - .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::>() + .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::>() + .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); } } diff --git a/common/src/states/basic_beam.rs b/common/src/states/basic_beam.rs index defb22f81f..31080f42c0 100644 --- a/common/src/states/basic_beam.rs +++ b/common/src/states/basic_beam.rs @@ -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); diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 8cb8c73aea..3a5db4cf69 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -225,15 +225,24 @@ impl Block { /// arbitrary and only important when compared to one-another. #[inline] pub fn explode_power(&self) -> Option { + // 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), + }), } } diff --git a/server/src/events/information.rs b/server/src/events/information.rs index 490fe2ba44..cd11861ccd 100644 --- a/server/src/events/information.rs +++ b/server/src/events/information.rs @@ -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], ) }) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index fb4f013f69..67240676be 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -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))); diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index b28703a6b0..1d8ecde8c1 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -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| -> String { + let net_error = |error: String, mismatched_server_info: Option| -> 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!( diff --git a/world/economy_testinput2.ron b/world/economy_testinput2.ron new file mode 100644 index 0000000000..16e10df778 --- /dev/null +++ b/world/economy_testinput2.ron @@ -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, + ), + ], + ), +] \ No newline at end of file diff --git a/world/examples/economy_tree.rs b/world/examples/economy_tree.rs index 4bc05d322d..d60ac0ac79 100644 --- a/world/examples/economy_tree.rs +++ b/world/examples/economy_tree.rs @@ -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, "}}")?; diff --git a/world/src/sim2/mod.rs b/world/src/sim2/mod.rs index 92b64705b6..4c27fa849c 100644 --- a/world/src/sim2/mod.rs +++ b/world/src/sim2/mod.rs @@ -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 { 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 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::>(); 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, transportation_capacity: f32, external_orders: &mut DHashMap, Vec>, - potential_trade: &mut MapVec, -) -> MapVec { + potential_trade: &mut GoodMap, +) -> GoodMap { // 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 = MapVec::from_iter( + let mut extra_goods: GoodMap = 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, GoodRatioPrice> = site .economy .neighbors @@ -322,7 +375,7 @@ fn plan_trade_for_site( .collect(); // price+stock per site and good type SitePriceStock = Vec<(Id, (f32, f32))>; - let mut good_price: DHashMap = missing_goods + let mut good_price: DHashMap = 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, MapVec> = site + let mut neighbor_orders: DHashMap, GoodMap> = 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 = MapVec::from_default(0.0); + let mut total_orders: GoodMap = 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> = MapVec::from_iter( + let order_stock_ratio: GoodMap> = 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) { +fn collect_deliveries( + site: &mut Site, + deliveries: &mut Vec, + 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) { ); // 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) { /// 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, dt: f32) { +pub fn tick_site_economy( + index: &mut Index, + site_id: Id, + 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, 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, 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, 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, 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 = 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 = trade .iter() - .filter(|(_, a)| **a > 0.0) .chain(potential_trade.iter()) + .filter(|(_, a)| **a > 0.0) .map(|(g, _)| g) .collect(); - let empty_goods: DHashSet = DHashSet::default(); + //let empty_goods: DHashSet = 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 = 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 = 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::().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, dt: f32) { site.economy.stocks[*g] = 0.0; } - let mut total_labor_values = MapVec::<_, f32>::default(); + let mut total_labor_values = GoodMap::::default(); // TODO: trade - let mut total_outputs = MapVec::<_, f32>::default(); + let mut total_outputs = GoodMap::::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, dt: f32) { site.economy.stocks[*good] = (site.economy.stocks[*good] - used).max(0.0); } } - let mut produced_goods: MapVec = MapVec::from_default(0.0); + let mut produced_goods: GoodMap = 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, 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::(); + 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, 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, 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 = - 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 diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 514c5116de..bfbfe39a9d 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -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, 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( diff --git a/world/src/site/economy.rs b/world/src/site/economy.rs index e842b543b2..20dd4ef789 100644 --- a/world/src/site/economy.rs +++ b/world/src/site/economy.rs @@ -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); - -#[derive(Debug)] -pub struct AreaResources { - pub resource_sum: MapVec, - pub resource_chunks: MapVec, - 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, - - // computation simplifying cached values - pub chunks_per_resource: MapVec, - pub average_yield_per_chunk: MapVec, -} - -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); - -impl assets::Asset for RawProfessions { - type Loader = assets::RonLoader; - - const EXTENSION: &'static str = "ron"; -} - -pub fn default_professions() -> AssetHandle { - RawProfessions::load_expect("common.professions") -} - -lazy_static! { - static ref LABOR: AssetHandle = 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, - pub amount: MapVec, // positive for orders, negative for exchange -} - -#[derive(Debug)] -pub struct TradeDelivery { - pub supplier: Id, - pub amount: MapVec, // positive for orders, negative for exchange - pub prices: MapVec, // at the time of interaction - pub supply: MapVec, // maximum amount available, at the time of interaction -} - -#[derive(Debug)] -pub struct TradeInformation { - pub orders: DHashMap, Vec>, // per provider - pub deliveries: DHashMap, Vec>, // per receiver -} - -impl Default for TradeInformation { - fn default() -> Self { - Self { - orders: Default::default(), - deliveries: Default::default(), - } - } -} - -#[derive(Debug)] -pub struct NeighborInformation { - pub id: Id, - pub travel_distance: usize, - - // remembered from last interaction - pub last_values: MapVec, - pub last_supplies: MapVec, -} - -#[derive(Debug)] -pub struct Economy { - // Population - pub pop: f32, - - /// Total available amount of each good - pub stocks: MapVec, - /// Surplus stock compared to demand orders - pub surplus: MapVec, - /// change rate (derivative) of stock in the current situation - pub marginal_surplus: MapVec, - /// amount of wares not needed by the economy (helps with trade planning) - pub unconsumed_stock: MapVec, - // For some goods, such a goods without any supply, it doesn't make sense to talk about value - pub values: MapVec>, - pub last_exports: MapVec, - pub active_exports: MapVec, // unfinished trade (amount unconfirmed) - //pub export_targets: MapVec, - pub labor_values: MapVec>, - pub material_costs: MapVec, - - // Proportion of individuals dedicated to an industry - pub labors: MapVec, - // Per worker, per year, of their output good - pub yields: MapVec, - pub productivity: MapVec, - - pub natural_resources: NaturalResources, - // usize is distance - pub neighbors: Vec, -} - -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, 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> { - let products: MapVec> = 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, 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>| { - let sum = xs - .iter() - .map(|(_, x)| (*x).unwrap_or(0.0)) - .sum::() - .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 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 for GoodIndex { + type Error = NotFound; + + fn try_from(e: Good) -> Result { index_from_enum(e) } +} + +impl From 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 { + data: [V; GoodIndex::LENGTH], +} + +impl Default for GoodMap { + fn default() -> Self { + GoodMap { + data: [V::default(); GoodIndex::LENGTH], + } + } +} + +impl fmt::Debug for GoodMap { + 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 Index for GoodMap { + type Output = V; + + fn index(&self, index: GoodIndex) -> &Self::Output { &self.data[index.idx] } +} + +impl IndexMut for GoodMap { + fn index_mut(&mut self, index: GoodIndex) -> &mut Self::Output { &mut self.data[index.idx] } +} + +impl GoodMap { + pub fn iter(&self) -> impl Iterator + '_ { + (&self.data) + .iter() + .enumerate() + .map(|(idx, v)| (GoodIndex { idx }, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator + '_ { + (&mut self.data) + .iter_mut() + .enumerate() + .map(|(idx, v)| (GoodIndex { idx }, v)) + } +} + +impl GoodMap { + pub fn from_default(default: V) -> Self { + GoodMap { + data: [default; GoodIndex::LENGTH], + } + } + + pub fn from_iter(i: impl Iterator, 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(self, mut f: impl FnMut(GoodIndex, V) -> U) -> GoodMap { + let mut result = GoodMap::::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, 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); + +// 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 { + data: Vec, +} + +impl Default for LaborMap { + fn default() -> Self { + LaborMap { + data: std::iter::repeat(V::default()).take(*LABOR_COUNT).collect(), + } + } +} + +impl fmt::Debug for LaborMap { + 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 Index for LaborMap { + type Output = V; + + fn index(&self, index: LaborIndex) -> &Self::Output { &self.data[index.into_usize()] } +} + +impl IndexMut for LaborMap { + fn index_mut(&mut self, index: LaborIndex) -> &mut Self::Output { + &mut self.data[index.into_usize()] + } +} + +impl LaborMap { + pub fn iter(&self) -> impl Iterator + '_ { + (&self.data) + .iter() + .enumerate() + .map(|(idx, v)| (LaborIndex::from_usize(idx), v)) + } +} + +impl LaborMap { + pub fn from_default(default: V) -> Self { + LaborMap { + data: std::iter::repeat(default).take(*LABOR_COUNT).collect(), + } + } +} + +impl LaborMap { + pub fn from_iter(i: impl Iterator, 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(&self, f: impl Fn(LaborIndex, &V) -> U) -> LaborMap { + LaborMap { + data: self.iter().map(|i| f(i.0, i.1)).collect(), + } + } +} + +#[derive(Debug)] +pub struct AreaResources { + pub resource_sum: GoodMap, + pub resource_chunks: GoodMap, + 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, + + // computation simplifying cached values + pub chunks_per_resource: GoodMap, + pub average_yield_per_chunk: GoodMap, +} + +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); + +impl assets::Asset for RawProfessions { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + +pub fn default_professions() -> Vec { + 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 = 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, + pub amount: GoodMap, // positive for orders, negative for exchange +} + +#[derive(Debug)] +pub struct TradeDelivery { + pub supplier: Id, + pub amount: GoodMap, // positive for orders, negative for exchange + pub prices: GoodMap, // at the time of interaction + pub supply: GoodMap, // maximum amount available, at the time of interaction +} + +#[derive(Debug)] +pub struct TradeInformation { + pub orders: DHashMap, Vec>, // per provider + pub deliveries: DHashMap, Vec>, // per receiver +} + +impl Default for TradeInformation { + fn default() -> Self { + Self { + orders: Default::default(), + deliveries: Default::default(), + } + } +} + +#[derive(Debug)] +pub struct NeighborInformation { + pub id: Id, + pub travel_distance: usize, + + // remembered from last interaction + pub last_values: GoodMap, + pub last_supplies: GoodMap, +} + +#[derive(Debug)] +pub struct Economy { + // Population + pub pop: f32, + + /// Total available amount of each good + pub stocks: GoodMap, + /// Surplus stock compared to demand orders + pub surplus: GoodMap, + /// change rate (derivative) of stock in the current situation + pub marginal_surplus: GoodMap, + /// amount of wares not needed by the economy (helps with trade planning) + pub unconsumed_stock: GoodMap, + // For some goods, such a goods without any supply, it doesn't make sense to talk about value + pub values: GoodMap>, + pub last_exports: GoodMap, + pub active_exports: GoodMap, // unfinished trade (amount unconfirmed) + //pub export_targets: GoodMap, + pub labor_values: GoodMap>, + pub material_costs: GoodMap, + + // Proportion of individuals dedicated to an industry + pub labors: LaborMap, + // Per worker, per year, of their output good + pub yields: LaborMap, + pub productivity: LaborMap, + + pub natural_resources: NaturalResources, + // usize is distance + pub neighbors: Vec, +} + +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, 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, 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>| { + let sum = xs + .iter() + .map(|(_, x)| (*x).unwrap_or(0.0)) + .sum::() + .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 { + (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 { - (0..LABOR.read().0.len()) + (0..LABOR.len()) .filter(|&i| i != (DUMMY_LABOR.0 as usize)) .map(|i| Self(i as u8, PhantomData)) } diff --git a/world/src/site/mod.rs b/world/src/site/mod.rs index 37283b40a7..90c6635ae7 100644 --- a/world/src/site/mod.rs +++ b/world/src/site/mod.rs @@ -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) diff --git a/world/src/util/map_array.rs b/world/src/util/map_array.rs new file mode 100644 index 0000000000..84f6fde108 --- /dev/null +++ b/world/src/util/map_array.rs @@ -0,0 +1,110 @@ +use std::cmp::PartialEq; + +pub trait GenericIndex { + 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, const N: usize>( + val: E, +) -> Result { + I::VALUES + .iter() + .position(|v| val == *v) + .ok_or(NotFound {}) + .map(I::from_usize) +} + +pub fn enum_from_index, 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 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 for MyIndex { + type Error = NotFound; + + fn try_from(e: MyEnum) -> Result { index_from_enum(e) } + } + + impl From for MyEnum { + fn from(idx: MyIndex) -> MyEnum { enum_from_index(idx) } + } + + // the "map" itself + struct MyMap([V; MyIndex::LENGTH]); + + impl Default for MyMap { + fn default() -> Self { MyMap([V::default(); MyIndex::LENGTH]) } + } + + impl Index for MyMap { + type Output = V; + + fn index(&self, index: MyIndex) -> &Self::Output { &self.0[index.0] } + } + + impl IndexMut for MyMap { + fn index_mut(&mut self, index: MyIndex) -> &mut Self::Output { &mut self.0[index.0] } + } + + impl MyMap { + pub fn iter(&self) -> impl Iterator + '_ { + (&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); + } + } +} diff --git a/world/src/util/mod.rs b/world/src/util/mod.rs index 0a22405bcd..27cc30c080 100644 --- a/world/src/util/mod.rs +++ b/world/src/util/mod.rs @@ -1,4 +1,5 @@ pub mod fast_noise; +pub mod map_array; pub mod map_vec; pub mod math; pub mod random;