Животные A5 (часть 2): части тела, кровь, capacities

BodyDef/BodyPartDef + bodies.json (Quadruped — данные: иерархия органов, вклады в способности). AnimalDef.Body. HealthState расширен Parts/BloodLevel/Pain/Capacities; CapacityCalc обобщённо считает способности по зависимостям (кровь->сердце/лёгкие->сознание->движение/зрение), модовые проходят сырыми. Фабрика строит части (HP x размер тела) и начальные способности. Источников урона нет -> всё на полном HP, способности=1 (каркас под болезни/хищников). Команда body <species>.

Сборка чистая, --check-content (10 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 05:36:34 +03:00
co-authored by Claude Opus 4.8
parent c90bf32460
commit 9db12273c9
10 changed files with 272 additions and 5 deletions
+1
View File
@@ -12,6 +12,7 @@
"maleTexture": "things/pawn/animal/deer/DeerMale_east", "maleTexture": "things/pawn/animal/deer/DeerMale_east",
"babyTexture": "things/pawn/animal/deer/DeerBaby_east", "babyTexture": "things/pawn/animal/deer/DeerBaby_east",
"corpseTexture": "things/pawn/animal/deer/Dessicated_DeerFemale_east", "corpseTexture": "things/pawn/animal/deer/Dessicated_DeerFemale_east",
"body": "Quadruped",
"diet": ["plant"], "diet": ["plant"],
"spawnPer1000Cells": 2.5, "spawnPer1000Cells": 2.5,
"genome": { "genome": {
+34
View File
@@ -0,0 +1,34 @@
{
"type": "Body",
// Анатомия видов (фаза A5): дерево частей/органов. Вклады в способности (capacities) по телу
// суммируются ≈1 у здоровой особи. maxHp умножается на размер тела особи при создании.
// Источников урона пока нет — части на полном HP, способности = 1; каркас под болезни/хищников.
"defs": [
{
"defName": "Quadruped",
"parts": [
{ "name": "torso", "coverage": 0.32, "maxHp": 40, "vital": true },
{ "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "BloodPumping": 1.0 } },
{ "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } },
{ "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } },
{ "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true },
{ "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Digestion": 1.0 } },
{ "name": "head", "coverage": 0.1, "maxHp": 25 },
{ "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "Consciousness": 1.0 } },
{ "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } },
{ "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } },
{ "name": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legFrontRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legBackRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } }
]
}
]
}
+7 -1
View File
@@ -8,7 +8,13 @@
> труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` + > труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` +
> `corpseTexture` оленя (`Dessicated_*`, скелет — бледный тинт, своего спрайта пока нет). Продукты > `corpseTexture` оленя (`Dessicated_*`, скелет — бледный тинт, своего спрайта пока нет). Продукты
> `ProductMeat/Bone/Leather` в `products.json` (разделка/падальщики — позже). Сборка/линт чистые. > `ProductMeat/Bone/Leather` в `products.json` (разделка/падальщики — позже). Сборка/линт чистые.
> Осталось в A5: части тела (`BodyDef`), кровь, capacities (часть 2). > **A5 часть 2 (готово): части тела, кровь, capacities.** `BodyDef`/`BodyPartDef` + `bodies.json`
> (`Quadruped` — данные: иерархия органов, вклады в способности); `AnimalDef.Body`; `HealthState`
> расширен `Parts`/`BloodLevel`/`Pain`/`Capacities`; `CapacityCalc` (обобщённый расчёт по зависимостям
> кровь→сердце/лёгкие→сознание→движение/зрение, модовые способности — сырыми); фабрика строит части
> (HP × размер тела) и считает способности. Источников урона ещё нет → всё на полном HP, способности =
> 1 (каркас под болезни A6/хищников). Команда `body <species>`. Сборка/линт чистые (10 типов дефов).
> Осталось в A5: смерть через здоровье (vital-часть/кровь) + истощение как hediff — когда появятся раны.
> >
> **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`, > **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`,
> компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`; > компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`;
+4
View File
@@ -33,6 +33,9 @@ public sealed class AnimalSet
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary> /// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
public required AnimalStageDef[] Stages { get; init; } public required AnimalStageDef[] Stages { get; init; }
/// <summary>Анатомия вида (части тела) или null, если вид без тела.</summary>
public required BodyDef? Body { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary> /// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
public required GenomeTemplate Template { get; init; } public required GenomeTemplate Template { get; init; }
} }
@@ -80,6 +83,7 @@ public sealed class AnimalSet
? female ? female
: atlases.GetRegion(device, def.CorpseTexture), : atlases.GetRegion(device, def.CorpseTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
Template = BuildTemplate(def.Genome, genes), Template = BuildTemplate(def.Genome, genes),
}; };
_index[def] = i; _index[def] = i;
+1
View File
@@ -66,6 +66,7 @@ public sealed class GameContent
defs.RegisterType<AnimalDef>("Animal"); defs.RegisterType<AnimalDef>("Animal");
defs.RegisterType<HediffDef>("Hediff"); defs.RegisterType<HediffDef>("Hediff");
defs.RegisterType<NeedDef>("Need"); defs.RegisterType<NeedDef>("Need");
defs.RegisterType<BodyDef>("Body");
defs.RegisterType<WorldPresetDef>("WorldPreset"); defs.RegisterType<WorldPresetDef>("WorldPreset");
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
+39
View File
@@ -252,6 +252,9 @@ public sealed class AnimalDef : PawnDef
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary> /// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string CorpseTexture { get; init; } = ""; public string CorpseTexture { get; init; } = "";
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
public string Body { get; init; } = "";
/// <summary> /// <summary>
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение — /// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки /// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
@@ -317,6 +320,42 @@ public sealed class NeedDef : Def
public float MinBrain { get; init; } public float MinBrain { get; init; }
} }
/// <summary>
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
/// способности (capacity id → доля). Модер задаёт любую анатомию (8 ног, два сердца) без правки кода.
/// </summary>
public sealed class BodyPartDef
{
/// <summary>Имя части (уникально в теле); на него ссылается <see cref="Parent"/> детей.</summary>
public string Name { get; init; } = "";
/// <summary>Имя родительской части ("" — корневая, напр. торс).</summary>
public string Parent { get; init; } = "";
/// <summary>Вес попадания (доля тела) — для будущего выбора задетой части.</summary>
public float Coverage { get; init; } = 0.1f;
/// <summary>Базовый максимум HP (× размер тела при создании особи).</summary>
public float MaxHp { get; init; } = 10f;
/// <summary>Жизненно важная: уничтожение → смерть.</summary>
public bool Vital { get; init; }
/// <summary>Вклады в способности: capacity id (см. <c>AnimalCapacities</c>) → доля (сумма по телу ≈ 1).</summary>
public Dictionary<string, float> Capacities { get; init; } = new();
}
/// <summary>
/// Анатомия вида (Defs/bodies.json): дерево частей тела/органов. Используется системой здоровья (фаза
/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities).
/// </summary>
public sealed class BodyDef : Def
{
/// <summary>Части тела/органы вида.</summary>
public BodyPartDef[] Parts { get; init; } = [];
}
/// <summary> /// <summary>
/// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты. /// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты.
/// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый <see cref="Def.Label"/>; стадии, /// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый <see cref="Def.Label"/>; стадии,
+47
View File
@@ -709,6 +709,11 @@ public sealed class WorldScene : Scene
"popstats [species] — live population trait means and generation span (selection drift)", "popstats [species] — live population trait means and generation span (selection drift)",
(c, args) => RunPopStats(c, args) (c, args) => RunPopStats(c, args)
); );
console.Register(
"body",
"body <species> — dump a species body parts and a healthy individual's capacities",
(c, args) => RunBodyDemo(c, content, args)
);
console.Register( console.Register(
"menu", "menu",
"menu — return to the main menu", "menu — return to the main menu",
@@ -913,6 +918,48 @@ public sealed class WorldScene : Scene
} }
} }
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
private void RunBodyDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Animal");
if (args.Length == 0)
{
console.WriteLine("usage: body <species> e.g. body Deer");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
{
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
if (string.IsNullOrEmpty(def.Body) || !content.Defs.TryGet<BodyDef>(def.Body, out var body))
{
console.WriteLine($"{def.DefName}: no body");
return;
}
console.WriteLine($"{def.DefName} body '{body.DefName}': {body.Parts.Length} parts");
foreach (var part in body.Parts)
{
var caps = part.Capacities.Count > 0
? " [" + string.Join(", ", part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")) + "]"
: "";
console.WriteLine($" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}");
}
var state = HealthState.Create(body, 1f);
console.WriteLine(
"capacities (healthy): "
+ string.Join(
", ",
state.Capacities.OrderBy(k => k.Key).Select(kv => $"{kv.Key} {kv.Value:0.##}")
)
);
}
private static string ProductLabel(GameContent content, string productDefName) => private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product) content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label) ? content.Languages.Get(product.Label)
+1 -1
View File
@@ -61,7 +61,7 @@ public static class AnimalFactory
TargetPlant = -1, TargetPlant = -1,
TargetMate = -1, TargetMate = -1,
}, },
new Health { State = new HealthState() } new Health { State = HealthState.Create(sp.Body, traits.BodySize) }
); );
} }
+66 -3
View File
@@ -113,10 +113,27 @@ public struct AnimalGrowth : IComponent
public float AgeDays; public float AgeDays;
} }
/// <summary>Инстанс части тела особи: ссылка на деф части и текущий/максимальный HP (масштаб по размеру).</summary>
public struct PartInstance
{
/// <summary>Деф части тела (анатомия вида).</summary>
public Content.BodyPartDef Def;
/// <summary>Текущий HP.</summary>
public float Hp;
/// <summary>Максимальный HP (база × размер тела).</summary>
public float MaxHp;
/// <summary>Доля сохранности [0,1].</summary>
public readonly float Fraction => MaxHp > 0f ? Math.Clamp(Hp / MaxHp, 0f, 1f) : 0f;
}
/// <summary> /// <summary>
/// Состояние здоровья особи (фаза A4 — лёгкий каркас): список активных хедифов. Managed-объект, как /// Состояние здоровья особи (фаза A4A5): список активных хедифов + части тела, уровень крови и
/// <see cref="Genome"/> (Friflo допускает ссылку в компоненте). В A5/A6 прирастёт частями тела, /// вычисленные способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает
/// кровью, capacities и иммунитетом — контейнер один, растёт по фазам. /// ссылку в компоненте). Источников урона пока нет — части на полном HP, способности = 1; каркас под
/// болезни (A6) и хищников (горизонт). Растёт по фазам — контейнер один.
/// </summary> /// </summary>
public sealed class HealthState public sealed class HealthState
{ {
@@ -125,6 +142,52 @@ public sealed class HealthState
/// <summary>Активные хедифы (гон, позже — раны/болезни).</summary> /// <summary>Активные хедифы (гон, позже — раны/болезни).</summary>
public IReadOnlyList<HediffDef> Hediffs => _hediffs; public IReadOnlyList<HediffDef> Hediffs => _hediffs;
/// <summary>Части тела особи (из анатомии вида); пусто — у вида нет тела.</summary>
public PartInstance[] Parts { get; private set; } = [];
/// <summary>Уровень крови [0,1]; ниже порога — потеря сознания и смерть (когда появятся раны).</summary>
public float BloodLevel { get; set; } = 1f;
/// <summary>Боль [0,1] — снижает сознание (когда появятся раны).</summary>
public float Pain { get; set; }
/// <summary>Погибла ли особь по здоровью (vital-часть уничтожена / кровь на нуле).</summary>
public bool Dead { get; set; }
/// <summary>Вычисленные способности (capacity id → [0,1]); у здоровой особи все = 1.</summary>
public Dictionary<string, float> Capacities { get; private set; } = new();
/// <summary>Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий.</summary>
public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f;
/// <summary>Пересчитывает способности из текущих частей/крови/боли.</summary>
public void RecomputeCapacities() =>
Capacities = CapacityCalc.Compute(Parts, BloodLevel, Pain);
/// <summary>Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности.</summary>
public static HealthState Create(Content.BodyDef? body, float bodySize)
{
var state = new HealthState();
if (body is { Parts.Length: > 0 })
{
var scale = MathF.Max(0.2f, bodySize);
state.Parts = new PartInstance[body.Parts.Length];
for (var i = 0; i < body.Parts.Length; i++)
{
var hp = body.Parts[i].MaxHp * scale;
state.Parts[i] = new PartInstance
{
Def = body.Parts[i],
Hp = hp,
MaxHp = hp,
};
}
}
state.RecomputeCapacities();
return state;
}
/// <summary>Несёт ли особь хедиф с данным именем дефа.</summary> /// <summary>Несёт ли особь хедиф с данным именем дефа.</summary>
public bool Has(string defName) public bool Has(string defName)
{ {
+72
View File
@@ -0,0 +1,72 @@
using System.Collections.Generic;
namespace LittleSim.Sim;
/// <summary>Идентификаторы способностей (capacities) организма — на них ссылаются вклады частей тела.</summary>
public static class AnimalCapacities
{
public const string Consciousness = "Consciousness";
public const string Moving = "Moving";
public const string Breathing = "Breathing";
public const string BloodPumping = "BloodPumping";
public const string Sight = "Sight";
public const string Digestion = "Digestion";
}
/// <summary>
/// Обобщённый расчёт способностей (capacities) из частей тела и крови (фаза A5). Сырой вклад способности
/// = Σ(доля части × доля её HP). Затем известные способности домножаются по зависимостям (кровь →
/// сердце/лёгкие → сознание → движение/зрение), как в модели RimWorld; неизвестные (модовые) проходят
/// сырыми. У здоровой особи (полное HP, кровь 1, боль 0) все способности = 1.
/// </summary>
public static class CapacityCalc
{
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary>
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain)
{
var raw = new Dictionary<string, float>(System.StringComparer.Ordinal);
foreach (var part in parts)
{
if (part.Def is null)
{
continue;
}
var fraction = part.Fraction;
foreach (var (capacity, contribution) in part.Def.Capacities)
{
raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction;
}
}
float Raw(string id) => raw.GetValueOrDefault(id);
static float Clamp01(float v) => v < 0f ? 0f : v > 1f ? 1f : v;
var bloodLevel = Clamp01(blood);
var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel;
var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel;
var consciousness =
Raw(AnimalCapacities.Consciousness)
* Clamp01(bloodPumping)
* Clamp01(breathing)
* (1f - Clamp01(pain));
var result = new Dictionary<string, float>(System.StringComparer.Ordinal)
{
[AnimalCapacities.BloodPumping] = bloodPumping,
[AnimalCapacities.Breathing] = breathing,
[AnimalCapacities.Consciousness] = consciousness,
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping),
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping),
};
// Модовые способности, не охваченные зависимостями, — сырыми.
foreach (var (capacity, value) in raw)
{
result.TryAdd(capacity, value);
}
return result;
}
}