diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json
index d899015..0b10d67 100644
--- a/Mods/Core/Defs/Animals/Animals.json
+++ b/Mods/Core/Defs/Animals/Animals.json
@@ -1,8 +1,9 @@
{
"type": "Animal",
- // Виды-животные (фаза A1): подтип Pawn (kind=animal) + базовый геном вида из общих Gene-дефов
- // (см. genes.json). Особь при спавне получает аллели вокруг этих баз, фенотип — формулами генов.
- // Пол, стадии роста, размножение, здоровье и поведение приходят в фазах A2+.
+ // Виды-животные: подтип Pawn (kind=animal) + базовый геном вида из общих Gene-дефов (см. Defs/Genes/).
+ // Особь при спавне получает аллели вокруг этих баз, фенотип — формулами генов. Диета задаётся ГЕНАМИ
+ // (herbivory/carnivory/omnivory): олень — травоядный, волк — хищник, кабан — всеядный. Хищники несут
+ // боевые параметры (attack*) и охотятся на добычу не крупнее себя; жертвы убегают (см. AnimalSystems).
"defs": [
{
"defName": "Deer",
@@ -28,7 +29,75 @@
"GeneLifespan": 360,
"GeneBreedingSeason": 2,
"GeneGestationDays": 30,
- "GeneLitterSize": 1
+ "GeneLitterSize": 1,
+ "GeneHerbivory": 1.0
+ }
+ },
+
+ {
+ "defName": "Wolf",
+ "label": "pawn.wolf",
+ "kind": "animal",
+ "texture": "things/pawn/animal/wolf_timber/Wolf_Timber_east",
+ "maleTexture": "things/pawn/animal/wolf_timber/Wolf_Timber3_east",
+ "body": "Quadruped",
+ "diet": ["meat"],
+ "spawnPer1000Cells": 0.8,
+ "baseSpeed": 32,
+ "visionCells": 18,
+ "attackRangeCells": 1.3,
+ "attackDamage": 80,
+ "attackBleed": 3.0,
+ "attackBloodLoss": 5.0,
+ "genome": {
+ "GeneMaxBodySize": 1.6,
+ "GeneMetabolism": 1.1,
+ "GeneMoveSpeed": 1.4,
+ "GeneBloodVolume": 1.3,
+ "GeneVision": 1.5,
+ "GeneBrainSize": 0.5,
+ "GeneInsulation": 0.6,
+ "GeneFurColor": 0.7,
+ "GeneMaturityAge": 70,
+ "GeneLifespan": 240,
+ "GeneBreedingSeason": 3,
+ "GeneGestationDays": 35,
+ "GeneLitterSize": 4,
+ "GeneCarnivory": 1.0
+ }
+ },
+
+ {
+ "defName": "WildBoar",
+ "label": "pawn.boar",
+ "kind": "animal",
+ "texture": "things/pawn/animal/wildboar/WildBoar_east",
+ "body": "Quadruped",
+ "diet": ["plant", "meat"],
+ "spawnPer1000Cells": 1.0,
+ "baseSpeed": 26,
+ "visionCells": 13,
+ "attackRangeCells": 1.2,
+ "attackDamage": 45,
+ "attackBleed": 1.8,
+ "attackBloodLoss": 3.0,
+ "genome": {
+ "GeneMaxBodySize": 1.3,
+ "GeneMetabolism": 1.2,
+ "GeneMoveSpeed": 1.0,
+ "GeneBloodVolume": 1.2,
+ "GeneVision": 1.0,
+ "GeneBrainSize": 0.42,
+ "GeneInsulation": 0.5,
+ "GeneFurColor": 0.85,
+ "GeneMaturityAge": 80,
+ "GeneLifespan": 280,
+ "GeneBreedingSeason": 0,
+ "GeneGestationDays": 28,
+ "GeneLitterSize": 5,
+ "GeneHerbivory": 0.4,
+ "GeneCarnivory": 0.2,
+ "GeneOmnivory": 0.85
}
}
]
diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json
index ce91513..452e3ef 100644
--- a/Mods/Core/Defs/Genes/Animal.json
+++ b/Mods/Core/Defs/Genes/Animal.json
@@ -31,6 +31,21 @@
"default": 90, "min": 1, "max": 400, "tags": ["animal", "lifecycle"],
"effects": { "maturityAge": "value" } },
+ // --- Диета как ГЕНЫ (хищничество/травоядство/всеядство) ---
+ // Каждый ген выражает свой признак [0..1]; рацион выводится порогами: ест растения, если
+ // max(herbivory, omnivory) ≥ порога; ест мясо (охота/падаль), если max(carnivory, omnivory) ≥ порога.
+ // Поэтому всеядность — отдельный ген, который включает ОБА источника пищи (генералист), а
+ // специалисты задаются высоким herbivory ЛИБО carnivory. См. AnimalFactory.Diet.
+ { "defName": "GeneHerbivory", "parent": "BaseNumericGene", "label": "gene.herbivory",
+ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
+ "effects": { "herbivory": "value" } },
+ { "defName": "GeneCarnivory", "parent": "BaseNumericGene", "label": "gene.carnivory",
+ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
+ "effects": { "carnivory": "value" } },
+ { "defName": "GeneOmnivory", "parent": "BaseNumericGene", "label": "gene.omnivory",
+ "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
+ "effects": { "omnivory": "value" } },
+
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
diff --git a/Mods/Core/Defs/Hediffs/Hediffs.json b/Mods/Core/Defs/Hediffs/Hediffs.json
index 5505d67..00b001e 100644
--- a/Mods/Core/Defs/Hediffs/Hediffs.json
+++ b/Mods/Core/Defs/Hediffs/Hediffs.json
@@ -12,6 +12,16 @@
"initialSeverity": 0.05, "severityPerDay": 0.25, "immunityPerDay": 0.32,
"lethalSeverity": 1.0, "pain": 0.3, "ambientPerDay": 0.03,
"capMods": { "Moving": 0.6, "Consciousness": 0.85, "Digestion": 0.8 }
+ },
+
+ // Рана/кровотечение (предатор-кластер): первый травматический урон. Не прогрессирует сама
+ // (severityPerDay 0), но immunityPerDay её «сворачивает» (рана заживает ~за 2 дня). Пока активна —
+ // теряется кровь (bloodLossPerDay × тяжесть); кровь на нуле = смерть. Боль и капмоды от тяжёлых ран.
+ // Каждый укус усиливает тяжесть и сбрасывает заживление (см. HealthState.Intensify).
+ {
+ "defName": "Bleeding", "label": "hediff.bleeding",
+ "initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8,
+ "pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 }
}
]
}
diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index d4cc4ee..5f1abda 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -63,6 +63,7 @@
"cap.sight": "sight",
"hediff.rut": "rut",
"hediff.fever": "fever",
+ "hediff.bleeding": "bleeding",
"inspect.hint": "LMB — select · RMB/Esc — clear",
"hud.paused": "PAUSED",
"menu.title": "LittleSim",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index bd45d20..0c6d660 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -63,6 +63,7 @@
"cap.sight": "зрение",
"hediff.rut": "гон",
"hediff.fever": "лихорадка",
+ "hediff.bleeding": "кровотечение",
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
"hud.paused": "ПАУЗА",
"menu.title": "LittleSim",
diff --git a/docs/животные.md b/docs/животные.md
index 8acaca9..3393585 100644
--- a/docs/животные.md
+++ b/docs/животные.md
@@ -529,6 +529,24 @@ CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип = к
То, ради чего вся глубина: превращает «зоопарк» в живую экосистему. Каждая фича опирается
на уже построенные системы.
+> **ГОТОВО — хищный кластер (часть 1): диета-гены + охота + урон + бегство.** Диета теперь
+> ДАННЫЕ-через-ГЕНЫ: `GeneHerbivory`/`GeneCarnivory`/`GeneOmnivory` → признаки
+> `herbivory/carnivory/omnivory`; рацион выводит `AnimalFactory.Diet` (ест растения, если
+> max(herb,omni) ≥ 0.5; ест мясо, если max(carn,omni) ≥ 0.5 — всеядность включает оба; откат
+> на `AnimalDef.Diet` у видов без генов диеты). Новые виды: **Wolf** (хищник, охотится) и
+> **WildBoar** (всеядный); у оленя добавлен `GeneHerbivory`. Действие `Hunt`
+> (`AnimalDecisionSystem` ищет ближайший труп-падаль ИЛИ живую добычу не крупнее себя;
+> `AnimalActionSystem.Hunt` ведёт к цели и при контакте кусает или ест труп). **Первый
+> травматический урон**: `HealthState.ApplyInjury` (урон части тела по весу попадания + острая
+> кровопотеря + хедиф `Bleeding`); `Bleeding` (`bloodLossPerDay`) точит кровь в
+> `AnimalHealthSystem` (+восстановление крови в покое), кровь на нуле → смерть → труп → падаль.
+> Боевые параметры — данные на `AnimalDef` (`attackDamage/attackBleed/attackBloodLoss/
+> attackRangeCells`). **Бегство**: `AnimalActions.Flee` — жертва, заметившая хищника не мельче
+> себя в радиусе восприятия, убегает (приоритет над нуждами); сам хищник бежит лишь от заметно
+> более крупного мясоеда. Общий путь смерти — `AnimalFactory.Die`. Консоль: `diet [species]`;
+> `--check-content` печатает роль каждого вида. **Не покрыто (горизонт):** падальщики как
+> отдельная роль, удар по настроению от страха, снижение страха стадом, спрайт скелета.
+
- **Хищный кластер.** Хищничество (волк→олень): диета = живая добыча; действие `Hunt`
(выследить → преследовать → атака → **урон через здоровье A5** → добить → съесть труп).
Это **первый «активный» (травмирующий) источник урона** для системы здоровья и
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 66e010a..436545e 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -285,6 +285,18 @@ public sealed class AnimalDef : PawnDef
/// Сила выедания: на сколько игровых дней роста убавляется растение за день кормёжки.
public float ForageBiteDays { get; init; } = 40f;
+
+ /// Дальность атаки в клетках: на этом расстоянии хищник кусает добычу (контакт).
+ public float AttackRangeCells { get; init; } = 1.2f;
+
+ /// Урон части тела добычи за игровой день укусов (0 — вид не атакует, не хищник по бою).
+ public float AttackDamage { get; init; }
+
+ /// Прирост тяжести раны-кровотечения у добычи за игровой день укусов.
+ public float AttackBleed { get; init; }
+
+ /// Острая кровопотеря добычи за игровой день укусов (доля объёма крови) — основной путь к смерти.
+ public float AttackBloodLoss { get; init; }
}
///
@@ -397,6 +409,12 @@ public sealed class HediffDef : Def
/// Шанс подхватить фоном за день (0 — не фоновая болезнь).
public float AmbientPerDay { get; init; }
+ ///
+ /// Кровопотеря за игровой день при тяжести 1 (масштабируется тяжестью): доля объёма крови, которую
+ /// рана теряет в сутки. 0 — хедиф не вызывает кровотечения. Кровь на нуле → смерть (см. система здоровья).
+ ///
+ public float BloodLossPerDay { get; init; }
+
/// Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).
public Dictionary CapMods { get; init; } = new();
}
diff --git a/src/LittleSim/Program.cs b/src/LittleSim/Program.cs
index 2e8d078..350680d 100644
--- a/src/LittleSim/Program.cs
+++ b/src/LittleSim/Program.cs
@@ -1,6 +1,7 @@
using LittleSim.Content;
using LittleSim.Net;
using LittleSim.Scenes;
+using LittleSim.Sim;
using Microsoft.Xna.Framework;
using MrGameEng.Genetics;
using MrGameEng.Host;
@@ -20,6 +21,36 @@ if (args.Contains("--check-content"))
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
);
+
+ // Диета животных из генов (предатор-кластер): для каждого вида строим центральный геном из его
+ // баз, считаем признаки формулами и выводим рацион (травоядное/хищник/всеядное) — проверка без
+ // GPU, что хищники заданы и роль выводится из генов herbivory/carnivory/omnivory.
+ foreach (var def in content.Defs.All())
+ {
+ var entries = new List();
+ foreach (var (geneId, baseValue) in def.Genome)
+ {
+ if (registry.TryGetValue(geneId, out var gene))
+ {
+ var spread = gene.Kind == GeneKind.Discrete ? 0f : gene.Spread;
+ entries.Add(new GenomeTemplate.Entry(gene, baseValue, spread));
+ }
+ }
+
+ var genome = new GenomeTemplate(entries).Generate(new Random(7));
+ var t = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, registry));
+ var (eatsPlants, eatsMeat) = AnimalFactory.Diet(t, def);
+ var role =
+ eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
+ : eatsPlants ? "herbivore"
+ : "none";
+ var hunt = eatsMeat && def.AttackDamage > 0f ? $", attack {def.AttackDamage:0}/d" : "";
+ Console.WriteLine(
+ $" {def.DefName}: {role} (herb {t.Herbivory:0.##}/carn {t.Carnivory:0.##}/omni {t.Omnivory:0.##}),"
+ + $" spawn {def.SpawnPer1000Cells:0.##}/1000{hunt}"
+ );
+ }
+
return;
}
catch (Exception error)
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index b47bb86..700ae0b 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -259,6 +259,7 @@ public sealed class WorldScene : Scene
_config.Seed + 0x5EED
)
);
+ var bleeding = content.Defs.TryGet("Bleeding", out var bl) ? bl : null;
UpdateSystems.Add(
new AnimalActionSystem(
Store,
@@ -268,7 +269,10 @@ public sealed class WorldScene : Scene
thoughts,
Context.Clock,
SecondsPerDay,
- _bounds
+ CellSize,
+ _bounds,
+ bleeding,
+ _config.Seed + 0x1B17
)
);
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
@@ -952,6 +956,11 @@ public sealed class WorldScene : Scene
"animal [seed] — sample an animal genome and show its gene-driven traits",
(c, args) => RunAnimalDemo(c, content, args)
);
+ console.Register(
+ "diet",
+ "diet [species] — gene-derived diet (herbivory/carnivory/omnivory), role and hunting stats",
+ (c, args) => RunDiet(c, content, args)
+ );
console.Register(
"popstats",
"popstats [species] — live population trait means and generation span (selection drift)",
@@ -1119,6 +1128,50 @@ public sealed class WorldScene : Scene
);
}
+ // Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
+ // выведенный рацион (ест растения/мясо), роль и боевые параметры — проверка, что хищники заданы.
+ private void RunDiet(DevConsole console, GameContent content, string[] args)
+ {
+ var species = content.Defs.NamesOf("Animal");
+ for (var s = 0; s < _animals.Count; s++)
+ {
+ var sp = _animals[s];
+ if (
+ args.Length > 0
+ && !string.Equals(sp.Def.DefName, args[0], StringComparison.OrdinalIgnoreCase)
+ )
+ {
+ continue;
+ }
+
+ var genome = _animals.GenerateGenome(s, new Random(0x01E7 + s));
+ var traits = AnimalPhenotype.FromTraits(
+ Phenotype.Compute(genome, _animals.GeneRegistry)
+ );
+ var (eatsPlants, eatsMeat) = AnimalFactory.Diet(traits, sp.Def);
+ var role =
+ eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
+ : eatsPlants ? "herbivore"
+ : "none";
+ console.WriteLine(
+ $"{sp.Def.DefName}: {role} — herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / "
+ + $"omni {traits.Omnivory:0.##} -> plants={eatsPlants} meat={eatsMeat}, spawn {sp.Def.SpawnPer1000Cells:0.##}/1000"
+ );
+ if (eatsMeat && sp.Def.AttackDamage > 0f)
+ {
+ console.WriteLine(
+ $" hunt: dmg {sp.Def.AttackDamage:0}/d, bleed {sp.Def.AttackBleed:0.#}/d, "
+ + $"bloodloss {sp.Def.AttackBloodLoss:0.#}/d, range {sp.Def.AttackRangeCells:0.#} cells"
+ );
+ }
+ }
+
+ if (args.Length == 0)
+ {
+ console.WriteLine($"species: {string.Join(", ", species)}");
+ }
+ }
+
// Наблюдаемость отбора (фаза A2): средние ключевых признаков живой популяции и размах поколений —
// видно дрейф генов под отбором. Без аргумента — все виды; с аргументом — один вид.
private void RunPopStats(DevConsole console, string[] args)
diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs
index 06c2279..6eb5596 100644
--- a/src/LittleSim/Sim/AnimalFactory.cs
+++ b/src/LittleSim/Sim/AnimalFactory.cs
@@ -110,6 +110,54 @@ public static class AnimalFactory
);
}
+ /// Порог выраженности признака диеты, с которого вид реально ест данный корм.
+ public const float DietThreshold = 0.5f;
+
+ ///
+ /// Рацион особи из ГЕНОВ: ест растения, если max(herbivory, omnivory) ≥ порога; ест мясо
+ /// (охота/падаль), если max(carnivory, omnivory) ≥ порога — всеядность (omnivory) включает оба
+ /// источника, специалисты задаются высоким herbivory ЛИБО carnivory. Если у вида нет генов диеты
+ /// (все три ≈ 0) — откат на список (легаси/модерский fallback).
+ ///
+ public static (bool EatsPlants, bool EatsMeat) Diet(in AnimalPhenotype traits, AnimalDef def)
+ {
+ if (traits.Herbivory <= 0f && traits.Carnivory <= 0f && traits.Omnivory <= 0f)
+ {
+ return (DietContains(def, "plant"), DietContains(def, "meat"));
+ }
+
+ var eatsPlants = MathF.Max(traits.Herbivory, traits.Omnivory) >= DietThreshold;
+ var eatsMeat = MathF.Max(traits.Carnivory, traits.Omnivory) >= DietThreshold;
+ return (eatsPlants, eatsMeat);
+ }
+
+ private static bool DietContains(AnimalDef def, string food)
+ {
+ foreach (var f in def.Diet)
+ {
+ if (string.Equals(f, food, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// Гибель особи: оставляет труп (мясо ∝ размер тела) и удаляет сущность. Общий путь смерти.
+ public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
+ {
+ if (entity.IsNull || !entity.HasComponent())
+ {
+ return;
+ }
+
+ ref readonly var org = ref entity.GetComponent();
+ var position = entity.GetComponent().Position;
+ CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, cellSize);
+ entity.DeleteEntity();
+ }
+
///
/// Эффективный интеллект особи (фаза A7): размер мозга (ген) × сознание (capacity из здоровья).
/// Повреждение мозга/болезнь/боль/кровопотеря снижают Consciousness → динамически роняют интеллект,
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 6f56161..9fd739e 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -39,6 +39,15 @@ public struct AnimalPhenotype
/// Возраст созревания (игровых дней) — порог стадии Adult.
public float MaturityAge;
+ /// Травоядность [0..1] — способность/тяга питаться растениями.
+ public float Herbivory;
+
+ /// Хищничество [0..1] — тяга охотиться и питаться мясом (живая добыча/падаль).
+ public float Carnivory;
+
+ /// Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).
+ public float Omnivory;
+
/// Продолжительность жизни (игровых дней).
public float Lifespan;
@@ -66,6 +75,9 @@ public struct AnimalPhenotype
Insulation = T("insulation"),
FurHue = T("furHue"),
MaturityAge = T("maturityAge"),
+ Herbivory = T("herbivory"),
+ Carnivory = T("carnivory"),
+ Omnivory = T("omnivory"),
Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"),
@@ -316,6 +328,89 @@ public sealed class HealthState
return false;
}
+
+ ///
+ /// Травма (укус хищника): повреждает часть тела (выбор по весу попадания coverage), вызывает
+ /// мгновенную кровопотерю и навешивает/усиливает кровотечение, затем пересчитывает способности.
+ /// Первый травматический источник урона — оживляет каркас здоровья A5.
+ ///
+ public void ApplyInjury(
+ float partDamage,
+ float bloodLoss,
+ HediffDef? bleeding,
+ float bleedSeverity,
+ Random rng
+ )
+ {
+ if (Parts.Length > 0 && partDamage > 0f)
+ {
+ var idx = PickPart(rng);
+ var part = Parts[idx];
+ part.Hp = MathF.Max(0f, part.Hp - partDamage);
+ Parts[idx] = part;
+ }
+
+ BloodLevel = Math.Clamp(BloodLevel - bloodLoss, 0f, 1f);
+ if (bleeding is not null && bleedSeverity > 0f)
+ {
+ Intensify(bleeding, bleedSeverity);
+ }
+
+ RecomputeCapacities();
+ }
+
+ // Выбор задетой части по весу попадания (coverage); детерминируется переданным rng.
+ private int PickPart(Random rng)
+ {
+ var total = 0f;
+ foreach (var p in Parts)
+ {
+ total += p.Def?.Coverage ?? 0f;
+ }
+
+ if (total <= 0f)
+ {
+ return rng.Next(Parts.Length);
+ }
+
+ var roll = (float)rng.NextDouble() * total;
+ for (var i = 0; i < Parts.Length; i++)
+ {
+ if (Parts[i].Def is null)
+ {
+ continue;
+ }
+
+ roll -= Parts[i].Def.Coverage;
+ if (roll <= 0f)
+ {
+ return i;
+ }
+ }
+
+ return Parts.Length - 1;
+ }
+
+ ///
+ /// Навешивает хедиф или усиливает существующий: тяжесть растёт (до 1), а заживление (иммунитет)
+ /// сбрасывается — свежая рана начинает затягиваться заново. Для повторных укусов/ранений.
+ ///
+ public void Intensify(HediffDef def, float addSeverity)
+ {
+ for (var i = 0; i < _hediffs.Count; i++)
+ {
+ if (_hediffs[i].Def.DefName == def.DefName)
+ {
+ var h = _hediffs[i];
+ h.Severity = Math.Clamp(h.Severity + addSeverity, 0f, 1f);
+ h.Immunity = 0f; // свежая рана — заживление с нуля
+ _hediffs[i] = h;
+ return;
+ }
+ }
+
+ _hediffs.Add(new Hediff { Def = def, Severity = Math.Clamp(addSeverity, 0f, 1f) });
+ }
}
/// Здоровье особи: managed-состояние со списком хедифов (см. ).
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index 5cf52ed..c51183f 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -22,6 +22,12 @@ public static class AnimalActions
public const string Drink = "Drink";
public const string Sleep = "Sleep";
public const string Mate = "Mate";
+
+ /// Охота: преследовать живую добычу и кусать (урон) либо есть труп — утоляет голод хищника.
+ public const string Hunt = "Hunt";
+
+ /// Бегство: уходить от ближайшего хищника (реакция жертвы, важнее прочих нужд).
+ public const string Flee = "Flee";
}
///
@@ -69,6 +75,12 @@ public struct AnimalBrain : IComponent
/// Id партнёра для ; -1 — нет.
public int TargetMate;
+ /// Id цели для — живой добычи или трупа; -1 — нет.
+ public int TargetPrey;
+
+ /// Цель — труп (падаль), а не живая добыча: при достижении едим, а не атакуем.
+ public bool TargetIsCorpse;
+
/// Секунды до следующего пересмотра решения.
public float DecideIn;
}
@@ -226,6 +238,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
Transform2D
> _animals;
private readonly ArchetypeQuery _plants;
+ private readonly ArchetypeQuery _corpses;
public AnimalDecisionSystem(
EntityStore store,
@@ -252,6 +265,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
Transform2D
>();
_plants = store.Query();
+ _corpses = store.Query();
}
// Строит utility-reasoner из данных: на каждую нужду — действие с соображением. Deplete: чем ниже
@@ -316,15 +330,54 @@ public sealed class AnimalDecisionSystem : BaseSystem
);
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent();
+
+ // Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
+ // любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
+ if (TryFindThreat(pos, radius, o[i].Species, o[i].Traits, out var threatPos))
+ {
+ brain.Action = AnimalActions.Flee;
+ brain.TargetPlant = -1;
+ brain.TargetPrey = -1;
+ var away = pos - threatPos;
+ var len = away.Length();
+ brain.Target =
+ len > 0.001f
+ ? pos + away / len * (radius + _cellSize)
+ : pos + new Vector2(_cellSize, 0f);
+ continue;
+ }
+
+ var (eatsPlants, eatsMeat) = AnimalFactory.Diet(
+ o[i].Traits,
+ _animalSet[o[i].Species].Def
+ );
var name =
_brain.Select(new AnimalContext(n[i].Values, _needs, intelligence))?.Name
?? AnimalActions.Wander;
switch (name)
{
+ // Хищник/всеядное голодает → ищет труп (падаль) или живую добычу и охотится.
case AnimalActions.Eat
- when EatsPlants(o[i].Species)
- && TryFindPlant(pos, radius, out var plant, out var pp):
+ when eatsMeat
+ && TryFindKill(
+ pos,
+ radius,
+ o[i].Species,
+ o[i].Traits,
+ self.Id,
+ out var preyId,
+ out var preyPos,
+ out var preyIsCorpse
+ ):
+ brain.Action = AnimalActions.Hunt;
+ brain.TargetPlant = -1;
+ brain.TargetPrey = preyId;
+ brain.TargetIsCorpse = preyIsCorpse;
+ brain.Target = preyPos;
+ break;
+ case AnimalActions.Eat
+ when eatsPlants && TryFindPlant(pos, radius, out var plant, out var pp):
brain.Action = AnimalActions.Eat;
brain.TargetPlant = plant;
brain.Target = pp;
@@ -371,21 +424,167 @@ public sealed class AnimalDecisionSystem : BaseSystem
private float VisionRadius(int species, float vision) =>
MathF.Max(1f, _animalSet[species].Def.VisionCells) * MathF.Max(0.2f, vision) * _cellSize;
- // Ест ли вид растения (диета данными) — гейт поиска корма; хищники (meat) появятся на горизонте.
- private bool EatsPlants(int species)
+ // Ближайший хищник-угроза в радиусе восприятия (для бегства жертвы). Угроза = другой вид, который
+ // ест мясо и достаточно крупный (см. IsThreatTo). O(n) на особь — как поиск корма.
+ private bool TryFindThreat(
+ Vector2 from,
+ float radius,
+ int selfSpecies,
+ in AnimalPhenotype selfTraits,
+ out Vector2 threatPos
+ )
{
- var diet = _animalSet[species].Def.Diet;
- foreach (var food in diet)
+ var bestSq = radius * radius;
+ threatPos = default;
+ var found = false;
+ var (_, selfEatsMeat) = AnimalFactory.Diet(selfTraits, _animalSet[selfSpecies].Def);
+ var selfBody = selfTraits.BodySize;
+ foreach (var (_, _, organisms, _, transforms, _) in _animals.Chunks)
{
- if (string.Equals(food, "plant", StringComparison.Ordinal))
+ var oo = organisms.Span;
+ var tt = transforms.Span;
+ for (var i = 0; i < tt.Length; i++)
{
- return true;
+ if (
+ oo[i].Species == selfSpecies
+ || !IsThreatTo(selfEatsMeat, selfBody, oo[i].Species, oo[i].Traits)
+ )
+ {
+ continue;
+ }
+
+ var sq = Vector2.DistanceSquared(from, tt[i].Position);
+ if (sq < bestSq)
+ {
+ bestSq = sq;
+ threatPos = tt[i].Position;
+ found = true;
+ }
}
}
+ return found;
+ }
+
+ // Является ли вид-кандидат угрозой особи: он ест мясо и не мельче её. Чистая жертва (не мясоед)
+ // бежит от любого хищника не мельче себя; сам хищник — лишь от заметно более крупного мясоеда.
+ private bool IsThreatTo(
+ bool selfEatsMeat,
+ float selfBody,
+ int otherSpecies,
+ in AnimalPhenotype otherTraits
+ )
+ {
+ var (_, otherEatsMeat) = AnimalFactory.Diet(otherTraits, _animalSet[otherSpecies].Def);
+ if (!otherEatsMeat)
+ {
+ return false;
+ }
+
+ var otherBody = otherTraits.BodySize;
+ return selfEatsMeat ? otherBody > selfBody * 1.1f : otherBody >= selfBody * 0.9f;
+ }
+
+ // Ближайшая «мясная» цель: труп (падаль, приоритет — даровая еда без риска) или живая добыча.
+ // Возвращает id, позицию и флаг трупа. Добыча — другой вид не крупнее охотника (см. IsPreyFor).
+ private bool TryFindKill(
+ Vector2 from,
+ float radius,
+ int selfSpecies,
+ in AnimalPhenotype selfTraits,
+ int selfId,
+ out int targetId,
+ out Vector2 position,
+ out bool isCorpse
+ )
+ {
+ targetId = -1;
+ position = default;
+ isCorpse = false;
+
+ // 1) Падаль с остатком мяса.
+ var bestCorpseSq = radius * radius;
+ var corpseId = -1;
+ var corpsePos = default(Vector2);
+ foreach (var (transforms, corpses, entities) in _corpses.Chunks)
+ {
+ var tt = transforms.Span;
+ var cc = corpses.Span;
+ for (var i = 0; i < tt.Length; i++)
+ {
+ if (cc[i].Meat <= 0f)
+ {
+ continue;
+ }
+
+ var sq = Vector2.DistanceSquared(from, tt[i].Position);
+ var id = entities.EntityAt(i).Id;
+ if (sq < bestCorpseSq || (sq == bestCorpseSq && id < corpseId))
+ {
+ bestCorpseSq = sq;
+ corpseId = id;
+ corpsePos = tt[i].Position;
+ }
+ }
+ }
+
+ // 2) Живая добыча.
+ var bestPreySq = radius * radius;
+ var preyId = -1;
+ var preyPos = default(Vector2);
+ var selfBody = selfTraits.BodySize;
+ foreach (var (_, _, organisms, _, transforms, entities) in _animals.Chunks)
+ {
+ var oo = organisms.Span;
+ var tt = transforms.Span;
+ for (var i = 0; i < tt.Length; i++)
+ {
+ var id = entities.EntityAt(i).Id;
+ if (oo[i].Species == selfSpecies || id == selfId)
+ {
+ continue;
+ }
+
+ if (!IsPreyFor(selfBody, oo[i].Traits))
+ {
+ continue;
+ }
+
+ var sq = Vector2.DistanceSquared(from, tt[i].Position);
+ if (sq < bestPreySq || (sq == bestPreySq && id < preyId))
+ {
+ bestPreySq = sq;
+ preyId = id;
+ preyPos = tt[i].Position;
+ }
+ }
+ }
+
+ // Предпочитаем падаль, если она не намного дальше живой добычи (даровая еда, без риска).
+ if (corpseId >= 0 && (preyId < 0 || bestCorpseSq <= bestPreySq * 2f))
+ {
+ targetId = corpseId;
+ position = corpsePos;
+ isCorpse = true;
+ return true;
+ }
+
+ if (preyId >= 0)
+ {
+ targetId = preyId;
+ position = preyPos;
+ isCorpse = false;
+ return true;
+ }
+
return false;
}
+ // Подходит ли особь в добычу охотнику данного размера: важно лишь, что она не крупнее охотника
+ // (на превосходящих не нападаем). Вид/мясоедство добычи не важны — хищник ест и травоядных, и прочих.
+ private static bool IsPreyFor(float hunterBody, in AnimalPhenotype otherTraits) =>
+ otherTraits.BodySize <= hunterBody * 1.25f;
+
// Ближайшее растение в радиусе (квадрат расстояния); ничьи — по меньшему id (детерминизм).
private bool TryFindPlant(Vector2 from, float radius, out int plantId, out Vector2 position)
{
@@ -496,6 +695,7 @@ public sealed class AnimalActionSystem : BaseSystem
{
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
+ private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
private readonly EntityStore _store;
private readonly PlantSet _plants;
@@ -504,7 +704,10 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly ThoughtSet _thoughts;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
+ private readonly int _cellSize;
private readonly RectF _bounds;
+ private readonly HediffDef? _bleeding;
+ private readonly Random _rng;
private readonly ArchetypeQuery<
AnimalBrain,
AnimalNeeds,
@@ -513,6 +716,8 @@ public sealed class AnimalActionSystem : BaseSystem
Health
> _query;
private readonly List _eaten = [];
+ private readonly List _consumed = []; // трупы, доеденные до нуля
+ private readonly List _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
private readonly List<(Entity Self, Entity Partner)> _matings = [];
public AnimalActionSystem(
@@ -523,7 +728,10 @@ public sealed class AnimalActionSystem : BaseSystem
ThoughtSet thoughts,
GameClock clock,
float secondsPerDay,
- RectF bounds
+ int cellSize,
+ RectF bounds,
+ HediffDef? bleeding,
+ int seed
)
{
_store = store;
@@ -533,7 +741,10 @@ public sealed class AnimalActionSystem : BaseSystem
_thoughts = thoughts;
_clock = clock;
_secondsPerDay = secondsPerDay;
+ _cellSize = cellSize;
_bounds = bounds;
+ _bleeding = bleeding;
+ _rng = new Random(seed);
_query = store.Query();
}
@@ -547,6 +758,8 @@ public sealed class AnimalActionSystem : BaseSystem
var days = seconds / _secondsPerDay;
_eaten.Clear();
+ _consumed.Clear();
+ _kills.Clear();
_matings.Clear();
foreach (
@@ -622,6 +835,14 @@ public sealed class AnimalActionSystem : BaseSystem
break;
+ case AnimalActions.Hunt:
+ Hunt(ref brain, ref pos, values, def, speed * seconds, days);
+ break;
+
+ case AnimalActions.Flee:
+ MoveTo(ref pos, brain.Target, speed * FleeSpeedMult * seconds);
+ break;
+
default: // Wander
MoveTo(ref pos, brain.Target, speed * seconds);
break;
@@ -637,9 +858,109 @@ public sealed class AnimalActionSystem : BaseSystem
plant.DeleteEntity();
}
+ foreach (var corpse in _consumed)
+ {
+ if (!corpse.IsNull)
+ {
+ corpse.DeleteEntity(); // труп доеден до нуля
+ }
+ }
+
+ foreach (var prey in _kills)
+ {
+ AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
+ }
+
ApplyMatings();
}
+ // Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
+ // либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
+ // травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
+ private void Hunt(
+ ref AnimalBrain brain,
+ ref Vector2 pos,
+ float[] values,
+ AnimalDef def,
+ float step,
+ float days
+ )
+ {
+ if (
+ brain.TargetPrey < 0
+ || !_store.TryGetEntityById(brain.TargetPrey, out var target)
+ || target.IsNull
+ || !target.HasComponent()
+ )
+ {
+ return; // цель исчезла — действие пересмотрят на следующем решении
+ }
+
+ var targetPos = target.GetComponent().Position;
+ var range = def.AttackRangeCells * _cellSize;
+ var inRange = Vector2.DistanceSquared(pos, targetPos) <= range * range;
+
+ if (brain.TargetIsCorpse)
+ {
+ if (!target.HasComponent())
+ {
+ return;
+ }
+
+ if (!inRange)
+ {
+ MoveTo(ref pos, targetPos, step);
+ return;
+ }
+
+ ref var corpse = ref target.GetComponent();
+ Refill(values, AnimalActions.Eat, days);
+ corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
+ if (corpse.Meat <= 0f && !_consumed.Contains(target))
+ {
+ _consumed.Add(target);
+ }
+
+ return;
+ }
+
+ // Живая добыча.
+ if (!target.HasComponent() || !target.HasComponent())
+ {
+ return;
+ }
+
+ if (!inRange)
+ {
+ MoveTo(ref pos, targetPos, step);
+ return;
+ }
+
+ var preyHealth = target.GetComponent().State;
+ if (preyHealth is null)
+ {
+ return;
+ }
+
+ preyHealth.ApplyInjury(
+ def.AttackDamage * days,
+ def.AttackBloodLoss * days,
+ _bleeding,
+ def.AttackBleed * days,
+ _rng
+ );
+ if (
+ preyHealth.BloodLevel <= 0f
+ || preyHealth.Capacity(AnimalCapacities.Consciousness) <= 0.01f
+ )
+ {
+ if (!_kills.Contains(target))
+ {
+ _kills.Add(target); // добита — смерть после прохода (структурное изменение)
+ }
+ }
+ }
+
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay.
private void Refill(float[] values, string action, float days)
{
@@ -985,22 +1306,7 @@ public sealed class AnimalMortalitySystem : BaseSystem
foreach (var dead in _deaths)
{
- if (dead.IsNull || !dead.HasComponent())
- {
- continue;
- }
-
- ref readonly var org = ref dead.GetComponent();
- var position = dead.GetComponent().Position;
- AnimalFactory.CreateCorpse(
- _store,
- _animals,
- org.Species,
- position,
- org.Traits.BodySize,
- _cellSize
- );
- dead.DeleteEntity();
+ AnimalFactory.Die(_store, _animals, dead, _cellSize);
}
}
}
@@ -1228,6 +1534,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
public sealed class AnimalHealthSystem : BaseSystem
{
private const float TickDays = 1f / 24f; // ~игровой час
+ private const float BloodRecoveryPerDay = 0.25f; // восстановление крови в покое (нет кровотечений)
private readonly EntityStore _store;
private readonly AnimalSet _animals;
@@ -1295,6 +1602,26 @@ public sealed class AnimalHealthSystem : BaseSystem
}
var lethal = state.AdvanceHediffs(days);
+ // Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
+ var bleed = 0f;
+ foreach (var hd in state.Hediffs)
+ {
+ bleed += hd.Def.BloodLossPerDay * hd.Severity;
+ }
+
+ if (bleed > 0f)
+ {
+ state.BloodLevel = Math.Clamp(state.BloodLevel - bleed * days, 0f, 1f);
+ }
+ else if (state.BloodLevel < 1f)
+ {
+ state.BloodLevel = Math.Clamp(
+ state.BloodLevel + BloodRecoveryPerDay * days,
+ 0f,
+ 1f
+ );
+ }
+
state.RecomputeCapacities();
if (
lethal
@@ -1309,22 +1636,7 @@ public sealed class AnimalHealthSystem : BaseSystem
foreach (var dead in _deaths)
{
- if (dead.IsNull || !dead.HasComponent())
- {
- continue;
- }
-
- ref readonly var org = ref dead.GetComponent();
- var position = dead.GetComponent().Position;
- AnimalFactory.CreateCorpse(
- _store,
- _animals,
- org.Species,
- position,
- org.Traits.BodySize,
- _cellSize
- );
- dead.DeleteEntity();
+ AnimalFactory.Die(_store, _animals, dead, _cellSize);
}
}
}