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));