Животные 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:
co-authored by
Claude Opus 4.8
parent
26bf83db64
commit
1b12831817
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "Thought",
|
||||||
|
// Мысли настроения (фаза A8): событие вешает на зверя временный сдвиг настроения, затухающий за
|
||||||
|
// durationDays. Триггер события — код (реестр AnimalThoughts), сила/длительность/метка — данные.
|
||||||
|
// Настроение есть только у тира 4+ (мозг ≥ 0.40); непрерывный фон (голод/жажда/усталость/боль)
|
||||||
|
// считается отдельно, мысли — поверх него. Негативные событийные мысли (потеря детёныша) придут с
|
||||||
|
// родительской опекой (горизонт) — пока горе идёт через непрерывный фон.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GaveBirth", "label": "thought.gaveBirth", "moodOffset": 0.25, "durationDays": 3 },
|
||||||
|
{ "defName": "Mated", "label": "thought.mated", "moodOffset": 0.15, "durationDays": 1.5 },
|
||||||
|
{ "defName": "QuenchedThirst", "label": "thought.quenchedThirst", "moodOffset": 0.1, "durationDays": 0.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -123,6 +123,9 @@
|
|||||||
"pawn.wolf": "wolf",
|
"pawn.wolf": "wolf",
|
||||||
"pawn.muffalo": "muffalo",
|
"pawn.muffalo": "muffalo",
|
||||||
"pawn.squirrel": "squirrel",
|
"pawn.squirrel": "squirrel",
|
||||||
|
"thought.gaveBirth": "gave birth",
|
||||||
|
"thought.mated": "mated",
|
||||||
|
"thought.quenchedThirst": "drank its fill",
|
||||||
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
||||||
"net.connecting": "connecting…",
|
"net.connecting": "connecting…",
|
||||||
"net.connected": "connected",
|
"net.connected": "connected",
|
||||||
|
|||||||
@@ -123,6 +123,9 @@
|
|||||||
"pawn.wolf": "волк",
|
"pawn.wolf": "волк",
|
||||||
"pawn.muffalo": "муффало",
|
"pawn.muffalo": "муффало",
|
||||||
"pawn.squirrel": "белка",
|
"pawn.squirrel": "белка",
|
||||||
|
"thought.gaveBirth": "родила потомство",
|
||||||
|
"thought.mated": "спарилась",
|
||||||
|
"thought.quenchedThirst": "напилась вволю",
|
||||||
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||||
"net.connecting": "подключение…",
|
"net.connecting": "подключение…",
|
||||||
"net.connected": "подключено",
|
"net.connected": "подключено",
|
||||||
|
|||||||
+12
-2
@@ -1,7 +1,17 @@
|
|||||||
# Животные (зоология)
|
# Животные (зоология)
|
||||||
|
|
||||||
> Статус: **A1–A7 + рефактор расширяемости реализованы**, остальное (A8) по плану. Это живой
|
> Статус: **A1–A7 + A8 часть 1 (ядро настроения) реализованы**, осталось A8 часть 2 (инспектор). Это
|
||||||
> план — правится по мере реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
|
> живой план — правится по мере реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
|
||||||
|
>
|
||||||
|
> **A8 часть 1 (готово): настроение — ядро.** Тип дефа `ThoughtDef` + `thoughts.json` (мысли: роды/
|
||||||
|
> спаривание/утоление жажды — сила и длительность данными); компонент `Mood` (значение 0..1, активность по
|
||||||
|
> тиру мозга, список затухающих мыслей) на всех животных; `AnimalMoodSystem` (тир 4+: настроение из
|
||||||
|
> непрерывного фона — средняя удовлетворённость убывающих нужд минус боль из здоровья — плюс сумма мыслей;
|
||||||
|
> мысли угасают). Реестр `ThoughtSet` + id-константы `AnimalThoughts`; событийные триггеры: роды
|
||||||
|
> (`AnimalPregnancySystem`), спаривание и утоление сильной жажды (`AnimalActionSystem`). **Эффект на
|
||||||
|
> поведение**: низкое настроение (стресс/истощение) подавляет спаривание в `AnimalDecisionSystem`. Команда
|
||||||
|
> `mood [species]`. Сборка + `--check-content` чистые (11 типов дефов). Осталось A8.2: вкладки инспектора
|
||||||
|
> (needs/health/mood) — сейчас животные в инспекторе не осматриваются (он только растительный).
|
||||||
>
|
>
|
||||||
> **A7 (готово): мозг → интеллект → гейт нужд/действий.** Эффективный интеллект = `brainSize` (ген) ×
|
> **A7 (готово): мозг → интеллект → гейт нужд/действий.** Эффективный интеллект = `brainSize` (ген) ×
|
||||||
> `Consciousness` (capacity из здоровья) — `AnimalFactory.Intelligence`. `AnimalContext` гейтит выбор ИИ:
|
> `Consciousness` (capacity из здоровья) — `AnimalFactory.Intelligence`. `AnimalContext` гейтит выбор ИИ:
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ public sealed class GameContent
|
|||||||
defs.RegisterType<AnimalDef>("Animal");
|
defs.RegisterType<AnimalDef>("Animal");
|
||||||
defs.RegisterType<HediffDef>("Hediff");
|
defs.RegisterType<HediffDef>("Hediff");
|
||||||
defs.RegisterType<NeedDef>("Need");
|
defs.RegisterType<NeedDef>("Need");
|
||||||
|
defs.RegisterType<ThoughtDef>("Thought");
|
||||||
defs.RegisterType<BodyDef>("Body");
|
defs.RegisterType<BodyDef>("Body");
|
||||||
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||||
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||||
|
|||||||
@@ -320,6 +320,21 @@ public sealed class NeedDef : Def
|
|||||||
public float MinBrain { get; init; }
|
public float MinBrain { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Мысль настроения (Defs/thoughts.json) как ДАННЫЕ (фаза A8, модель thoughts RimWorld): событие
|
||||||
|
/// (роды/спаривание/утоление жажды/…) вешает на особь временный сдвиг настроения, затухающий за
|
||||||
|
/// <see cref="DurationDays"/>. Триггеры событий — код (реестр <c>AnimalThoughts</c>), а величины/метка —
|
||||||
|
/// данные: модер правит силу и длительность мысли одной записью JSON, без пересборки.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThoughtDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Сдвиг настроения [-1,1] (положительный — радость, отрицательный — горе).</summary>
|
||||||
|
public float MoodOffset { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль живёт, прежде чем угаснуть.</summary>
|
||||||
|
public float DurationDays { get; init; } = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
|
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
|
||||||
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
|
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реестр мыслей настроения (<see cref="ThoughtDef"/>) с доступом по id (фаза A8): системы-источники
|
||||||
|
/// событий (роды/спаривание/жажда) берут нужную мысль по строковому id (см. <c>AnimalThoughts</c>) и
|
||||||
|
/// вешают её на особь. Метка/сила/длительность мысли — данные; модер правит их в thoughts.json. Зеркало
|
||||||
|
/// <see cref="NeedSet"/> по духу. Удалил модер мысль из дефов — <see cref="Get"/> вернёт null (no-op).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThoughtSet
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ThoughtDef> _byId;
|
||||||
|
|
||||||
|
/// <summary>Строит реестр из всех <see cref="ThoughtDef"/> мода.</summary>
|
||||||
|
public ThoughtSet(GameContent content) =>
|
||||||
|
_byId = content.Defs.All<ThoughtDef>().ToDictionary(t => t.DefName, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Число мыслей.</summary>
|
||||||
|
public int Count => _byId.Count;
|
||||||
|
|
||||||
|
/// <summary>Деф мысли по id или null, если такой нет (мод убрал).</summary>
|
||||||
|
public ThoughtDef? Get(string id) => _byId.GetValueOrDefault(id);
|
||||||
|
}
|
||||||
@@ -214,6 +214,7 @@ public sealed class WorldScene : Scene
|
|||||||
// Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет
|
// Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет
|
||||||
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
|
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
|
||||||
var shore = ComputeShore();
|
var shore = ComputeShore();
|
||||||
|
var thoughts = new ThoughtSet(content);
|
||||||
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs));
|
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
|
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
|
||||||
@@ -229,6 +230,8 @@ public sealed class WorldScene : Scene
|
|||||||
_config.Seed + 0x3EAD
|
_config.Seed + 0x3EAD
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Настроение (A8) до выбора решения: стресс (низкое настроение) подавляет спаривание.
|
||||||
|
UpdateSystems.Add(new AnimalMoodSystem(Context.Clock, SecondsPerDay, _needs));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new AnimalDecisionSystem(
|
new AnimalDecisionSystem(
|
||||||
Store,
|
Store,
|
||||||
@@ -246,6 +249,7 @@ public sealed class WorldScene : Scene
|
|||||||
_plants,
|
_plants,
|
||||||
_animals,
|
_animals,
|
||||||
_needs,
|
_needs,
|
||||||
|
thoughts,
|
||||||
Context.Clock,
|
Context.Clock,
|
||||||
SecondsPerDay,
|
SecondsPerDay,
|
||||||
_bounds
|
_bounds
|
||||||
@@ -260,6 +264,7 @@ public sealed class WorldScene : Scene
|
|||||||
Store,
|
Store,
|
||||||
_animals,
|
_animals,
|
||||||
_needs,
|
_needs,
|
||||||
|
thoughts,
|
||||||
Context.Clock,
|
Context.Clock,
|
||||||
SecondsPerDay,
|
SecondsPerDay,
|
||||||
CellSize,
|
CellSize,
|
||||||
@@ -738,6 +743,11 @@ public sealed class WorldScene : Scene
|
|||||||
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
|
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
|
||||||
(c, args) => RunIntel(c, content, args)
|
(c, args) => RunIntel(c, content, args)
|
||||||
);
|
);
|
||||||
|
console.Register(
|
||||||
|
"mood",
|
||||||
|
"mood [species] — live population mood (avg/min/max) and content/stressed counts",
|
||||||
|
(c, args) => RunMood(c, args)
|
||||||
|
);
|
||||||
console.Register(
|
console.Register(
|
||||||
"menu",
|
"menu",
|
||||||
"menu — return to the main menu",
|
"menu — return to the main menu",
|
||||||
@@ -1071,6 +1081,79 @@ public sealed class WorldScene : Scene
|
|||||||
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "—")}");
|
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "—")}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Наблюдаемость настроения (фаза A8): среднее/мин/макс настроения живой популяции по виду и счётчики
|
||||||
|
// довольных (>0.7) / угнетённых (<0.35). Считает только особей с активным настроением (тир 4+).
|
||||||
|
private void RunMood(DevConsole console, string[] args)
|
||||||
|
{
|
||||||
|
if (_animals.Count == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("no animal species");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = new int[_animals.Count];
|
||||||
|
var sum = new double[_animals.Count];
|
||||||
|
var min = new float[_animals.Count];
|
||||||
|
var max = new float[_animals.Count];
|
||||||
|
var content_ = new int[_animals.Count];
|
||||||
|
var stressed = new int[_animals.Count];
|
||||||
|
Array.Fill(min, 1f);
|
||||||
|
|
||||||
|
Store
|
||||||
|
.Query<AnimalOrganism, Mood>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref AnimalOrganism o, ref Mood m, Entity _) =>
|
||||||
|
{
|
||||||
|
if (!m.Active)
|
||||||
|
{
|
||||||
|
return; // тир ниже зверя — настроения нет
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = o.Species;
|
||||||
|
count[s]++;
|
||||||
|
sum[s] += m.Value;
|
||||||
|
min[s] = MathF.Min(min[s], m.Value);
|
||||||
|
max[s] = MathF.Max(max[s], m.Value);
|
||||||
|
if (m.Value > 0.7f)
|
||||||
|
{
|
||||||
|
content_[s]++;
|
||||||
|
}
|
||||||
|
else if (m.Value < 0.35f)
|
||||||
|
{
|
||||||
|
stressed[s]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var filter = args.Length > 0 ? args[0] : null;
|
||||||
|
var any = false;
|
||||||
|
for (var s = 0; s < _animals.Count; s++)
|
||||||
|
{
|
||||||
|
var name = _animals[s].Def.DefName;
|
||||||
|
if (
|
||||||
|
count[s] == 0
|
||||||
|
|| (
|
||||||
|
filter is not null
|
||||||
|
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
any = true;
|
||||||
|
console.WriteLine(
|
||||||
|
$"{name}: n={count[s]}, mood {sum[s] / count[s]:0.##} (min {min[s]:0.##}, max {max[s]:0.##}), "
|
||||||
|
+ $"content {content_[s]}, stressed {stressed[s]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any)
|
||||||
|
{
|
||||||
|
console.WriteLine(filter is null ? "no animals with mood" : $"no '{filter}' with mood");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string ProductLabel(GameContent content, string productDefName) =>
|
private static string ProductLabel(GameContent content, string productDefName) =>
|
||||||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||||
? content.Languages.Get(product.Label)
|
? content.Languages.Get(product.Label)
|
||||||
|
|||||||
@@ -64,7 +64,14 @@ public static class AnimalFactory
|
|||||||
TargetPlant = -1,
|
TargetPlant = -1,
|
||||||
TargetMate = -1,
|
TargetMate = -1,
|
||||||
},
|
},
|
||||||
new Health { State = HealthState.Create(sp.Body, traits.BodySize) }
|
new Health { State = HealthState.Create(sp.Body, traits.BodySize) },
|
||||||
|
// Настроение (фаза A8): нейтральное на старте, активность по тиру мозга считает MoodSystem.
|
||||||
|
new Mood
|
||||||
|
{
|
||||||
|
Value = 0.5f,
|
||||||
|
Active = false,
|
||||||
|
Thoughts = [],
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -326,6 +326,60 @@ public struct Corpse : IComponent
|
|||||||
public float Meat;
|
public float Meat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
|
||||||
|
public struct ThoughtInstance
|
||||||
|
{
|
||||||
|
/// <summary>Деф мысли (сдвиг настроения и длительность).</summary>
|
||||||
|
public Content.ThoughtDef Def;
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||||
|
public float RemainingDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настроение особи (фаза A8): значение [0,1] и активные событийные мысли. Считается
|
||||||
|
/// <see cref="Sim.AnimalMoodSystem"/> из непрерывного фона (нужды/боль) и суммы мыслей. Есть только у
|
||||||
|
/// тира 4+ (<see cref="Active"/> по размеру мозга) — у примитивных настроения нет. Низкое настроение
|
||||||
|
/// влияет на поведение (стресс не даёт спариваться) и видно в инспекторе. Список мыслей — managed-ссылка
|
||||||
|
/// (как <c>AnimalNeeds.Values</c>): события добавляют мысли через <see cref="Add"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct Mood : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Текущее настроение [0,1]; у неактивного (тир < 4) держится нейтральным.</summary>
|
||||||
|
public float Value;
|
||||||
|
|
||||||
|
/// <summary>Есть ли у особи настроение (гейт тиром мозга) — иначе фон/мысли не считаются.</summary>
|
||||||
|
public bool Active;
|
||||||
|
|
||||||
|
/// <summary>Активные событийные мысли (затухают со временем).</summary>
|
||||||
|
public List<ThoughtInstance> Thoughts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вешает событийную мысль (или обновляет её длительность, если уже висит — без накопления
|
||||||
|
/// дубликатов). Вызывается из систем-источников событий (роды, спаривание, утоление жажды).
|
||||||
|
/// </summary>
|
||||||
|
public readonly void Add(Content.ThoughtDef? def)
|
||||||
|
{
|
||||||
|
if (Thoughts is null || def is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Thoughts.Count; i++)
|
||||||
|
{
|
||||||
|
if (Thoughts[i].Def == def)
|
||||||
|
{
|
||||||
|
var refreshed = Thoughts[i];
|
||||||
|
refreshed.RemainingDays = def.DurationDays;
|
||||||
|
Thoughts[i] = refreshed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thoughts.Add(new ThoughtInstance { Def = def, RemainingDays = def.DurationDays });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
|
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
|
||||||
/// срока вынашивания и размер помёта. По истечении срока <see cref="Sim.AnimalPregnancySystem"/>
|
/// срока вынашивания и размер помёта. По истечении срока <see cref="Sim.AnimalPregnancySystem"/>
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ public static class AnimalActions
|
|||||||
public const string Mate = "Mate";
|
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>
|
/// <summary>
|
||||||
/// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
|
/// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
|
||||||
/// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
|
/// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
|
||||||
@@ -197,6 +209,7 @@ public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDe
|
|||||||
public sealed class AnimalDecisionSystem : BaseSystem
|
public sealed class AnimalDecisionSystem : BaseSystem
|
||||||
{
|
{
|
||||||
private const float Interval = 0.6f;
|
private const float Interval = 0.6f;
|
||||||
|
private const float MateMoodFloor = 0.35f; // ниже — стресс/истощение, зверь не спаривается (A8)
|
||||||
|
|
||||||
private readonly UtilityAi<AnimalContext> _brain;
|
private readonly UtilityAi<AnimalContext> _brain;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
@@ -294,12 +307,15 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
var pos = t[i].Position;
|
var pos = t[i].Position;
|
||||||
var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage);
|
var adult = AnimalFactory.IsAdult(_animalSet[o[i].Species], gr[i].Stage);
|
||||||
var radius = VisionRadius(o[i].Species, o[i].Traits.Vision);
|
var radius = VisionRadius(o[i].Species, o[i].Traits.Vision);
|
||||||
|
var self = entities.EntityAt(i);
|
||||||
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
|
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
|
||||||
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
|
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
|
||||||
var intelligence = AnimalFactory.Intelligence(
|
var intelligence = AnimalFactory.Intelligence(
|
||||||
o[i].Traits,
|
o[i].Traits,
|
||||||
entities.EntityAt(i).GetComponent<Health>().State
|
self.GetComponent<Health>().State
|
||||||
);
|
);
|
||||||
|
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
|
||||||
|
var mood = self.GetComponent<Mood>();
|
||||||
var name =
|
var name =
|
||||||
_brain.Select(new AnimalContext(n[i].Values, _needs, intelligence))?.Name
|
_brain.Select(new AnimalContext(n[i].Values, _needs, intelligence))?.Name
|
||||||
?? AnimalActions.Wander;
|
?? AnimalActions.Wander;
|
||||||
@@ -324,6 +340,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
break;
|
break;
|
||||||
case AnimalActions.Mate
|
case AnimalActions.Mate
|
||||||
when adult
|
when adult
|
||||||
|
&& (!mood.Active || mood.Value >= MateMoodFloor)
|
||||||
&& TryFindMate(
|
&& TryFindMate(
|
||||||
pos,
|
pos,
|
||||||
o[i].IsMale,
|
o[i].IsMale,
|
||||||
@@ -478,11 +495,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
public sealed class AnimalActionSystem : BaseSystem
|
public sealed class AnimalActionSystem : BaseSystem
|
||||||
{
|
{
|
||||||
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
|
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
|
||||||
|
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
|
||||||
|
|
||||||
private readonly EntityStore _store;
|
private readonly EntityStore _store;
|
||||||
private readonly PlantSet _plants;
|
private readonly PlantSet _plants;
|
||||||
private readonly AnimalSet _animalSet;
|
private readonly AnimalSet _animalSet;
|
||||||
private readonly NeedSet _needs;
|
private readonly NeedSet _needs;
|
||||||
|
private readonly ThoughtSet _thoughts;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
private readonly float _secondsPerDay;
|
private readonly float _secondsPerDay;
|
||||||
private readonly RectF _bounds;
|
private readonly RectF _bounds;
|
||||||
@@ -501,6 +520,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
PlantSet plants,
|
PlantSet plants,
|
||||||
AnimalSet animals,
|
AnimalSet animals,
|
||||||
NeedSet needs,
|
NeedSet needs,
|
||||||
|
ThoughtSet thoughts,
|
||||||
GameClock clock,
|
GameClock clock,
|
||||||
float secondsPerDay,
|
float secondsPerDay,
|
||||||
RectF bounds
|
RectF bounds
|
||||||
@@ -510,6 +530,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
_plants = plants;
|
_plants = plants;
|
||||||
_animalSet = animals;
|
_animalSet = animals;
|
||||||
_needs = needs;
|
_needs = needs;
|
||||||
|
_thoughts = thoughts;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
_secondsPerDay = secondsPerDay;
|
_secondsPerDay = secondsPerDay;
|
||||||
_bounds = bounds;
|
_bounds = bounds;
|
||||||
@@ -565,7 +586,21 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
case AnimalActions.Drink:
|
case AnimalActions.Drink:
|
||||||
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
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);
|
Refill(values, AnimalActions.Drink, days);
|
||||||
|
if (parched)
|
||||||
|
{
|
||||||
|
entities
|
||||||
|
.EntityAt(i)
|
||||||
|
.GetComponent<Mood>()
|
||||||
|
.Add(_thoughts.Get(AnimalThoughts.QuenchedThirst));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -695,6 +730,10 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
|
|
||||||
ResetMating(a, mateNeed);
|
ResetMating(a, mateNeed);
|
||||||
ResetMating(b, 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+ (мозг ≥ порога) считает настроение из непрерывного фона
|
||||||
|
/// (удовлетворённость убывающих нужд минус боль из здоровья) и суммы затухающих событийных мыслей.
|
||||||
|
/// Мысли угасают со временем. У примитивных (тир < 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>
|
/// <summary>
|
||||||
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
|
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
|
||||||
/// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
|
/// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
|
||||||
@@ -957,6 +1093,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
|||||||
private readonly EntityStore _store;
|
private readonly EntityStore _store;
|
||||||
private readonly AnimalSet _animals;
|
private readonly AnimalSet _animals;
|
||||||
private readonly NeedSet _needs;
|
private readonly NeedSet _needs;
|
||||||
|
private readonly ThoughtSet _thoughts;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
private readonly float _secondsPerDay;
|
private readonly float _secondsPerDay;
|
||||||
private readonly int _cellSize;
|
private readonly int _cellSize;
|
||||||
@@ -970,6 +1107,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
|||||||
EntityStore store,
|
EntityStore store,
|
||||||
AnimalSet animals,
|
AnimalSet animals,
|
||||||
NeedSet needs,
|
NeedSet needs,
|
||||||
|
ThoughtSet thoughts,
|
||||||
GameClock clock,
|
GameClock clock,
|
||||||
float secondsPerDay,
|
float secondsPerDay,
|
||||||
int cellSize,
|
int cellSize,
|
||||||
@@ -980,6 +1118,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
|||||||
_store = store;
|
_store = store;
|
||||||
_animals = animals;
|
_animals = animals;
|
||||||
_needs = needs;
|
_needs = needs;
|
||||||
|
_thoughts = thoughts;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
_secondsPerDay = secondsPerDay;
|
_secondsPerDay = secondsPerDay;
|
||||||
_cellSize = cellSize;
|
_cellSize = cellSize;
|
||||||
@@ -1035,6 +1174,12 @@ public sealed class AnimalPregnancySystem : BaseSystem
|
|||||||
birth.Mother.RemoveComponent<Pregnant>();
|
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++)
|
for (var k = 0; k < birth.Litter && count < _maxAnimals; k++)
|
||||||
{
|
{
|
||||||
var child = Genome.Breed(
|
var child = Genome.Breed(
|
||||||
|
|||||||
Reference in New Issue
Block a user