diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json index 4a235e6..c2ce0ed 100644 --- a/Mods/Core/Defs/Animals/Animals.json +++ b/Mods/Core/Defs/Animals/Animals.json @@ -16,6 +16,7 @@ "body": "Quadruped", "diet": ["plant"], "spawnPer1000Cells": 2.5, + "baseMassKg": 70, "genome": { "GeneMaxBodySize": 1.5, "GeneMetabolism": 1.0, @@ -51,6 +52,7 @@ "attackDamage": 80, "attackBleed": 3.0, "attackBloodLoss": 5.0, + "baseMassKg": 45, "genome": { "GeneMaxBodySize": 1.6, "GeneMetabolism": 1.1, @@ -84,6 +86,7 @@ "attackDamage": 45, "attackBleed": 1.8, "attackBloodLoss": 3.0, + "baseMassKg": 80, "genome": { "GeneMaxBodySize": 1.3, "GeneMetabolism": 1.2, @@ -116,6 +119,7 @@ "spawnPer1000Cells": 1.5, "baseSpeed": 22, "visionCells": 10, + "baseMassKg": 2.5, "genome": { "GeneMaxBodySize": 0.5, "GeneMetabolism": 1.3, diff --git a/Mods/Core/Defs/Plants/Grass.json b/Mods/Core/Defs/Plants/Grass.json index 93bac6f..9fab8b7 100644 --- a/Mods/Core/Defs/Plants/Grass.json +++ b/Mods/Core/Defs/Plants/Grass.json @@ -3,6 +3,7 @@ // Трава: быстрый почвопокров, основной скаттер лугов и леса. "defs": [ { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5, + "baseMassKg": 0.4, "harvestProduct": "ProductGrass", "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8, "coldHardiness": 9, "heatHardiness": 6, diff --git a/Mods/Core/Defs/Plants/_Bases.json b/Mods/Core/Defs/Plants/_Bases.json index e5d2935..a36dec9 100644 --- a/Mods/Core/Defs/Plants/_Bases.json +++ b/Mods/Core/Defs/Plants/_Bases.json @@ -4,6 +4,7 @@ // через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …). "defs": [ { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, + "baseMassKg": 450, "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10, "coldHardiness": 14, "heatHardiness": 12, @@ -19,6 +20,7 @@ // Деревья тёплой и холодной температурных ниш (showcase температурных генов). { "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, + "baseMassKg": 420, "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9, "coldHardiness": 8, "heatHardiness": 16, @@ -32,6 +34,7 @@ { "sizeCells": 2.0, "label": "plant.stage.mature" } ] }, { "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26, + "baseMassKg": 500, "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12, "coldHardiness": 18, "heatHardiness": 8, @@ -46,6 +49,7 @@ ] }, { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, + "baseMassKg": 6, "fruitProduct": "ProductBerry", "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9, "coldHardiness": 10, "heatHardiness": 8, @@ -60,6 +64,7 @@ // Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы. { "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0, + "baseMassKg": 35, "harvestProduct": "ProductFiber", "genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8, "coldHardiness": 20, "heatHardiness": 18, @@ -76,6 +81,7 @@ // Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно // (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение. { "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0, + "baseMassKg": 0.3, "harvestProduct": "ProductMushroom", "genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11, "coldHardiness": 12, "heatHardiness": 8, @@ -91,6 +97,7 @@ // Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда). { "defName": "BaseFlower", "abstract": true, "label": "plant.flower", "sizeCells": 1.0, "trunkRadiusCells": 0, + "baseMassKg": 0.15, "genome": { "optimalLight": 0.9, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 9, "coldHardiness": 8, "heatHardiness": 8, "optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4, @@ -103,6 +110,7 @@ ] }, { "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0, + "baseMassKg": 5, "fruitProduct": "ProductBerry", "genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9, "coldHardiness": 10, "heatHardiness": 7, diff --git a/Mods/Core/Defs/Products/Products.json b/Mods/Core/Defs/Products/Products.json index be8b667..447c04a 100644 --- a/Mods/Core/Defs/Products/Products.json +++ b/Mods/Core/Defs/Products/Products.json @@ -3,16 +3,16 @@ // Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct; // количество задаётся генами (harvestAmount / fruitYield). "defs": [ - { "defName": "ProductWood", "label": "product.wood", "kind": "material" }, - { "defName": "ProductGrass", "label": "product.grass", "kind": "material" }, - { "defName": "ProductFiber", "label": "product.fiber", "kind": "material" }, - { "defName": "ProductBerry", "label": "product.berry", "kind": "food" }, - { "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }, - { "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" }, + { "defName": "ProductWood", "label": "product.wood", "kind": "material", "massKg": 1.0 }, + { "defName": "ProductGrass", "label": "product.grass", "kind": "material", "massKg": 0.1 }, + { "defName": "ProductFiber", "label": "product.fiber", "kind": "material", "massKg": 0.2 }, + { "defName": "ProductBerry", "label": "product.berry", "kind": "food", "massKg": 0.05 }, + { "defName": "ProductAcorn", "label": "product.acorn", "kind": "food", "massKg": 0.05 }, + { "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food", "massKg": 0.1 }, // Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже. - { "defName": "ProductMeat", "label": "product.meat", "kind": "food" }, - { "defName": "ProductBone", "label": "product.bone", "kind": "material" }, - { "defName": "ProductLeather", "label": "product.leather", "kind": "material" } + { "defName": "ProductMeat", "label": "product.meat", "kind": "food", "massKg": 0.5 }, + { "defName": "ProductBone", "label": "product.bone", "kind": "material", "massKg": 0.3 }, + { "defName": "ProductLeather", "label": "product.leather", "kind": "material", "massKg": 0.4 } ] } diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 2f0a270..dbaca85 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -17,6 +17,8 @@ "inspect.growing": "Growth: {0:0}% to next stage", "inspect.mature": "Fully grown", "inspect.age": "Age: {0:0.0} d · lives to {1:0}", + "inspect.mass": "Weight: {0:0.#} kg · carries up to {1:0.#} kg", + "inspect.plantmass": "Weight: {0:0.##} kg", "inspect.state": "State: {0}", "inspect.state.growing": "growing", "inspect.state.mature": "mature", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 0d93144..ada3961 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -17,6 +17,8 @@ "inspect.growing": "Рост: {0:0}% до следующей стадии", "inspect.mature": "Полностью выросло", "inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}", + "inspect.mass": "Вес: {0:0.#} кг · несёт до {1:0.#} кг", + "inspect.plantmass": "Вес: {0:0.##} кг", "inspect.state": "Состояние: {0}", "inspect.state.growing": "растёт", "inspect.state.mature": "созрело", diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index c3fa062..32a427c 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -75,6 +75,9 @@ public sealed class ProductDef : Def { /// Категория продукта (например "material" или "food") — для будущей экономики/инвентаря. public string Kind { get; init; } = ""; + + /// Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность). + public float MassKg { get; init; } = 1f; } /// @@ -177,6 +180,9 @@ public sealed class PlantDef : Def /// Имя , выдаваемого плодами (ягоды/жёлудь); null — не плодоносит. public string? FruitProduct { get; init; } + + /// Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³. + public float BaseMassKg { get; init; } = 20f; } /// @@ -264,6 +270,9 @@ public sealed class AnimalDef : PawnDef /// Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки. public string EggTexture { get; init; } = "things/plant/seed_default"; + /// Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³. + public float BaseMassKg { get; init; } = 40f; + /// Имя — анатомия вида (части тела/органы); пусто — без частей тела. public string Body { get; init; } = ""; diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 15310e8..a4ac37c 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -231,7 +231,7 @@ public sealed class WorldScene : Scene // нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой. var shore = ComputeShore(); var thoughts = new ThoughtSet(content); - UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs)); + UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals)); UpdateSystems.Add( new AnimalRutSystem(_animals, climate, content.Defs.Get("Rut")) ); @@ -637,7 +637,7 @@ public sealed class WorldScene : Scene X = transform.Position.X, Y = transform.Position.Y, RotDays = corpse.RotDays, - BodySize = corpse.Meat / AnimalFactory.MeatPerBodySize, + BodySize = corpse.BodySize, Meat = corpse.Meat, } ) @@ -732,12 +732,12 @@ public sealed class WorldScene : Scene _animals.IndexOf(def), new Vector2(saved.X, saved.Y), saved.BodySize, + saved.Meat, CellSize ); ref var data = ref corpse.GetComponent(); data.RotDays = saved.RotDays; data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике - data.Meat = saved.Meat; } foreach (var saved in _save.Eggs) @@ -1144,6 +1144,7 @@ public sealed class WorldScene : Scene $" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, " + $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})" ); + console.WriteLine($" mass (mature): {def.BaseMassKg:0.##} kg"); if (def.HarvestProduct is { } harvest) { @@ -1211,6 +1212,10 @@ public sealed class WorldScene : Scene console.WriteLine( $" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}" ); + var adultMass = Mass.OfAnimal(def, traits, 1f); + console.WriteLine( + $" mass: {adultMass:0.#} kg (adult), carries ~{Mass.CarryCapacity(adultMass, 1f):0.#} kg" + ); } // Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory, diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 268ece5..666ff0f 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -75,19 +75,18 @@ public static class AnimalFactory ); } - /// Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков). - public const float MeatPerBodySize = 25f; - // Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem). private static readonly Color CorpseFreshTint = new(176, 148, 128); - /// Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела). + /// Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и + /// запас мяса (∝ масса тела, считается вызывающим через ). public static Entity CreateCorpse( EntityStore store, AnimalSet animals, int species, Vector2 position, float bodySize, + float meatKg, int cellSize ) { @@ -105,7 +104,8 @@ public static class AnimalFactory Species = species, RotDays = 0f, Stage = 0, - Meat = bodySize * MeatPerBodySize, + Meat = meatKg, + BodySize = bodySize, } ); } @@ -184,7 +184,7 @@ public static class AnimalFactory return false; } - /// Гибель особи: оставляет труп (мясо ∝ размер тела) и удаляет сущность. Общий путь смерти. + /// Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти. public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize) { if (entity.IsNull || !entity.HasComponent()) @@ -194,7 +194,12 @@ public static class AnimalFactory ref readonly var org = ref entity.GetComponent(); var position = entity.GetComponent().Position; - CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, cellSize); + var sp = animals[org.Species]; + var stageScale = entity.TryGetComponent(out var grow) + ? sp.Stages[Math.Clamp(grow.Stage, 0, sp.Stages.Length - 1)].Scale + : 1f; + var meat = Mass.OfAnimal(sp.Def, org.Traits, stageScale) * Mass.MeatFraction; + CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, meat, cellSize); entity.DeleteEntity(); } diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 4bca504..394eac3 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -464,8 +464,11 @@ public struct Corpse : IComponent /// Стадия: 0 свежий, 1 гниющий, 2 скелет. public int Stage; - /// Доступное мясо (∝ размер тела) — для разделки/падальщиков (фаза-горизонт). + /// Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт). public float Meat; + + /// Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса. + public float BodySize; } /// @@ -488,6 +491,17 @@ public struct Egg : IComponent public float IncubateDays; } +/// +/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз +/// сверх грузоподъёмности () замедляет движение. Компонент появляется +/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь. +/// +public struct Carrier : IComponent +{ + /// Суммарная масса переносимого груза (кг). + public float CarriedKg; +} + /// /// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт /// его семя в кишечнике, затем «высаживает» его в другом месте () — diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index c6dcbb8..c9ef9f1 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -122,8 +122,12 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell /// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные /// нужды на нуле копят счётчик истощения. Полностью управляется данными . /// -public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs) - : QuerySystem +public sealed class AnimalNeedsSystem( + GameClock clock, + float secondsPerDay, + NeedSet needs, + AnimalSet animals +) : QuerySystem { // Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2). private const float ToleranceMetabolicCost = 0.5f; @@ -136,11 +140,12 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need return; } - foreach (var (needsChunk, organisms, healths, _) in Query.Chunks) + foreach (var (needsChunk, organisms, healths, growths, _) in Query.Chunks) { var n = needsChunk.Span; var o = organisms.Span; var h = healths.Span; + var g = growths.Span; for (var i = 0; i < n.Length; i++) { var values = n[i].Values; @@ -149,14 +154,16 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need continue; // защита от старого/пустого состояния } - // Цена устойчивости к яду (коэволюция C2): детоксикация метаболически затратна — чем выше - // toxinTolerance, тем быстрее расходуются нужды (зверь голоднее). Без этой платы устойчивость - // ушла бы к максимуму у всех и гонка вооружений встала бы. Множитель к обмену веществ. + // Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела: + // расход ∝ масса^0.75, нормирован на эталон вида → молодняк экономнее, крупные особи + // прожорливее (масса растёт с размером/возрастом). Множитель к расходу убывающих нужд. + var sp = animals[o[i].Species]; + var stageScale = sp.Stages[Math.Clamp(g[i].Stage, 0, sp.Stages.Length - 1)].Scale; + var mass = Mass.OfAnimal(sp.Def, o[i].Traits, stageScale); var metabolism = MathF.Max(0.1f, o[i].Traits.Metabolism) - * ( - 1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f) - ); + * (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f)) + * Mass.KleiberFactor(mass, sp.Def.BaseMassKg); var lethalEmpty = false; for (var k = 0; k < needs.Count; k++) { @@ -965,6 +972,22 @@ public sealed class AnimalActionSystem : BaseSystem // даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1). var eating = hh[i].State.Capacity(AnimalCapacities.Eating); var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion); + // Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость. + if ( + entities.EntityAt(i).TryGetComponent(out var carrier) + && carrier.CarriedKg > 0f + ) + { + var sp = _animalSet[o[i].Species]; + var stageScale = entities.EntityAt(i).TryGetComponent(out var gr) + ? sp.Stages[Math.Clamp(gr.Stage, 0, sp.Stages.Length - 1)].Scale + : 1f; + var capacity = Mass.CarryCapacity( + Mass.OfAnimal(sp.Def, o[i].Traits, stageScale), + moving + ); + speed *= Mass.LoadSpeedFactor(carrier.CarriedKg, capacity); + } switch (brain.Action) { diff --git a/src/LittleSim/Sim/Mass.cs b/src/LittleSim/Sim/Mass.cs new file mode 100644 index 0000000..8e9b28d --- /dev/null +++ b/src/LittleSim/Sim/Mass.cs @@ -0,0 +1,70 @@ +using LittleSim.Content; + +namespace LittleSim.Sim; + +/// +/// Система веса (масса тел и грузоподъёмность). Масса — производная от РАЗМЕРА по кубическому закону +/// (вдвое крупнее ⇒ в 8 раз тяжелее): масса = BaseMassKg × (размер / эталон)³, где эталон — +/// базовый размер вида. Так детёныш/росток легче взрослого/зрелого, а ген размера двигает и массу. +/// Грузоподъёмность — задел под перенос предметов: доля массы тела × способность (манипуляция/движение). +/// +public static class Mass +{ + /// Какую долю собственной массы существо может нести (×способность). Человек (руки) — больше. + public const float CarryFraction = 0.35f; + + /// Доля массы тела, превращающаяся в мясо трупа (для разделки/падальщиков). + public const float MeatFraction = 0.5f; + + private static float Cube(float v) => v * v * v; + + /// Масса особи-животного (кг): эталон вида × куб (текущий линейный размер / эталонный). + public static float OfAnimal(AnimalDef def, in AnimalPhenotype traits, float stageScale) + { + var reference = def.Genome.GetValueOrDefault("GeneMaxBodySize", 1f); + if (reference <= 0f) + { + reference = 1f; + } + + var linear = MathF.Max(0.01f, traits.BodySize) * MathF.Max(0.05f, stageScale); + return def.BaseMassKg * Cube(linear / reference); + } + + /// Масса растения (кг): эталон вида × куб (размер текущей стадии / размер зрелой). + public static float OfPlant(PlantDef def, float stageSizeCells, float matureSizeCells) + { + var ratio = matureSizeCells > 0f ? stageSizeCells / matureSizeCells : 1f; + return def.BaseMassKg * Cube(ratio); + } + + /// Грузоподъёмность (кг): доля массы тела × способность нести (манипуляция/движение, 0..1+). + public static float CarryCapacity(float bodyMassKg, float capability) => + bodyMassKg * CarryFraction * MathF.Max(0f, capability); + + /// + /// Метаболический множитель по закону Клайбера: расход энергии ∝ масса^0.75. Нормирован на эталонную + /// массу вида (взрослый базового размера = 1), поэтому крупные особи прожорливее, а молодняк — экономнее, + /// без межвидового взрыва баланса. + /// + public static float KleiberFactor(float massKg, float referenceMassKg) + { + if (referenceMassKg <= 0f || massKg <= 0f) + { + return 1f; + } + + return MathF.Pow(massKg / referenceMassKg, 0.75f); + } + + /// Множитель скорости от ноши: в пределах грузоподъёмности — 1; перегруз тормозит (до 0.25). + public static float LoadSpeedFactor(float carriedKg, float capacityKg) + { + if (capacityKg <= 0f || carriedKg <= capacityKg) + { + return 1f; + } + + return MathF.Max(0.25f, capacityKg / carriedKg); + } +} diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs index a397a85..9b8dd8c 100644 --- a/src/LittleSim/UI/InspectPanel.cs +++ b/src/LittleSim/UI/InspectPanel.cs @@ -291,6 +291,14 @@ internal sealed class InspectPanel text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan)); + // Вес растения (∝ кубу размера текущей стадии относительно зрелой). + var plantMass = Mass.OfPlant( + species.Def, + species.Stages[grow.Stage].SizeCells, + species.Stages[^1].SizeCells + ); + text.AppendLine(languages.Format("inspect.plantmass", plantMass)); + // Состояние по температуре (рост/покой) и накопленный стресс. var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); var temperature = _climate.Temperature; @@ -400,6 +408,16 @@ internal sealed class InspectPanel text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan)); var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female"); text.AppendLine(languages.Format("inspect.animal.sex", sex, org.Generation)); + + // Вес тела и грузоподъёмность (∝ массе × способность нести; четвероногие — через Moving). + var stageScale = sp.Stages[grow.Stage].Scale; + var mass = Mass.OfAnimal(sp.Def, org.Traits, stageScale); + var carryCap = + entity.TryGetComponent(out var health) && health.State is not null + ? Mass.CarryCapacity(mass, health.State.Capacity(AnimalCapacities.Moving)) + : Mass.CarryCapacity(mass, 1f); + text.AppendLine(languages.Format("inspect.mass", mass, carryCap)); + if (entity.TryGetComponent(out var preg)) { text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays));