diff --git a/Mods/Core/Defs/animals.json b/Mods/Core/Defs/animals.json index df6f5b0..d899015 100644 --- a/Mods/Core/Defs/animals.json +++ b/Mods/Core/Defs/animals.json @@ -12,6 +12,7 @@ "maleTexture": "things/pawn/animal/deer/DeerMale_east", "babyTexture": "things/pawn/animal/deer/DeerBaby_east", "corpseTexture": "things/pawn/animal/deer/Dessicated_DeerFemale_east", + "body": "Quadruped", "diet": ["plant"], "spawnPer1000Cells": 2.5, "genome": { diff --git a/Mods/Core/Defs/bodies.json b/Mods/Core/Defs/bodies.json new file mode 100644 index 0000000..88c1e06 --- /dev/null +++ b/Mods/Core/Defs/bodies.json @@ -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 } } + ] + } + ] +} diff --git a/docs/животные.md b/docs/животные.md index e0d8236..3c5ded9 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -8,7 +8,13 @@ > труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` + > `corpseTexture` оленя (`Dessicated_*`, скелет — бледный тинт, своего спрайта пока нет). Продукты > `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 `. Сборка/линт чистые (10 типов дефов). +> Осталось в A5: смерть через здоровье (vital-часть/кровь) + истощение как hediff — когда появятся раны. > > **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`, > компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`; diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index a5a353a..6382510 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -33,6 +33,9 @@ public sealed class AnimalSet /// Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа. public required AnimalStageDef[] Stages { get; init; } + /// Анатомия вида (части тела) или null, если вид без тела. + public required BodyDef? Body { get; init; } + /// Набор генов вида: базовые значения для генерации особи. 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(def.Body, out var body) ? body : null, Template = BuildTemplate(def.Genome, genes), }; _index[def] = i; diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index 63af0d0..ee594b3 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -66,6 +66,7 @@ public sealed class GameContent defs.RegisterType("Animal"); defs.RegisterType("Hediff"); defs.RegisterType("Need"); + defs.RegisterType("Body"); defs.RegisterType("WorldPreset"); // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 36e7da3..7f359eb 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -252,6 +252,9 @@ public sealed class AnimalDef : PawnDef /// Текстура трупа (фаза A5); пусто — берётся . public string CorpseTexture { get; init; } = ""; + /// Имя — анатомия вида (части тела/органы); пусто — без частей тела. + public string Body { get; init; } = ""; + /// /// Геном вида как ДАННЫЕ: ключ — id гена (), значение — /// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки @@ -317,6 +320,42 @@ public sealed class NeedDef : Def public float MinBrain { get; init; } } +/// +/// Часть тела/орган (вложенный объект ) как ДАННЫЕ: иерархия (родитель), +/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в +/// способности (capacity id → доля). Модер задаёт любую анатомию (8 ног, два сердца) без правки кода. +/// +public sealed class BodyPartDef +{ + /// Имя части (уникально в теле); на него ссылается детей. + public string Name { get; init; } = ""; + + /// Имя родительской части ("" — корневая, напр. торс). + public string Parent { get; init; } = ""; + + /// Вес попадания (доля тела) — для будущего выбора задетой части. + public float Coverage { get; init; } = 0.1f; + + /// Базовый максимум HP (× размер тела при создании особи). + public float MaxHp { get; init; } = 10f; + + /// Жизненно важная: уничтожение → смерть. + public bool Vital { get; init; } + + /// Вклады в способности: capacity id (см. AnimalCapacities) → доля (сумма по телу ≈ 1). + public Dictionary Capacities { get; init; } = new(); +} + +/// +/// Анатомия вида (Defs/bodies.json): дерево частей тела/органов. Используется системой здоровья (фаза +/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities). +/// +public sealed class BodyDef : Def +{ + /// Части тела/органы вида. + public BodyPartDef[] Parts { get; init; } = []; +} + /// /// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты. /// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый ; стадии, diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index f4e5b41..9a25dfa 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -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 — 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 e.g. body Deer"); + console.WriteLine($"species: {string.Join(", ", species)}"); + return; + } + + if (!content.Defs.TryGet(args[0], out var def)) + { + console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}"); + return; + } + + if (string.IsNullOrEmpty(def.Body) || !content.Defs.TryGet(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(productDefName, out var product) ? content.Languages.Get(product.Label) diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 9dd6258..b8e8383 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -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) } ); } diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 061709f..244da8d 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -113,10 +113,27 @@ public struct AnimalGrowth : IComponent public float AgeDays; } +/// Инстанс части тела особи: ссылка на деф части и текущий/максимальный HP (масштаб по размеру). +public struct PartInstance +{ + /// Деф части тела (анатомия вида). + public Content.BodyPartDef Def; + + /// Текущий HP. + public float Hp; + + /// Максимальный HP (база × размер тела). + public float MaxHp; + + /// Доля сохранности [0,1]. + public readonly float Fraction => MaxHp > 0f ? Math.Clamp(Hp / MaxHp, 0f, 1f) : 0f; +} + /// -/// Состояние здоровья особи (фаза A4 — лёгкий каркас): список активных хедифов. Managed-объект, как -/// (Friflo допускает ссылку в компоненте). В A5/A6 прирастёт частями тела, -/// кровью, capacities и иммунитетом — контейнер один, растёт по фазам. +/// Состояние здоровья особи (фаза A4–A5): список активных хедифов + части тела, уровень крови и +/// вычисленные способности (capacities). Managed-объект, как (Friflo допускает +/// ссылку в компоненте). Источников урона пока нет — части на полном HP, способности = 1; каркас под +/// болезни (A6) и хищников (горизонт). Растёт по фазам — контейнер один. /// public sealed class HealthState { @@ -125,6 +142,52 @@ public sealed class HealthState /// Активные хедифы (гон, позже — раны/болезни). public IReadOnlyList Hediffs => _hediffs; + /// Части тела особи (из анатомии вида); пусто — у вида нет тела. + public PartInstance[] Parts { get; private set; } = []; + + /// Уровень крови [0,1]; ниже порога — потеря сознания и смерть (когда появятся раны). + public float BloodLevel { get; set; } = 1f; + + /// Боль [0,1] — снижает сознание (когда появятся раны). + public float Pain { get; set; } + + /// Погибла ли особь по здоровью (vital-часть уничтожена / кровь на нуле). + public bool Dead { get; set; } + + /// Вычисленные способности (capacity id → [0,1]); у здоровой особи все = 1. + public Dictionary Capacities { get; private set; } = new(); + + /// Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий. + public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f; + + /// Пересчитывает способности из текущих частей/крови/боли. + public void RecomputeCapacities() => + Capacities = CapacityCalc.Compute(Parts, BloodLevel, Pain); + + /// Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности. + 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; + } + /// Несёт ли особь хедиф с данным именем дефа. public bool Has(string defName) { diff --git a/src/LittleSim/Sim/CapacityCalc.cs b/src/LittleSim/Sim/CapacityCalc.cs new file mode 100644 index 0000000..87a1ed9 --- /dev/null +++ b/src/LittleSim/Sim/CapacityCalc.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace LittleSim.Sim; + +/// Идентификаторы способностей (capacities) организма — на них ссылаются вклады частей тела. +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"; +} + +/// +/// Обобщённый расчёт способностей (capacities) из частей тела и крови (фаза A5). Сырой вклад способности +/// = Σ(доля части × доля её HP). Затем известные способности домножаются по зависимостям (кровь → +/// сердце/лёгкие → сознание → движение/зрение), как в модели RimWorld; неизвестные (модовые) проходят +/// сырыми. У здоровой особи (полное HP, кровь 1, боль 0) все способности = 1. +/// +public static class CapacityCalc +{ + /// Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1). + public static Dictionary Compute(PartInstance[] parts, float blood, float pain) + { + var raw = new Dictionary(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(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; + } +}