Merge branch 'mass-system' into main

Weight system: cubic body mass for plants/animals (BaseMassKg), carry capacity,
Kleiber metabolism, mass-based corpse meat, and a dormant Carrier foundation for
future item hauling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 21:36:18 +03:00
co-authored by Claude Opus 4.8
13 changed files with 190 additions and 29 deletions
+4
View File
@@ -16,6 +16,7 @@
"body": "Quadruped", "body": "Quadruped",
"diet": ["plant"], "diet": ["plant"],
"spawnPer1000Cells": 2.5, "spawnPer1000Cells": 2.5,
"baseMassKg": 70,
"genome": { "genome": {
"GeneMaxBodySize": 1.5, "GeneMaxBodySize": 1.5,
"GeneMetabolism": 1.0, "GeneMetabolism": 1.0,
@@ -51,6 +52,7 @@
"attackDamage": 80, "attackDamage": 80,
"attackBleed": 3.0, "attackBleed": 3.0,
"attackBloodLoss": 5.0, "attackBloodLoss": 5.0,
"baseMassKg": 45,
"genome": { "genome": {
"GeneMaxBodySize": 1.6, "GeneMaxBodySize": 1.6,
"GeneMetabolism": 1.1, "GeneMetabolism": 1.1,
@@ -84,6 +86,7 @@
"attackDamage": 45, "attackDamage": 45,
"attackBleed": 1.8, "attackBleed": 1.8,
"attackBloodLoss": 3.0, "attackBloodLoss": 3.0,
"baseMassKg": 80,
"genome": { "genome": {
"GeneMaxBodySize": 1.3, "GeneMaxBodySize": 1.3,
"GeneMetabolism": 1.2, "GeneMetabolism": 1.2,
@@ -116,6 +119,7 @@
"spawnPer1000Cells": 1.5, "spawnPer1000Cells": 1.5,
"baseSpeed": 22, "baseSpeed": 22,
"visionCells": 10, "visionCells": 10,
"baseMassKg": 2.5,
"genome": { "genome": {
"GeneMaxBodySize": 0.5, "GeneMaxBodySize": 0.5,
"GeneMetabolism": 1.3, "GeneMetabolism": 1.3,
+1
View File
@@ -3,6 +3,7 @@
// Трава: быстрый почвопокров, основной скаттер лугов и леса. // Трава: быстрый почвопокров, основной скаттер лугов и леса.
"defs": [ "defs": [
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5, { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5,
"baseMassKg": 0.4,
"harvestProduct": "ProductGrass", "harvestProduct": "ProductGrass",
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8, "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8,
"coldHardiness": 9, "heatHardiness": 6, "coldHardiness": 9, "heatHardiness": 6,
+8
View File
@@ -4,6 +4,7 @@
// через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …). // через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …).
"defs": [ "defs": [
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"baseMassKg": 450,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10,
"coldHardiness": 14, "heatHardiness": 12, "coldHardiness": 14, "heatHardiness": 12,
@@ -19,6 +20,7 @@
// Деревья тёплой и холодной температурных ниш (showcase температурных генов). // Деревья тёплой и холодной температурных ниш (showcase температурных генов).
{ "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, { "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"baseMassKg": 420,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9, "genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9,
"coldHardiness": 8, "heatHardiness": 16, "coldHardiness": 8, "heatHardiness": 16,
@@ -32,6 +34,7 @@
{ "sizeCells": 2.0, "label": "plant.stage.mature" } { "sizeCells": 2.0, "label": "plant.stage.mature" }
] }, ] },
{ "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26, { "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26,
"baseMassKg": 500,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12, "genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12,
"coldHardiness": 18, "heatHardiness": 8, "coldHardiness": 18, "heatHardiness": 8,
@@ -46,6 +49,7 @@
] }, ] },
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
"baseMassKg": 6,
"fruitProduct": "ProductBerry", "fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9,
"coldHardiness": 10, "heatHardiness": 8, "coldHardiness": 10, "heatHardiness": 8,
@@ -60,6 +64,7 @@
// Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы. // Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы.
{ "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0, { "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0,
"baseMassKg": 35,
"harvestProduct": "ProductFiber", "harvestProduct": "ProductFiber",
"genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8, "genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8,
"coldHardiness": 20, "heatHardiness": 18, "coldHardiness": 20, "heatHardiness": 18,
@@ -76,6 +81,7 @@
// Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно // Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно
// (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение. // (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение.
{ "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0, { "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0,
"baseMassKg": 0.3,
"harvestProduct": "ProductMushroom", "harvestProduct": "ProductMushroom",
"genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11, "genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11,
"coldHardiness": 12, "heatHardiness": 8, "coldHardiness": 12, "heatHardiness": 8,
@@ -91,6 +97,7 @@
// Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда). // Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда).
{ "defName": "BaseFlower", "abstract": true, "label": "plant.flower", "sizeCells": 1.0, "trunkRadiusCells": 0, { "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, "genome": { "optimalLight": 0.9, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 9,
"coldHardiness": 8, "heatHardiness": 8, "coldHardiness": 8, "heatHardiness": 8,
"optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4, "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, { "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0,
"baseMassKg": 5,
"fruitProduct": "ProductBerry", "fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9, "genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9,
"coldHardiness": 10, "heatHardiness": 7, "coldHardiness": 10, "heatHardiness": 7,
+9 -9
View File
@@ -3,16 +3,16 @@
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct; // Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
// количество задаётся генами (harvestAmount / fruitYield). // количество задаётся генами (harvestAmount / fruitYield).
"defs": [ "defs": [
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" }, { "defName": "ProductWood", "label": "product.wood", "kind": "material", "massKg": 1.0 },
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" }, { "defName": "ProductGrass", "label": "product.grass", "kind": "material", "massKg": 0.1 },
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material" }, { "defName": "ProductFiber", "label": "product.fiber", "kind": "material", "massKg": 0.2 },
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" }, { "defName": "ProductBerry", "label": "product.berry", "kind": "food", "massKg": 0.05 },
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }, { "defName": "ProductAcorn", "label": "product.acorn", "kind": "food", "massKg": 0.05 },
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" }, { "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food", "massKg": 0.1 },
// Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже. // Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже.
{ "defName": "ProductMeat", "label": "product.meat", "kind": "food" }, { "defName": "ProductMeat", "label": "product.meat", "kind": "food", "massKg": 0.5 },
{ "defName": "ProductBone", "label": "product.bone", "kind": "material" }, { "defName": "ProductBone", "label": "product.bone", "kind": "material", "massKg": 0.3 },
{ "defName": "ProductLeather", "label": "product.leather", "kind": "material" } { "defName": "ProductLeather", "label": "product.leather", "kind": "material", "massKg": 0.4 }
] ]
} }
+2
View File
@@ -17,6 +17,8 @@
"inspect.growing": "Growth: {0:0}% to next stage", "inspect.growing": "Growth: {0:0}% to next stage",
"inspect.mature": "Fully grown", "inspect.mature": "Fully grown",
"inspect.age": "Age: {0:0.0} d · lives to {1:0}", "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": "State: {0}",
"inspect.state.growing": "growing", "inspect.state.growing": "growing",
"inspect.state.mature": "mature", "inspect.state.mature": "mature",
+2
View File
@@ -17,6 +17,8 @@
"inspect.growing": "Рост: {0:0}% до следующей стадии", "inspect.growing": "Рост: {0:0}% до следующей стадии",
"inspect.mature": "Полностью выросло", "inspect.mature": "Полностью выросло",
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}", "inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
"inspect.mass": "Вес: {0:0.#} кг · несёт до {1:0.#} кг",
"inspect.plantmass": "Вес: {0:0.##} кг",
"inspect.state": "Состояние: {0}", "inspect.state": "Состояние: {0}",
"inspect.state.growing": "растёт", "inspect.state.growing": "растёт",
"inspect.state.mature": "созрело", "inspect.state.mature": "созрело",
+9
View File
@@ -75,6 +75,9 @@ public sealed class ProductDef : Def
{ {
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary> /// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
public string Kind { get; init; } = ""; public string Kind { get; init; } = "";
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
public float MassKg { get; init; } = 1f;
} }
/// <summary> /// <summary>
@@ -177,6 +180,9 @@ public sealed class PlantDef : Def
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary> /// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
public string? FruitProduct { get; init; } public string? FruitProduct { get; init; }
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
public float BaseMassKg { get; init; } = 20f;
} }
/// <summary> /// <summary>
@@ -264,6 +270,9 @@ public sealed class AnimalDef : PawnDef
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary> /// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
public string EggTexture { get; init; } = "things/plant/seed_default"; public string EggTexture { get; init; } = "things/plant/seed_default";
/// <summary>Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³.</summary>
public float BaseMassKg { get; init; } = 40f;
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary> /// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
public string Body { get; init; } = ""; public string Body { get; init; } = "";
+8 -3
View File
@@ -231,7 +231,7 @@ public sealed class WorldScene : Scene
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой. // нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
var shore = ComputeShore(); var shore = ComputeShore();
var thoughts = new ThoughtSet(content); var thoughts = new ThoughtSet(content);
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs)); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
UpdateSystems.Add( UpdateSystems.Add(
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut")) new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
); );
@@ -637,7 +637,7 @@ public sealed class WorldScene : Scene
X = transform.Position.X, X = transform.Position.X,
Y = transform.Position.Y, Y = transform.Position.Y,
RotDays = corpse.RotDays, RotDays = corpse.RotDays,
BodySize = corpse.Meat / AnimalFactory.MeatPerBodySize, BodySize = corpse.BodySize,
Meat = corpse.Meat, Meat = corpse.Meat,
} }
) )
@@ -732,12 +732,12 @@ public sealed class WorldScene : Scene
_animals.IndexOf(def), _animals.IndexOf(def),
new Vector2(saved.X, saved.Y), new Vector2(saved.X, saved.Y),
saved.BodySize, saved.BodySize,
saved.Meat,
CellSize CellSize
); );
ref var data = ref corpse.GetComponent<Corpse>(); ref var data = ref corpse.GetComponent<Corpse>();
data.RotDays = saved.RotDays; data.RotDays = saved.RotDays;
data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике
data.Meat = saved.Meat;
} }
foreach (var saved in _save.Eggs) foreach (var saved in _save.Eggs)
@@ -1144,6 +1144,7 @@ public sealed class WorldScene : Scene
$" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, " $" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, "
+ $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})" + $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})"
); );
console.WriteLine($" mass (mature): {def.BaseMassKg:0.##} kg");
if (def.HarvestProduct is { } harvest) if (def.HarvestProduct is { } harvest)
{ {
@@ -1211,6 +1212,10 @@ public sealed class WorldScene : Scene
console.WriteLine( console.WriteLine(
$" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}" $" 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, // Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
+12 -7
View File
@@ -75,19 +75,18 @@ public static class AnimalFactory
); );
} }
/// <summary>Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков).</summary>
public const float MeatPerBodySize = 25f;
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem). // Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
private static readonly Color CorpseFreshTint = new(176, 148, 128); private static readonly Color CorpseFreshTint = new(176, 148, 128);
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).</summary> /// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и
/// запас мяса <paramref name="meatKg"/> (∝ масса тела, считается вызывающим через <see cref="Mass"/>).</summary>
public static Entity CreateCorpse( public static Entity CreateCorpse(
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
int species, int species,
Vector2 position, Vector2 position,
float bodySize, float bodySize,
float meatKg,
int cellSize int cellSize
) )
{ {
@@ -105,7 +104,8 @@ public static class AnimalFactory
Species = species, Species = species,
RotDays = 0f, RotDays = 0f,
Stage = 0, Stage = 0,
Meat = bodySize * MeatPerBodySize, Meat = meatKg,
BodySize = bodySize,
} }
); );
} }
@@ -184,7 +184,7 @@ public static class AnimalFactory
return false; return false;
} }
/// <summary>Гибель особи: оставляет труп (мясо ∝ размер тела) и удаляет сущность. Общий путь смерти.</summary> /// <summary>Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти.</summary>
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize) public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
{ {
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>()) if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
@@ -194,7 +194,12 @@ public static class AnimalFactory
ref readonly var org = ref entity.GetComponent<AnimalOrganism>(); ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
var position = entity.GetComponent<Transform2D>().Position; var position = entity.GetComponent<Transform2D>().Position;
CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, cellSize); var sp = animals[org.Species];
var stageScale = entity.TryGetComponent<AnimalGrowth>(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(); entity.DeleteEntity();
} }
+15 -1
View File
@@ -464,8 +464,11 @@ public struct Corpse : IComponent
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary> /// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
public int Stage; public int Stage;
/// <summary>Доступное мясо (∝ размер тела) — для разделки/падальщиков (фаза-горизонт).</summary> /// <summary>Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт).</summary>
public float Meat; public float Meat;
/// <summary>Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса.</summary>
public float BodySize;
} }
/// <summary> /// <summary>
@@ -488,6 +491,17 @@ public struct Egg : IComponent
public float IncubateDays; public float IncubateDays;
} }
/// <summary>
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
/// сверх грузоподъёмности (<see cref="Sim.Mass.CarryCapacity"/>) замедляет движение. Компонент появляется
/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь.
/// </summary>
public struct Carrier : IComponent
{
/// <summary>Суммарная масса переносимого груза (кг).</summary>
public float CarriedKg;
}
/// <summary> /// <summary>
/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт /// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
/// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) — /// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) —
+32 -9
View File
@@ -122,8 +122,12 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell
/// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные /// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные
/// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>. /// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>.
/// </summary> /// </summary>
public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs) public sealed class AnimalNeedsSystem(
: QuerySystem<AnimalNeeds, AnimalOrganism, Health> GameClock clock,
float secondsPerDay,
NeedSet needs,
AnimalSet animals
) : QuerySystem<AnimalNeeds, AnimalOrganism, Health, AnimalGrowth>
{ {
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2). // Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
private const float ToleranceMetabolicCost = 0.5f; private const float ToleranceMetabolicCost = 0.5f;
@@ -136,11 +140,12 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
return; return;
} }
foreach (var (needsChunk, organisms, healths, _) in Query.Chunks) foreach (var (needsChunk, organisms, healths, growths, _) in Query.Chunks)
{ {
var n = needsChunk.Span; var n = needsChunk.Span;
var o = organisms.Span; var o = organisms.Span;
var h = healths.Span; var h = healths.Span;
var g = growths.Span;
for (var i = 0; i < n.Length; i++) for (var i = 0; i < n.Length; i++)
{ {
var values = n[i].Values; var values = n[i].Values;
@@ -149,14 +154,16 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
continue; // защита от старого/пустого состояния continue; // защита от старого/пустого состояния
} }
// Цена устойчивости к яду (коэволюция C2): детоксикация метаболически затратна — чем выше // Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела:
// toxinTolerance, тем быстрее расходуются нужды (зверь голоднее). Без этой платы устойчивость // расход ∝ масса^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 = var metabolism =
MathF.Max(0.1f, o[i].Traits.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; var lethalEmpty = false;
for (var k = 0; k < needs.Count; k++) for (var k = 0; k < needs.Count; k++)
{ {
@@ -965,6 +972,22 @@ public sealed class AnimalActionSystem : BaseSystem
// даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1). // даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
var eating = hh[i].State.Capacity(AnimalCapacities.Eating); var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion); var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
if (
entities.EntityAt(i).TryGetComponent<Carrier>(out var carrier)
&& carrier.CarriedKg > 0f
)
{
var sp = _animalSet[o[i].Species];
var stageScale = entities.EntityAt(i).TryGetComponent<AnimalGrowth>(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) switch (brain.Action)
{ {
+70
View File
@@ -0,0 +1,70 @@
using LittleSim.Content;
namespace LittleSim.Sim;
/// <summary>
/// Система веса (масса тел и грузоподъёмность). Масса — производная от РАЗМЕРА по кубическому закону
/// (вдвое крупнее ⇒ в 8 раз тяжелее): <c>масса = BaseMassKg × (размер / эталон)³</c>, где эталон —
/// базовый размер вида. Так детёныш/росток легче взрослого/зрелого, а ген размера двигает и массу.
/// Грузоподъёмность — задел под перенос предметов: доля массы тела × способность (манипуляция/движение).
/// </summary>
public static class Mass
{
/// <summary>Какую долю собственной массы существо может нести (×способность). Человек (руки) — больше.</summary>
public const float CarryFraction = 0.35f;
/// <summary>Доля массы тела, превращающаяся в мясо трупа (для разделки/падальщиков).</summary>
public const float MeatFraction = 0.5f;
private static float Cube(float v) => v * v * v;
/// <summary>Масса особи-животного (кг): эталон вида × куб (текущий линейный размер / эталонный).</summary>
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);
}
/// <summary>Масса растения (кг): эталон вида × куб (размер текущей стадии / размер зрелой).</summary>
public static float OfPlant(PlantDef def, float stageSizeCells, float matureSizeCells)
{
var ratio = matureSizeCells > 0f ? stageSizeCells / matureSizeCells : 1f;
return def.BaseMassKg * Cube(ratio);
}
/// <summary>Грузоподъёмность (кг): доля массы тела × способность нести (манипуляция/движение, 0..1+).</summary>
public static float CarryCapacity(float bodyMassKg, float capability) =>
bodyMassKg * CarryFraction * MathF.Max(0f, capability);
/// <summary>
/// Метаболический множитель по закону Клайбера: расход энергии ∝ масса^0.75. Нормирован на эталонную
/// массу вида (взрослый базового размера = 1), поэтому крупные особи прожорливее, а молодняк — экономнее,
/// без межвидового взрыва баланса.
/// </summary>
public static float KleiberFactor(float massKg, float referenceMassKg)
{
if (referenceMassKg <= 0f || massKg <= 0f)
{
return 1f;
}
return MathF.Pow(massKg / referenceMassKg, 0.75f);
}
/// <summary>Множитель скорости от ноши: в пределах грузоподъёмности — 1; перегруз тормозит (до 0.25).</summary>
public static float LoadSpeedFactor(float carriedKg, float capacityKg)
{
if (capacityKg <= 0f || carriedKg <= capacityKg)
{
return 1f;
}
return MathF.Max(0.25f, capacityKg / carriedKg);
}
}
+18
View File
@@ -291,6 +291,14 @@ internal sealed class InspectPanel
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan)); 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 (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
var temperature = _climate.Temperature; var temperature = _climate.Temperature;
@@ -400,6 +408,16 @@ internal sealed class InspectPanel
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan)); text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan));
var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female"); var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female");
text.AppendLine(languages.Format("inspect.animal.sex", sex, org.Generation)); 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<Health>(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<Pregnant>(out var preg)) if (entity.TryGetComponent<Pregnant>(out var preg))
{ {
text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays)); text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays));