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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "созрело",
|
||||
|
||||
@@ -75,6 +75,9 @@ public sealed class ProductDef : Def
|
||||
{
|
||||
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
|
||||
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
|
||||
public float MassKg { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,6 +180,9 @@ public sealed class PlantDef : Def
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||
public string? FruitProduct { get; init; }
|
||||
|
||||
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
|
||||
public float BaseMassKg { get; init; } = 20f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -264,6 +270,9 @@ public sealed class AnimalDef : PawnDef
|
||||
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
|
||||
public string EggTexture { get; init; } = "things/plant/seed_default";
|
||||
|
||||
/// <summary>Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³.</summary>
|
||||
public float BaseMassKg { get; init; } = 40f;
|
||||
|
||||
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
|
||||
public string Body { get; init; } = "";
|
||||
|
||||
|
||||
@@ -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<HediffDef>("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<Corpse>();
|
||||
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,
|
||||
|
||||
@@ -75,19 +75,18 @@ public static class AnimalFactory
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков).</summary>
|
||||
public const float MeatPerBodySize = 25f;
|
||||
|
||||
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
|
||||
private static readonly Color CorpseFreshTint = new(176, 148, 128);
|
||||
|
||||
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).</summary>
|
||||
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и
|
||||
/// запас мяса <paramref name="meatKg"/> (∝ масса тела, считается вызывающим через <see cref="Mass"/>).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Гибель особи: оставляет труп (мясо ∝ размер тела) и удаляет сущность. Общий путь смерти.</summary>
|
||||
/// <summary>Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти.</summary>
|
||||
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
|
||||
{
|
||||
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
|
||||
@@ -194,7 +194,12 @@ public static class AnimalFactory
|
||||
|
||||
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -464,8 +464,11 @@ public struct Corpse : IComponent
|
||||
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
|
||||
public int Stage;
|
||||
|
||||
/// <summary>Доступное мясо (∝ размер тела) — для разделки/падальщиков (фаза-горизонт).</summary>
|
||||
/// <summary>Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт).</summary>
|
||||
public float Meat;
|
||||
|
||||
/// <summary>Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса.</summary>
|
||||
public float BodySize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -488,6 +491,17 @@ public struct Egg : IComponent
|
||||
public float IncubateDays;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
|
||||
/// сверх грузоподъёмности (<see cref="Sim.Mass.CarryCapacity"/>) замедляет движение. Компонент появляется
|
||||
/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь.
|
||||
/// </summary>
|
||||
public struct Carrier : IComponent
|
||||
{
|
||||
/// <summary>Суммарная масса переносимого груза (кг).</summary>
|
||||
public float CarriedKg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
|
||||
/// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) —
|
||||
|
||||
@@ -122,8 +122,12 @@ public readonly struct AnimalContext(float[] values, NeedSet needs, float intell
|
||||
/// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные
|
||||
/// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>.
|
||||
/// </summary>
|
||||
public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, NeedSet needs)
|
||||
: QuerySystem<AnimalNeeds, AnimalOrganism, Health>
|
||||
public sealed class AnimalNeedsSystem(
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
NeedSet needs,
|
||||
AnimalSet animals
|
||||
) : QuerySystem<AnimalNeeds, AnimalOrganism, Health, AnimalGrowth>
|
||||
{
|
||||
// Прибавка к обмену веществ при устойчивости к яду 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<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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<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))
|
||||
{
|
||||
text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays));
|
||||
|
||||
Reference in New Issue
Block a user