Животные A8 часть 1: настроение — ядро (ThoughtDef + Mood + MoodSystem)

Тип дефа ThoughtDef + thoughts.json (роды/спаривание/жажда — сила и длительность данными). Компонент Mood (значение 0..1, активность по тиру мозга, список затухающих мыслей) на всех животных; AnimalMoodSystem (тир 4+: настроение из непрерывного фона — средняя удовлетворённость убывающих нужд минус боль — плюс сумма мыслей). Реестр ThoughtSet + id-константы AnimalThoughts; триггеры: роды (PregnancySystem), спаривание и утоление жажды (ActionSystem). Низкое настроение подавляет спаривание (DecisionSystem). Команда mood [species]. Локализация thought.* ru/en. Сборка + --check-content чистые (11 типов дефов). Осталось A8.2 — вкладки инспектора.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 06:13:46 +03:00
co-authored by Claude Opus 4.8
parent 26bf83db64
commit 1b12831817
11 changed files with 360 additions and 4 deletions
+146 -1
View File
@@ -24,6 +24,18 @@ public static class AnimalActions
public const string Mate = "Mate";
}
/// <summary>
/// Id событийных мыслей настроения (фаза A8). Системы-источники события вешают мысль с этим id (через
/// <see cref="Content.ThoughtSet"/>); её сила/длительность/метка — данные (thoughts.json). Новая мысль на
/// новое событие = код-триггер + деф; правка чисел существующей — только JSON.
/// </summary>
public static class AnimalThoughts
{
public const string GaveBirth = "GaveBirth";
public const string Mated = "Mated";
public const string QuenchedThirst = "QuenchedThirst";
}
/// <summary>
/// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
/// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
@@ -197,6 +209,7 @@ public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDe
public sealed class AnimalDecisionSystem : BaseSystem
{
private const float Interval = 0.6f;
private const float MateMoodFloor = 0.35f; // ниже — стресс/истощение, зверь не спаривается (A8)
private readonly UtilityAi<AnimalContext> _brain;
private readonly GameClock _clock;
@@ -294,12 +307,15 @@ public sealed class AnimalDecisionSystem : BaseSystem
var pos = t[i].Position;
var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage);
var radius = VisionRadius(o[i].Species, o[i].Traits.Vision);
var self = entities.EntityAt(i);
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
var intelligence = AnimalFactory.Intelligence(
o[i].Traits,
entities.EntityAt(i).GetComponent<Health>().State
self.GetComponent<Health>().State
);
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent<Mood>();
var name =
_brain.Select(new AnimalContext(n[i].Values, _needs, intelligence))?.Name
?? AnimalActions.Wander;
@@ -324,6 +340,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
break;
case AnimalActions.Mate
when adult
&& (!mood.Active || mood.Value >= MateMoodFloor)
&& TryFindMate(
pos,
o[i].IsMale,
@@ -478,11 +495,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
public sealed class AnimalActionSystem : BaseSystem
{
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private readonly EntityStore _store;
private readonly PlantSet _plants;
private readonly AnimalSet _animalSet;
private readonly NeedSet _needs;
private readonly ThoughtSet _thoughts;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly RectF _bounds;
@@ -501,6 +520,7 @@ public sealed class AnimalActionSystem : BaseSystem
PlantSet plants,
AnimalSet animals,
NeedSet needs,
ThoughtSet thoughts,
GameClock clock,
float secondsPerDay,
RectF bounds
@@ -510,6 +530,7 @@ public sealed class AnimalActionSystem : BaseSystem
_plants = plants;
_animalSet = animals;
_needs = needs;
_thoughts = thoughts;
_clock = clock;
_secondsPerDay = secondsPerDay;
_bounds = bounds;
@@ -565,7 +586,21 @@ public sealed class AnimalActionSystem : BaseSystem
case AnimalActions.Drink:
if (MoveTo(ref pos, brain.Target, speed * seconds))
{
// Утоление сильной жажды — событийная мысль настроения (A8); уровень берём
// ДО восполнения, чтобы радость была от облегчения, а не от каждого глотка.
var thirst = _needs.NeedForAction(AnimalActions.Drink);
var parched =
thirst >= 0
&& thirst < values.Length
&& values[thirst] < ThirstReliefBelow;
Refill(values, AnimalActions.Drink, days);
if (parched)
{
entities
.EntityAt(i)
.GetComponent<Mood>()
.Add(_thoughts.Get(AnimalThoughts.QuenchedThirst));
}
}
break;
@@ -695,6 +730,10 @@ public sealed class AnimalActionSystem : BaseSystem
ResetMating(a, mateNeed);
ResetMating(b, mateNeed);
// Спаривание — положительная событийная мысль обоим партнёрам (A8).
var mated = _thoughts.Get(AnimalThoughts.Mated);
a.GetComponent<Mood>().Add(mated);
b.GetComponent<Mood>().Add(mated);
}
}
@@ -713,6 +752,103 @@ public sealed class AnimalActionSystem : BaseSystem
}
}
/// <summary>
/// Настроение (фаза A8): для особей тира 4+ (мозг ≥ порога) считает настроение из непрерывного фона
/// (удовлетворённость убывающих нужд минус боль из здоровья) и суммы затухающих событийных мыслей.
/// Мысли угасают со временем. У примитивных (тир &lt; 4) настроения нет (Active=false, мысли сбрасываются).
/// Гейт интеллектом — тем же порогом тира 4, что и высшие нужды (сон/секс). Низкое настроение влияет на
/// поведение (стресс не даёт спариваться — см. <see cref="AnimalDecisionSystem"/>) и видно в консоли/инспекторе.
/// </summary>
public sealed class AnimalMoodSystem(GameClock clock, float secondsPerDay, NeedSet needs)
: QuerySystem<Mood, AnimalNeeds, AnimalOrganism, Health>
{
/// <summary>Порог размера мозга (тир 4), с которого у особи появляется настроение.</summary>
public const float MoodMinBrain = 0.40f;
private const float NeutralBase = 0.35f; // настроение при полном истощении нужд (без боли/мыслей)
private const float NeedsWeight = 0.55f; // вклад средней удовлетворённости убывающих нужд
private const float PainWeight = 0.4f; // боль из здоровья тянет настроение вниз
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
foreach (var (moods, needsChunk, organisms, healths, _) in Query.Chunks)
{
var m = moods.Span;
var n = needsChunk.Span;
var o = organisms.Span;
var h = healths.Span;
for (var i = 0; i < m.Length; i++)
{
ref var mood = ref m[i];
if (o[i].Traits.BrainSize < MoodMinBrain)
{
mood.Active = false; // тир ниже зверя — настроения нет
mood.Value = 0.5f;
mood.Thoughts?.Clear();
continue;
}
mood.Active = true;
// 1) Затухание событийных мыслей + сумма их сдвигов.
var thoughtSum = 0f;
var list = mood.Thoughts;
if (list is not null)
{
for (var k = list.Count - 1; k >= 0; k--)
{
var t = list[k];
t.RemainingDays -= days;
if (t.RemainingDays <= 0f)
{
list.RemoveAt(k);
continue;
}
list[k] = t;
thoughtSum += t.Def.MoodOffset;
}
}
// 2) Непрерывный фон: средняя удовлетворённость убывающих нужд минус боль из здоровья.
var wellbeing = AvgDeplete(n[i].Values);
var pain = h[i].State?.Pain ?? 0f;
mood.Value = Math.Clamp(
NeutralBase + NeedsWeight * wellbeing - PainWeight * pain + thoughtSum,
0f,
1f
);
}
}
}
// Средняя удовлетворённость убывающих нужд (голод/жажда/усталость) [0,1]; нет нужд → 1 (нейтрально).
private float AvgDeplete(float[] values)
{
if (values is null)
{
return 1f;
}
var sum = 0f;
var count = 0;
for (var k = 0; k < needs.Count && k < values.Length; k++)
{
if (needs.IsDeplete(k))
{
sum += values[k];
count++;
}
}
return count > 0 ? sum / count : 1f;
}
}
/// <summary>
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
/// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
@@ -957,6 +1093,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly NeedSet _needs;
private readonly ThoughtSet _thoughts;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly int _cellSize;
@@ -970,6 +1107,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
EntityStore store,
AnimalSet animals,
NeedSet needs,
ThoughtSet thoughts,
GameClock clock,
float secondsPerDay,
int cellSize,
@@ -980,6 +1118,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
_store = store;
_animals = animals;
_needs = needs;
_thoughts = thoughts;
_clock = clock;
_secondsPerDay = secondsPerDay;
_cellSize = cellSize;
@@ -1035,6 +1174,12 @@ public sealed class AnimalPregnancySystem : BaseSystem
birth.Mother.RemoveComponent<Pregnant>();
}
// Роды — сильная положительная мысль матери (A8).
if (birth.Mother.HasComponent<Mood>())
{
birth.Mother.GetComponent<Mood>().Add(_thoughts.Get(AnimalThoughts.GaveBirth));
}
for (var k = 0; k < birth.Litter && count < _maxAnimals; k++)
{
var child = Genome.Breed(