Weight system: body mass, carry capacity, Kleiber metabolism
Mass is derived by a cubic law: mass = BaseMassKg * (size / reference)^3, so a juvenile/seedling weighs far less than an adult/mature and the size gene moves mass too. New Mass helper + BaseMassKg on AnimalDef/PlantDef (deer 70, wolf 45, boar 80, chicken 2.5; trees ~450, grass 0.4, etc.) and MassKg per unit on ProductDef (foundation for hauling/inventory). Wired into the sim (as chosen): - Corpse meat is now proportional to body mass (Mass.MeatFraction), not raw bodySize; Corpse gained a BodySize field so sprite scale stays independent of meat (save/load updated). - Kleiber metabolism: need decay x= (mass/refMass)^0.75, normalized per species, so juveniles are cheaper to feed and heavier individuals eat a bit more, without cross-species balance blowups (adults of typical size are unchanged). - Carry capacity = mass x fraction x carry capability (Moving for quadrupeds; Manipulation for future humans), shown in inspector/console. A dormant Carrier component + Mass.LoadSpeedFactor hook slow an overloaded animal — the foundation for item hauling. Inspector shows body/plant weight (+carry); `animal`/`plant` console print mass. Build + --check-content clean (42 genes, 41 traits). Not GUI-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0e2cf55a54
commit
1a3f205f86
@@ -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