Животные 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:
co-authored by
Claude Opus 4.8
parent
c90bf32460
commit
9db12273c9
@@ -33,6 +33,9 @@ public sealed class AnimalSet
|
||||
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
|
||||
public required AnimalStageDef[] Stages { get; init; }
|
||||
|
||||
/// <summary>Анатомия вида (части тела) или null, если вид без тела.</summary>
|
||||
public required BodyDef? Body { get; init; }
|
||||
|
||||
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
|
||||
public required GenomeTemplate Template { get; init; }
|
||||
}
|
||||
@@ -80,6 +83,7 @@ public sealed class AnimalSet
|
||||
? female
|
||||
: atlases.GetRegion(device, def.CorpseTexture),
|
||||
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),
|
||||
};
|
||||
_index[def] = i;
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class GameContent
|
||||
defs.RegisterType<AnimalDef>("Animal");
|
||||
defs.RegisterType<HediffDef>("Hediff");
|
||||
defs.RegisterType<NeedDef>("Need");
|
||||
defs.RegisterType<BodyDef>("Body");
|
||||
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
||||
|
||||
@@ -252,6 +252,9 @@ public sealed class AnimalDef : PawnDef
|
||||
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||
public string CorpseTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
|
||||
public string Body { get; init; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
|
||||
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
|
||||
@@ -317,6 +320,42 @@ public sealed class NeedDef : Def
|
||||
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>
|
||||
/// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты.
|
||||
/// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый <see cref="Def.Label"/>; стадии,
|
||||
|
||||
@@ -709,6 +709,11 @@ public sealed class WorldScene : Scene
|
||||
"popstats [species] — live population trait means and generation span (selection drift)",
|
||||
(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(
|
||||
"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) =>
|
||||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||
? content.Languages.Get(product.Label)
|
||||
|
||||
@@ -61,7 +61,7 @@ public static class AnimalFactory
|
||||
TargetPlant = -1,
|
||||
TargetMate = -1,
|
||||
},
|
||||
new Health { State = new HealthState() }
|
||||
new Health { State = HealthState.Create(sp.Body, traits.BodySize) }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -113,10 +113,27 @@ public struct AnimalGrowth : IComponent
|
||||
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>
|
||||
/// Состояние здоровья особи (фаза A4 — лёгкий каркас): список активных хедифов. Managed-объект, как
|
||||
/// <see cref="Genome"/> (Friflo допускает ссылку в компоненте). В A5/A6 прирастёт частями тела,
|
||||
/// кровью, capacities и иммунитетом — контейнер один, растёт по фазам.
|
||||
/// Состояние здоровья особи (фаза A4–A5): список активных хедифов + части тела, уровень крови и
|
||||
/// вычисленные способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает
|
||||
/// ссылку в компоненте). Источников урона пока нет — части на полном HP, способности = 1; каркас под
|
||||
/// болезни (A6) и хищников (горизонт). Растёт по фазам — контейнер один.
|
||||
/// </summary>
|
||||
public sealed class HealthState
|
||||
{
|
||||
@@ -125,6 +142,52 @@ public sealed class HealthState
|
||||
/// <summary>Активные хедифы (гон, позже — раны/болезни).</summary>
|
||||
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>
|
||||
public bool Has(string defName)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user