Животные A4: размножение (гон-хедиф, беременность, роды)
Нужда Mating + лёгкий hediff-каркас (HediffDef, hediffs.json с Rut, Health/HealthState). Гены GeneBreedingSeason/GeneGestationDays/GeneLitterSize. AnimalRutSystem навешивает сезонный гон-хедиф взрослым и поднимает влечение (вне сезона зачатия нет). Действие Mate ищет партнёра противоположного пола и сближается; при контакте самка получает Pregnant (геном отца). AnimalPregnancySystem тикает срок и рожает помёт через Genome.Breed (пол наследуется без YY, поколение+1), с потолком численности. Сборка чистая, --check-content (8 типов дефов, 33 гена, 32 признака). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
355ced4937
commit
97521f81b8
@@ -22,6 +22,9 @@
|
|||||||
"furColor": 0.45,
|
"furColor": 0.45,
|
||||||
"maturityAge": 90,
|
"maturityAge": 90,
|
||||||
"lifespan": 360,
|
"lifespan": 360,
|
||||||
|
"breedingSeason": 2,
|
||||||
|
"gestationDays": 30,
|
||||||
|
"litterSize": 1,
|
||||||
"spread": 0.08
|
"spread": 0.08
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,17 @@
|
|||||||
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
|
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
|
||||||
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
|
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
|
||||||
|
|
||||||
|
// Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
|
||||||
|
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
|
||||||
|
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "breedingSeason": "value" } },
|
||||||
|
{ "defName": "GeneGestationDays", "parent": "BaseNumericGene", "label": "gene.gestationDays",
|
||||||
|
"default": 30, "min": 1, "max": 200, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "gestationDays": "value" } },
|
||||||
|
{ "defName": "GeneLitterSize", "parent": "BaseNumericGene", "label": "gene.litterSize",
|
||||||
|
"default": 1, "min": 1, "max": 12, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "litterSize": "value" } },
|
||||||
|
|
||||||
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
||||||
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
||||||
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "Hediff",
|
||||||
|
// Хедифы — состояния организма (фаза A4: лёгкий каркас). Гон (Rut) навешивается сезонно на взрослых
|
||||||
|
// и поднимает половое влечение. Раны/болезни/возрастные эффекты со стадиями придут в A5/A6.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "Rut", "label": "hediff.rut" }
|
||||||
|
]
|
||||||
|
}
|
||||||
+8
-1
@@ -1,8 +1,15 @@
|
|||||||
# Животные (зоология)
|
# Животные (зоология)
|
||||||
|
|
||||||
> Статус: **A1–A3 реализованы**, остальное по плану. Это живой план — правится по мере
|
> Статус: **A1–A4 реализованы**, остальное по плану. Это живой план — правится по мере
|
||||||
> реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
|
> реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
|
||||||
>
|
>
|
||||||
|
> **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`,
|
||||||
|
> компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`;
|
||||||
|
> `AnimalRutSystem` (сезонный гон-хедиф поднимает влечение — вне сезона зачатия нет); действие
|
||||||
|
> `Mate` (поиск партнёра, сближение); `Pregnant` + `AnimalPregnancySystem` (вынашивание → роды
|
||||||
|
> через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка
|
||||||
|
> чистая, `--check-content` (33 гена).
|
||||||
|
>
|
||||||
> **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY,
|
> **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY,
|
||||||
> генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`;
|
> генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`;
|
||||||
> стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена
|
> стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ public sealed class AnimalSet
|
|||||||
new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f),
|
new GenomeTemplate.Entry(Gene("GeneFurColor"), g.FurColor, 0.06f),
|
||||||
Numeric("GeneMaturityAge", g.MaturityAge),
|
Numeric("GeneMaturityAge", g.MaturityAge),
|
||||||
Numeric("GeneLifespan", g.Lifespan),
|
Numeric("GeneLifespan", g.Lifespan),
|
||||||
|
new GenomeTemplate.Entry(Gene("GeneBreedingSeason"), g.BreedingSeason, 0f),
|
||||||
|
Numeric("GeneGestationDays", g.GestationDays),
|
||||||
|
Numeric("GeneLitterSize", g.LitterSize),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public sealed class GameContent
|
|||||||
defs.RegisterType<PlantDef>("Plant");
|
defs.RegisterType<PlantDef>("Plant");
|
||||||
defs.RegisterType<PawnDef>("Pawn");
|
defs.RegisterType<PawnDef>("Pawn");
|
||||||
defs.RegisterType<AnimalDef>("Animal");
|
defs.RegisterType<AnimalDef>("Animal");
|
||||||
|
defs.RegisterType<HediffDef>("Hediff");
|
||||||
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'");
|
||||||
|
|||||||
@@ -247,6 +247,15 @@ public sealed class AnimalGenomeDef
|
|||||||
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
||||||
public float Lifespan { get; init; } = 160f;
|
public float Lifespan { get; init; } = 160f;
|
||||||
|
|
||||||
|
/// <summary>Сезон гона (0 весна, 1 лето, 2 осень, 3 зима) — фиксирован по виду.</summary>
|
||||||
|
public float BreedingSeason { get; init; } = 2f;
|
||||||
|
|
||||||
|
/// <summary>Срок вынашивания (игровых дней).</summary>
|
||||||
|
public float GestationDays { get; init; } = 30f;
|
||||||
|
|
||||||
|
/// <summary>Размер помёта (число детёнышей за роды).</summary>
|
||||||
|
public float LitterSize { get; init; } = 1f;
|
||||||
|
|
||||||
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||||
public float Spread { get; init; } = 0.08f;
|
public float Spread { get; init; } = 0.08f;
|
||||||
}
|
}
|
||||||
@@ -267,3 +276,10 @@ public sealed class AnimalDef : PawnDef
|
|||||||
/// <summary>Базовый геном вида: центры аллелей животных генов.</summary>
|
/// <summary>Базовый геном вида: центры аллелей животных генов.</summary>
|
||||||
public AnimalGenomeDef Genome { get; init; } = new();
|
public AnimalGenomeDef Genome { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние/хедиф организма (Defs/hediffs.json): гон, позже — раны, болезни, возрастные эффекты.
|
||||||
|
/// Фаза A4 — лёгкий каркас: пока идентичность + локализуемый <see cref="Def.Label"/>; стадии,
|
||||||
|
/// модификаторы способностей и иммунитет придут в A5/A6.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HediffDef : Def { }
|
||||||
|
|||||||
@@ -213,6 +213,14 @@ public sealed class WorldScene : Scene
|
|||||||
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
|
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
|
||||||
var shore = ComputeShore();
|
var shore = ComputeShore();
|
||||||
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay));
|
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay));
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalRutSystem(
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
climate,
|
||||||
|
content.Defs.Get<HediffDef>("Rut")
|
||||||
|
)
|
||||||
|
);
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new AnimalDecisionSystem(Store, Context.Clock, CellSize, shore, _config.Seed + 0x5EED)
|
new AnimalDecisionSystem(Store, Context.Clock, CellSize, shore, _config.Seed + 0x5EED)
|
||||||
);
|
);
|
||||||
@@ -222,6 +230,17 @@ public sealed class WorldScene : Scene
|
|||||||
UpdateSystems.Add(new AnimalAppearanceSystem());
|
UpdateSystems.Add(new AnimalAppearanceSystem());
|
||||||
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
|
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
|
||||||
UpdateSystems.Add(new AnimalMortalitySystem(Store));
|
UpdateSystems.Add(new AnimalMortalitySystem(Store));
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalPregnancySystem(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
CellSize,
|
||||||
|
maxAnimals: 500,
|
||||||
|
_config.Seed + 0x4BED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||||
|
|||||||
@@ -68,7 +68,13 @@ public static class AnimalFactory
|
|||||||
Thirst = 1f,
|
Thirst = 1f,
|
||||||
Rest = 1f,
|
Rest = 1f,
|
||||||
},
|
},
|
||||||
new AnimalBrain { Action = AnimalAction.Wander, TargetPlant = -1 }
|
new AnimalBrain
|
||||||
|
{
|
||||||
|
Action = AnimalAction.Wander,
|
||||||
|
TargetPlant = -1,
|
||||||
|
TargetMate = -1,
|
||||||
|
},
|
||||||
|
new Health { State = new HealthState() }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Friflo.Engine.ECS;
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
using MrGameEng.Genetics;
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
@@ -41,6 +42,15 @@ public struct AnimalPhenotype
|
|||||||
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||||
public float Lifespan;
|
public float Lifespan;
|
||||||
|
|
||||||
|
/// <summary>Сезон гона (0 весна … 3 зима).</summary>
|
||||||
|
public int BreedingSeason;
|
||||||
|
|
||||||
|
/// <summary>Срок вынашивания (игровых дней).</summary>
|
||||||
|
public float GestationDays;
|
||||||
|
|
||||||
|
/// <summary>Размер помёта.</summary>
|
||||||
|
public float LitterSize;
|
||||||
|
|
||||||
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||||
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
||||||
{
|
{
|
||||||
@@ -57,6 +67,9 @@ public struct AnimalPhenotype
|
|||||||
FurHue = T("furHue"),
|
FurHue = T("furHue"),
|
||||||
MaturityAge = T("maturityAge"),
|
MaturityAge = T("maturityAge"),
|
||||||
Lifespan = T("lifespan"),
|
Lifespan = T("lifespan"),
|
||||||
|
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||||
|
GestationDays = T("gestationDays"),
|
||||||
|
LitterSize = T("litterSize"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,3 +112,78 @@ public struct AnimalGrowth : IComponent
|
|||||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||||
public float AgeDays;
|
public float AgeDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние здоровья особи (фаза A4 — лёгкий каркас): список активных хедифов. Managed-объект, как
|
||||||
|
/// <see cref="Genome"/> (Friflo допускает ссылку в компоненте). В A5/A6 прирастёт частями тела,
|
||||||
|
/// кровью, capacities и иммунитетом — контейнер один, растёт по фазам.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HealthState
|
||||||
|
{
|
||||||
|
private readonly List<HediffDef> _hediffs = [];
|
||||||
|
|
||||||
|
/// <summary>Активные хедифы (гон, позже — раны/болезни).</summary>
|
||||||
|
public IReadOnlyList<HediffDef> Hediffs => _hediffs;
|
||||||
|
|
||||||
|
/// <summary>Несёт ли особь хедиф с данным именем дефа.</summary>
|
||||||
|
public bool Has(string defName)
|
||||||
|
{
|
||||||
|
foreach (var h in _hediffs)
|
||||||
|
{
|
||||||
|
if (h.DefName == defName)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Навешивает хедиф (если ещё не навешан).</summary>
|
||||||
|
public void Add(HediffDef def)
|
||||||
|
{
|
||||||
|
if (!Has(def.DefName))
|
||||||
|
{
|
||||||
|
_hediffs.Add(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Снимает хедиф по имени дефа; возвращает, был ли он.</summary>
|
||||||
|
public bool Remove(string defName)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _hediffs.Count; i++)
|
||||||
|
{
|
||||||
|
if (_hediffs[i].DefName == defName)
|
||||||
|
{
|
||||||
|
_hediffs.RemoveAt(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Здоровье особи: managed-состояние со списком хедифов (см. <see cref="HealthState"/>).</summary>
|
||||||
|
public struct Health : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Состояние здоровья (хедифы; позже части тела/кровь).</summary>
|
||||||
|
public HealthState State;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
|
||||||
|
/// срока вынашивания и размер помёта. По истечении срока <see cref="Sim.AnimalPregnancySystem"/>
|
||||||
|
/// рожает детёнышей через <see cref="Genome.Breed"/> и снимает компонент.
|
||||||
|
/// </summary>
|
||||||
|
public struct Pregnant : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Геном отца на момент зачатия.</summary>
|
||||||
|
public Genome FatherGenome;
|
||||||
|
|
||||||
|
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||||
|
public float DueInDays;
|
||||||
|
|
||||||
|
/// <summary>Сколько детёнышей родится.</summary>
|
||||||
|
public int Litter;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using LittleSim.Content;
|
|||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.AI;
|
using MrGameEng.AI;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
@@ -22,6 +23,9 @@ public enum AnimalAction
|
|||||||
|
|
||||||
/// <summary>Стоит и спит (восстанавливает отдых).</summary>
|
/// <summary>Стоит и спит (восстанавливает отдых).</summary>
|
||||||
Sleep,
|
Sleep,
|
||||||
|
|
||||||
|
/// <summary>Идёт к партнёру и спаривается (фаза A4) — у самки наступает беременность.</summary>
|
||||||
|
Mate,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,7 +47,10 @@ public struct AnimalNeeds : IComponent
|
|||||||
/// <summary>Накоплено игровых дней истощения (голод/жажда на нуле); порог — смерть (A3).</summary>
|
/// <summary>Накоплено игровых дней истощения (голод/жажда на нуле); порог — смерть (A3).</summary>
|
||||||
public float Starve;
|
public float Starve;
|
||||||
|
|
||||||
/// <summary>Наименьшая (самая острая) нужда — для выбора и внешнего вида.</summary>
|
/// <summary>Половое влечение [0,1] (фаза A4): растёт под гоном (хедиф Rut), обнуляется спариванием.</summary>
|
||||||
|
public float Mating;
|
||||||
|
|
||||||
|
/// <summary>Наименьшая (самая острая) витальная нужда — для выбора и внешнего вида (без влечения).</summary>
|
||||||
public readonly float Worst() => MathF.Min(Hunger, MathF.Min(Thirst, Rest));
|
public readonly float Worst() => MathF.Min(Hunger, MathF.Min(Thirst, Rest));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,12 +70,15 @@ public struct AnimalBrain : IComponent
|
|||||||
/// <summary>Id растения-цели для <see cref="AnimalAction.Eat"/>; -1 — нет.</summary>
|
/// <summary>Id растения-цели для <see cref="AnimalAction.Eat"/>; -1 — нет.</summary>
|
||||||
public int TargetPlant;
|
public int TargetPlant;
|
||||||
|
|
||||||
|
/// <summary>Id партнёра для <see cref="AnimalAction.Mate"/>; -1 — нет.</summary>
|
||||||
|
public int TargetMate;
|
||||||
|
|
||||||
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
||||||
public float DecideIn;
|
public float DecideIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Снимок состояния зверя для соображений utility-выбора.</summary>
|
/// <summary>Снимок состояния зверя для соображений utility-выбора.</summary>
|
||||||
public readonly record struct AnimalContext(float Hunger, float Thirst, float Rest);
|
public readonly record struct AnimalContext(float Hunger, float Thirst, float Rest, float Mating);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Падение нужд со временем (игровые дни через <see cref="GameClock"/>, с учётом паузы/скорости):
|
/// Падение нужд со временем (игровые дни через <see cref="GameClock"/>, с учётом паузы/скорости):
|
||||||
@@ -152,6 +162,11 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
|
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
// Спаривание (A4): тем привлекательнее, чем выше влечение (растёт только под гоном).
|
||||||
|
new UtilityAction<AnimalContext>(
|
||||||
|
"mate",
|
||||||
|
new Consideration<AnimalContext>("lustful", c => c.Mating)
|
||||||
|
),
|
||||||
new UtilityAction<AnimalContext>(
|
new UtilityAction<AnimalContext>(
|
||||||
"wander",
|
"wander",
|
||||||
new Consideration<AnimalContext>("idle", _ => 0.12f)
|
new Consideration<AnimalContext>("idle", _ => 0.12f)
|
||||||
@@ -162,7 +177,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
private readonly int _cellSize;
|
private readonly int _cellSize;
|
||||||
private readonly Vector2[] _shore;
|
private readonly Vector2[] _shore;
|
||||||
private readonly Random _rng;
|
private readonly Random _rng;
|
||||||
private readonly ArchetypeQuery<AnimalNeeds, AnimalBrain, AnimalOrganism, Transform2D> _animals;
|
private readonly ArchetypeQuery<
|
||||||
|
AnimalNeeds,
|
||||||
|
AnimalBrain,
|
||||||
|
AnimalOrganism,
|
||||||
|
AnimalGrowth,
|
||||||
|
Transform2D
|
||||||
|
> _animals;
|
||||||
private readonly ArchetypeQuery<Transform2D, PlantGrowth> _plants;
|
private readonly ArchetypeQuery<Transform2D, PlantGrowth> _plants;
|
||||||
|
|
||||||
public AnimalDecisionSystem(
|
public AnimalDecisionSystem(
|
||||||
@@ -177,18 +198,19 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
_cellSize = cellSize;
|
_cellSize = cellSize;
|
||||||
_shore = shore;
|
_shore = shore;
|
||||||
_rng = new Random(seed);
|
_rng = new Random(seed);
|
||||||
_animals = store.Query<AnimalNeeds, AnimalBrain, AnimalOrganism, Transform2D>();
|
_animals = store.Query<AnimalNeeds, AnimalBrain, AnimalOrganism, AnimalGrowth, Transform2D>();
|
||||||
_plants = store.Query<Transform2D, PlantGrowth>();
|
_plants = store.Query<Transform2D, PlantGrowth>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdateGroup()
|
protected override void OnUpdateGroup()
|
||||||
{
|
{
|
||||||
var delta = _clock.DeltaTime;
|
var delta = _clock.DeltaTime;
|
||||||
foreach (var (needs, brains, organisms, transforms, _) in _animals.Chunks)
|
foreach (var (needs, brains, organisms, growths, transforms, _) in _animals.Chunks)
|
||||||
{
|
{
|
||||||
var n = needs.Span;
|
var n = needs.Span;
|
||||||
var b = brains.Span;
|
var b = brains.Span;
|
||||||
var o = organisms.Span;
|
var o = organisms.Span;
|
||||||
|
var gr = growths.Span;
|
||||||
var t = transforms.Span;
|
var t = transforms.Span;
|
||||||
for (var i = 0; i < b.Length; i++)
|
for (var i = 0; i < b.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -201,8 +223,9 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
|
|
||||||
brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан
|
brain.DecideIn = Interval + _rng.NextSingle() * Interval; // джиттер — расфазировать скан
|
||||||
var pos = t[i].Position;
|
var pos = t[i].Position;
|
||||||
|
var adult = gr[i].Stage >= AnimalFactory.StageAdult;
|
||||||
var name = _brain
|
var name = _brain
|
||||||
.Select(new AnimalContext(n[i].Hunger, n[i].Thirst, n[i].Rest))
|
.Select(new AnimalContext(n[i].Hunger, n[i].Thirst, n[i].Rest, n[i].Mating))
|
||||||
?.Name;
|
?.Name;
|
||||||
|
|
||||||
switch (name)
|
switch (name)
|
||||||
@@ -221,6 +244,20 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
brain.Action = AnimalAction.Sleep;
|
brain.Action = AnimalAction.Sleep;
|
||||||
brain.TargetPlant = -1;
|
brain.TargetPlant = -1;
|
||||||
break;
|
break;
|
||||||
|
case "mate"
|
||||||
|
when adult
|
||||||
|
&& TryFindMate(
|
||||||
|
pos,
|
||||||
|
o[i].IsMale,
|
||||||
|
o[i].Traits.Vision,
|
||||||
|
out var mateId,
|
||||||
|
out var matePos
|
||||||
|
):
|
||||||
|
brain.Action = AnimalAction.Mate;
|
||||||
|
brain.TargetPlant = -1;
|
||||||
|
brain.TargetMate = mateId;
|
||||||
|
brain.Target = matePos;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
brain.Action = AnimalAction.Wander;
|
brain.Action = AnimalAction.Wander;
|
||||||
brain.TargetPlant = -1;
|
brain.TargetPlant = -1;
|
||||||
@@ -284,6 +321,49 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ближайший подходящий партнёр в радиусе зрения: взрослый, противоположного пола, с влечением > 0.5.
|
||||||
|
private bool TryFindMate(
|
||||||
|
Vector2 from,
|
||||||
|
bool selfMale,
|
||||||
|
float vision,
|
||||||
|
out int mateId,
|
||||||
|
out Vector2 position
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var radius = VisionCells * MathF.Max(0.2f, vision) * _cellSize;
|
||||||
|
var bestSq = radius * radius;
|
||||||
|
mateId = -1;
|
||||||
|
position = default;
|
||||||
|
foreach (var (needs, _, organisms, growths, transforms, entities) in _animals.Chunks)
|
||||||
|
{
|
||||||
|
var nn = needs.Span;
|
||||||
|
var oo = organisms.Span;
|
||||||
|
var gg = growths.Span;
|
||||||
|
var tt = transforms.Span;
|
||||||
|
for (var i = 0; i < tt.Length; i++)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
oo[i].IsMale == selfMale
|
||||||
|
|| gg[i].Stage < AnimalFactory.StageAdult
|
||||||
|
|| nn[i].Mating < 0.5f
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue; // тот же пол / не взрослый / без влечения (self отсеивается по полу)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sq = Vector2.DistanceSquared(from, tt[i].Position);
|
||||||
|
if (sq > 0.01f && sq < bestSq)
|
||||||
|
{
|
||||||
|
bestSq = sq;
|
||||||
|
mateId = entities.EntityAt(i).Id;
|
||||||
|
position = tt[i].Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mateId >= 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -308,6 +388,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
private readonly RectF _bounds;
|
private readonly RectF _bounds;
|
||||||
private readonly ArchetypeQuery<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D> _query;
|
private readonly ArchetypeQuery<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D> _query;
|
||||||
private readonly List<Entity> _eaten = [];
|
private readonly List<Entity> _eaten = [];
|
||||||
|
private readonly List<(Entity Self, Entity Partner)> _matings = [];
|
||||||
|
|
||||||
public AnimalActionSystem(
|
public AnimalActionSystem(
|
||||||
EntityStore store,
|
EntityStore store,
|
||||||
@@ -335,8 +416,9 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
|
|
||||||
var days = seconds / _secondsPerDay;
|
var days = seconds / _secondsPerDay;
|
||||||
_eaten.Clear();
|
_eaten.Clear();
|
||||||
|
_matings.Clear();
|
||||||
|
|
||||||
foreach (var (brains, needs, organisms, transforms, _) in _query.Chunks)
|
foreach (var (brains, needs, organisms, transforms, entities) in _query.Chunks)
|
||||||
{
|
{
|
||||||
var b = brains.Span;
|
var b = brains.Span;
|
||||||
var n = needs.Span;
|
var n = needs.Span;
|
||||||
@@ -372,6 +454,23 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case AnimalAction.Mate:
|
||||||
|
if (
|
||||||
|
brain.TargetMate >= 0
|
||||||
|
&& _store.TryGetEntityById(brain.TargetMate, out var partner)
|
||||||
|
&& !partner.IsNull
|
||||||
|
&& partner.HasComponent<Transform2D>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var partnerPos = partner.GetComponent<Transform2D>().Position;
|
||||||
|
if (MoveTo(ref pos, partnerPos, speed * seconds))
|
||||||
|
{
|
||||||
|
_matings.Add((entities.EntityAt(i), partner));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
default: // Wander
|
default: // Wander
|
||||||
MoveTo(ref pos, brain.Target, speed * seconds);
|
MoveTo(ref pos, brain.Target, speed * seconds);
|
||||||
break;
|
break;
|
||||||
@@ -386,6 +485,58 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
{
|
{
|
||||||
plant.DeleteEntity();
|
plant.DeleteEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplyMatings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление
|
||||||
|
// компонента — после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
|
||||||
|
private void ApplyMatings()
|
||||||
|
{
|
||||||
|
foreach (var (a, b) in _matings)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
a.IsNull
|
||||||
|
|| b.IsNull
|
||||||
|
|| !a.HasComponent<AnimalOrganism>()
|
||||||
|
|| !b.HasComponent<AnimalOrganism>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aMale = a.GetComponent<AnimalOrganism>().IsMale;
|
||||||
|
var bMale = b.GetComponent<AnimalOrganism>().IsMale;
|
||||||
|
if (aMale == bMale)
|
||||||
|
{
|
||||||
|
continue; // один пол — не пара (страховка)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mother = aMale ? b : a;
|
||||||
|
var father = aMale ? a : b;
|
||||||
|
if (!mother.HasComponent<Pregnant>())
|
||||||
|
{
|
||||||
|
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
|
||||||
|
mother.AddComponent(
|
||||||
|
new Pregnant
|
||||||
|
{
|
||||||
|
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
|
||||||
|
DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
|
||||||
|
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.HasComponent<AnimalNeeds>())
|
||||||
|
{
|
||||||
|
a.GetComponent<AnimalNeeds>().Mating = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.HasComponent<AnimalNeeds>())
|
||||||
|
{
|
||||||
|
b.GetComponent<AnimalNeeds>().Mating = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
|
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
|
||||||
@@ -544,3 +695,179 @@ public sealed class AnimalMortalitySystem : BaseSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Гон и влечение (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut и поднимает половое
|
||||||
|
/// влечение (<see cref="AnimalNeeds.Mating"/>); вне сезона снимает хедиф, влечение спадает. Только под
|
||||||
|
/// гоном влечение переходит порог действия — вне сезона зачатия фактически нет.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalRutSystem(
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
Climate climate,
|
||||||
|
HediffDef rut
|
||||||
|
) : QuerySystem<AnimalGrowth, AnimalOrganism, AnimalNeeds, Health>
|
||||||
|
{
|
||||||
|
private const float RutDrivePerDay = 1.2f;
|
||||||
|
private const float DecayPerDay = 1f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var days = clock.DeltaTime / secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var season = (int)climate.Season;
|
||||||
|
foreach (var (growths, organisms, needs, healths, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var n = needs.Span;
|
||||||
|
var h = healths.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
var inRut =
|
||||||
|
g[i].Stage >= AnimalFactory.StageAdult && season == o[i].Traits.BreedingSeason;
|
||||||
|
var state = h[i].State;
|
||||||
|
if (inRut)
|
||||||
|
{
|
||||||
|
state.Add(rut);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.Remove(rut.DefName);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var need = ref n[i];
|
||||||
|
need.Mating = inRut
|
||||||
|
? Math.Clamp(need.Mating + RutDrivePerDay * days, 0f, 1f)
|
||||||
|
: MathF.Max(0f, need.Mating - DecayPerDay * days);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Беременность и роды (фаза A4): тикает срок вынашивания; по его истечении рожает помёт — каждый
|
||||||
|
/// детёныш = <see cref="Genome.Breed"/> генома матери и отца (пол наследуется без YY), стадия Baby,
|
||||||
|
/// поколение матери + 1, рядом с матерью. Компонент <see cref="Pregnant"/> снимается. Жёсткий потолок
|
||||||
|
/// численности страхует от взрыва популяции (бум-крах остаётся ниже потолка).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalPregnancySystem : BaseSystem
|
||||||
|
{
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly AnimalSet _animals;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly int _maxAnimals;
|
||||||
|
private readonly Random _rng;
|
||||||
|
private readonly ArchetypeQuery<Pregnant, AnimalOrganism, Transform2D> _query;
|
||||||
|
private readonly ArchetypeQuery<AnimalOrganism> _census;
|
||||||
|
private readonly List<Birth> _births = [];
|
||||||
|
|
||||||
|
public AnimalPregnancySystem(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
int cellSize,
|
||||||
|
int maxAnimals,
|
||||||
|
int seed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_animals = animals;
|
||||||
|
_clock = clock;
|
||||||
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_maxAnimals = maxAnimals;
|
||||||
|
_rng = new Random(seed);
|
||||||
|
_query = store.Query<Pregnant, AnimalOrganism, Transform2D>();
|
||||||
|
_census = store.Query<AnimalOrganism>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
var days = _clock.DeltaTime / _secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_births.Clear();
|
||||||
|
foreach (var (pregnancies, organisms, transforms, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var p = pregnancies.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < p.Length; i++)
|
||||||
|
{
|
||||||
|
ref var preg = ref p[i];
|
||||||
|
preg.DueInDays -= days;
|
||||||
|
if (preg.DueInDays > 0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_births.Add(
|
||||||
|
new Birth
|
||||||
|
{
|
||||||
|
Mother = entities.EntityAt(i),
|
||||||
|
Species = o[i].Species,
|
||||||
|
MotherGenome = o[i].Genome,
|
||||||
|
FatherGenome = preg.FatherGenome,
|
||||||
|
Litter = preg.Litter,
|
||||||
|
Generation = o[i].Generation + 1,
|
||||||
|
Position = t[i].Position,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = _census.Count;
|
||||||
|
foreach (var birth in _births)
|
||||||
|
{
|
||||||
|
if (birth.Mother.HasComponent<Pregnant>())
|
||||||
|
{
|
||||||
|
birth.Mother.RemoveComponent<Pregnant>();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var k = 0; k < birth.Litter && count < _maxAnimals; k++)
|
||||||
|
{
|
||||||
|
var child = Genome.Breed(
|
||||||
|
birth.MotherGenome,
|
||||||
|
birth.FatherGenome,
|
||||||
|
_animals.GeneRegistry,
|
||||||
|
_rng
|
||||||
|
);
|
||||||
|
var offset =
|
||||||
|
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
|
||||||
|
AnimalFactory.Create(
|
||||||
|
_store,
|
||||||
|
_animals,
|
||||||
|
birth.Species,
|
||||||
|
birth.Position + offset,
|
||||||
|
ageDays: 0f,
|
||||||
|
child,
|
||||||
|
birth.Generation,
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Birth
|
||||||
|
{
|
||||||
|
public Entity Mother;
|
||||||
|
public int Species;
|
||||||
|
public Genome MotherGenome;
|
||||||
|
public Genome FatherGenome;
|
||||||
|
public int Litter;
|
||||||
|
public int Generation;
|
||||||
|
public Vector2 Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user