Животные A7: мозг → интеллект → гейт нужд/действий

Эффективный интеллект = brainSize (ген) × Consciousness (capacity из здоровья), AnimalFactory.Intelligence. AnimalContext гейтит выбор ИИ: нужда с minBrain выше интеллекта особи в выбор не входит (AnimalDecisionSystem читает Health поэлементно). Пороги в needs.json — тиры мозга (голод 0.0, жажда 0.25, сон/секс 0.40; у оленя мозг ~0.45). Обратная связь от повреждения мозга: болезнь/боль/кровопотеря роняют сознание → интеллект падает → первыми отключаются сон/спаривание. Команда intel <species> [consciousness]. Сборка + --check-content чистые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 06:00:11 +03:00
co-authored by Claude Opus 4.8
parent 9f8ff93591
commit 26bf83db64
5 changed files with 194 additions and 28 deletions
+9 -5
View File
@@ -2,15 +2,19 @@
"type": "Need",
// Нужды как данные (фаза A2, рефактор расширяемости): добавить нужду виду = добавить запись здесь.
// kind=deplete убывает и утоляется действием; kind=drive растёт под хедифом (гон) и сбрасывается им.
// action — id поведения-исполнителя (реестр в коде); minBrain — гейт интеллектом (включится в A7).
// action — id поведения-исполнителя (реестр в коде). minBrain — гейт интеллектом (активен с A7):
// нужда, чей minBrain выше эффективного интеллекта особи (brainSize × Consciousness), в выбор ИИ не
// входит. Пороги — тиры мозга: 0.0 голод-рефлекс, 0.25 жажда (низший позвоночный), 0.40 сон/секс
// (зверь). У оленя мозг ~0.45: здоров — весь набор; повреждение мозга/болезнь роняют сознание →
// эффективный интеллект падает → первыми отключаются сон/спаривание (обратная связь A7).
"defs": [
{ "defName": "Hunger", "label": "need.hunger", "kind": "deplete", "action": "Eat",
"decayPerDay": 0.55, "feedPerDay": 4, "lethal": true, "minBrain": 0.25 },
"decayPerDay": 0.55, "feedPerDay": 4, "lethal": true, "minBrain": 0.0 },
{ "defName": "Thirst", "label": "need.thirst", "kind": "deplete", "action": "Drink",
"decayPerDay": 0.9, "feedPerDay": 8, "lethal": true, "minBrain": 0.2 },
"decayPerDay": 0.9, "feedPerDay": 8, "lethal": true, "minBrain": 0.25 },
{ "defName": "Rest", "label": "need.rest", "kind": "deplete", "action": "Sleep",
"decayPerDay": 0.7, "feedPerDay": 3, "lethal": false, "minBrain": 0.45 },
"decayPerDay": 0.7, "feedPerDay": 3, "lethal": false, "minBrain": 0.40 },
{ "defName": "Mating", "label": "need.mating", "kind": "drive", "action": "Mate",
"decayPerDay": 1.0, "risePerDay": 1.2, "risesUnder": "Rut", "minBrain": 0.45 }
"decayPerDay": 1.0, "risePerDay": 1.2, "risesUnder": "Rut", "minBrain": 0.40 }
]
}
+9 -1
View File
@@ -1,8 +1,16 @@
# Животные (зоология)
> Статус: **A1A4 + рефактор расширяемости + A5(труп) реализованы**, остальное по плану. Это живой
> Статус: **A1A7 + рефактор расширяемости реализованы**, остальное (A8) по плану. Это живой
> план — правится по мере реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md).
>
> **A7 (готово): мозг → интеллект → гейт нужд/действий.** Эффективный интеллект = `brainSize` (ген) ×
> `Consciousness` (capacity из здоровья) — `AnimalFactory.Intelligence`. `AnimalContext` гейтит выбор ИИ:
> нужда с `minBrain` выше интеллекта особи в выбор не входит (`AnimalDecisionSystem` читает `Health`
> поэлементно). Пороги в `needs.json` — тиры мозга (голод 0.0, жажда 0.25, сон/секс 0.40; у оленя мозг
> ~0.45 → здоров весь набор). **Обратная связь от повреждения мозга**: болезнь/боль/кровопотеря роняют
> сознание → эффективный интеллект падает → первыми отключаются сон/спаривание (лихорадящий олень
> сводится к еде/питью). Команда `intel <species> [consciousness]`. Сборка + `--check-content` чистые.
>
> **A5 часть 1 (готово): смерть → труп → разложение.** `Corpse`-компонент + `CorpseSystem`
> (Fresh→Rotting→Skeleton→исчезает, скорость от температуры, тинт по стадии); смерть теперь оставляет
> труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` +
+79 -7
View File
@@ -215,7 +215,9 @@ public sealed class WorldScene : Scene
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
var shore = ComputeShore();
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs));
UpdateSystems.Add(new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut")));
UpdateSystems.Add(
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
);
UpdateSystems.Add(
new AnimalHealthSystem(
Store,
@@ -547,7 +549,8 @@ public sealed class WorldScene : Scene
var count = Math.Max(1, (int)MathF.Round(landCells / 1000f * def.SpawnPer1000Cells));
var maxAge =
(def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f) * 0.85f;
(def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f)
* 0.85f;
var placed = 0;
var attempts = 0;
while (placed < count && attempts < count * 50)
@@ -730,6 +733,11 @@ public sealed class WorldScene : Scene
"infect [hediff] — give every animal a disease (default Fever) to test the health system",
(c, args) => RunInfect(c, content, args)
);
console.Register(
"intel",
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
(c, args) => RunIntel(c, content, args)
);
console.Register(
"menu",
"menu — return to the main menu",
@@ -914,7 +922,10 @@ public sealed class WorldScene : Scene
var name = _animals[s].Def.DefName;
if (
count[s] == 0
|| (filter is not null && !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase))
|| (
filter is not null
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
)
)
{
continue;
@@ -960,10 +971,18 @@ public sealed class WorldScene : Scene
console.WriteLine($"{def.DefName} body '{body.DefName}': {body.Parts.Length} parts");
foreach (var part in body.Parts)
{
var caps = part.Capacities.Count > 0
? " [" + string.Join(", ", part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")) + "]"
: "";
console.WriteLine($" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}");
var caps =
part.Capacities.Count > 0
? " ["
+ string.Join(
", ",
part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")
)
+ "]"
: "";
console.WriteLine(
$" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}"
);
}
var state = HealthState.Create(body, 1f);
@@ -999,6 +1018,59 @@ public sealed class WorldScene : Scene
console.WriteLine($"infected {count} animals with {def.DefName}");
}
// Наблюдаемость интеллекта (фаза A7): мозг особи (ген) × сознание = эффективный интеллект, который
// гейтит набор нужд/действий. Показывает активные/отключённые нужды при заданном уровне сознания
// (по умолчанию 1 — здоров; меньше — имитация повреждения мозга/болезни/боли).
private void RunIntel(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Animal");
if (args.Length == 0)
{
console.WriteLine(
"usage: intel <species> [consciousness 0..1] [seed] e.g. intel Deer 0.6"
);
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
{
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var consciousness =
args.Length > 1
? Math.Clamp(
float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture),
0f,
1f
)
: 1f;
var seed = args.Length > 2 ? int.Parse(args[2]) : Random.Shared.Next();
var index = _animals.IndexOf(def);
var genome = _animals.GenerateGenome(index, new Random(seed));
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
var intelligence = traits.BrainSize * consciousness;
console.WriteLine(
$"{def.DefName} (seed {seed}): brain {traits.BrainSize:0.##} x consciousness {consciousness:0.##} "
+ $"= intelligence {intelligence:0.###}"
);
var active = new List<string>();
var gated = new List<string>();
for (var k = 0; k < _needs.Count; k++)
{
var need = _needs[k];
(need.MinBrain > intelligence ? gated : active).Add(
$"{need.DefName}({need.MinBrain:0.##})"
);
}
console.WriteLine($" active: {(active.Count > 0 ? string.Join(", ", active) : "")}");
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "")}");
}
private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
+24 -2
View File
@@ -42,7 +42,10 @@ public static class AnimalFactory
sprite.Color = FurTint(traits);
return store.CreateEntity(
new Transform2D(position, scale: new Vector2(Scale(sp, traits, stage, region, cellSize))),
new Transform2D(
position,
scale: new Vector2(Scale(sp, traits, stage, region, cellSize))
),
sprite,
new AnimalOrganism
{
@@ -97,12 +100,31 @@ public static class AnimalFactory
);
}
/// <summary>
/// Эффективный интеллект особи (фаза A7): размер мозга (ген) × сознание (capacity из здоровья).
/// Повреждение мозга/болезнь/боль/кровопотеря снижают Consciousness → динамически роняют интеллект,
/// что гейтит набор доступных нужд/действий (см. <see cref="AnimalContext"/>). Вид без частей тела
/// (сознание не вычисляется) считается полностью сознательным — чтобы не отключить ему всё.
/// </summary>
public static float Intelligence(in AnimalPhenotype traits, HealthState? health)
{
var consciousness = health is { Parts.Length: > 0 }
? health.Capacity(AnimalCapacities.Consciousness)
: 1f;
return traits.BrainSize * consciousness;
}
/// <summary>Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).</summary>
public static bool IsMaleGenome(Genome genome) =>
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
/// <summary>Индекс стадии по возрасту: последняя стадия, чей возраст входа (EnterAt × якорь) ≤ возраст.</summary>
public static int StageAt(float ageDays, float maturity, float lifespan, AnimalStageDef[] stages)
public static int StageAt(
float ageDays,
float maturity,
float lifespan,
AnimalStageDef[] stages
)
{
var stage = 0;
for (var i = 1; i < stages.Length; i++)
+73 -13
View File
@@ -61,13 +61,36 @@ public struct AnimalBrain : IComponent
public float DecideIn;
}
/// <summary>Снимок нужд зверя для соображений utility-выбора (доступ по индексу нужды).</summary>
public readonly struct AnimalContext(float[] values)
/// <summary>
/// Снимок нужд зверя для соображений utility-выбора (доступ по индексу нужды) с гейтом интеллектом
/// (фаза A7): нужда, чей <see cref="NeedDef.MinBrain"/> выше эффективного интеллекта особи
/// (brainSize × Consciousness), в выбор не входит.
/// </summary>
public readonly struct AnimalContext(float[] values, NeedSet needs, float intelligence)
{
private readonly float[] _values = values;
private readonly NeedSet _needs = needs;
private readonly float _intelligence = intelligence;
/// <summary>Значение нужды по индексу (0, если индекс вне диапазона).</summary>
public float Get(int index) => index < _values.Length ? _values[index] : 0f;
/// <summary>
/// Значение нужды по индексу. Если её порог мозга выше интеллекта особи — нужда недоступна и
/// возвращается её «не-срочный» край (deplete → 1 «сыта», drive → 0 «без влечения»), чтобы
/// соображение дало ~0 и действие не выбралось. Индекс вне диапазона → 0.
/// </summary>
public float Get(int index)
{
if (index >= _values.Length)
{
return 0f;
}
if (_needs[index].MinBrain > _intelligence)
{
return _needs.IsDrive(index) ? 0f : 1f;
}
return _values[index];
}
}
/// <summary>
@@ -112,7 +135,11 @@ public sealed class AnimalNeedsSystem(GameClock clock, float secondsPerDay, Need
}
else
{
values[k] = Math.Clamp(values[k] - def.DecayPerDay * metabolism * days, 0f, 1f);
values[k] = Math.Clamp(
values[k] - def.DecayPerDay * metabolism * days,
0f,
1f
);
if (def.Lethal && values[k] <= 0f)
{
lethalEmpty = true;
@@ -174,6 +201,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
private readonly UtilityAi<AnimalContext> _brain;
private readonly GameClock _clock;
private readonly AnimalSet _animalSet;
private readonly NeedSet _needs;
private readonly int _cellSize;
private readonly Vector2[] _shore;
private readonly Random _rng;
@@ -199,10 +227,17 @@ public sealed class AnimalDecisionSystem : BaseSystem
_brain = BuildBrain(needs);
_clock = clock;
_animalSet = animals;
_needs = needs;
_cellSize = cellSize;
_shore = shore;
_rng = new Random(seed);
_animals = store.Query<AnimalNeeds, AnimalBrain, AnimalOrganism, AnimalGrowth, Transform2D>();
_animals = store.Query<
AnimalNeeds,
AnimalBrain,
AnimalOrganism,
AnimalGrowth,
Transform2D
>();
_plants = store.Query<Transform2D, PlantGrowth>();
}
@@ -237,7 +272,9 @@ public sealed class AnimalDecisionSystem : BaseSystem
protected override void OnUpdateGroup()
{
var delta = _clock.DeltaTime;
foreach (var (needsChunk, brains, organisms, growths, transforms, _) in _animals.Chunks)
foreach (
var (needsChunk, brains, organisms, growths, transforms, entities) in _animals.Chunks
)
{
var n = needsChunk.Span;
var b = brains.Span;
@@ -257,8 +294,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);
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
var intelligence = AnimalFactory.Intelligence(
o[i].Traits,
entities.EntityAt(i).GetComponent<Health>().State
);
var name =
_brain.Select(new AnimalContext(n[i].Values))?.Name ?? AnimalActions.Wander;
_brain.Select(new AnimalContext(n[i].Values, _needs, intelligence))?.Name
?? AnimalActions.Wander;
switch (name)
{
@@ -280,7 +324,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
break;
case AnimalActions.Mate
when adult
&& TryFindMate(pos, o[i].IsMale, radius, out var mateId, out var matePos):
&& TryFindMate(
pos,
o[i].IsMale,
radius,
out var mateId,
out var matePos
):
brain.Action = AnimalActions.Mate;
brain.TargetPlant = -1;
brain.TargetMate = mateId;
@@ -388,7 +438,10 @@ public sealed class AnimalDecisionSystem : BaseSystem
var tt = transforms.Span;
for (var i = 0; i < tt.Length; i++)
{
if (oo[i].IsMale == selfMale || !AnimalFactory.IsAdult(_animalSet[oo[i].Species], gg[i].Stage))
if (
oo[i].IsMale == selfMale
|| !AnimalFactory.IsAdult(_animalSet[oo[i].Species], gg[i].Stage)
)
{
continue; // тот же пол / не взрослый (self отсеивается по полу)
}
@@ -475,7 +528,9 @@ public sealed class AnimalActionSystem : BaseSystem
_eaten.Clear();
_matings.Clear();
foreach (var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks)
foreach (
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
)
{
var b = brains.Span;
var n = needsChunk.Span;
@@ -744,7 +799,9 @@ public sealed class AnimalGrowthSystem(
ref var sprite = ref s[i];
sprite.Region = region;
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
t[i].Scale = new Vector2(AnimalFactory.Scale(sp, org.Traits, stage, region, cellSize));
t[i].Scale = new Vector2(
AnimalFactory.Scale(sp, org.Traits, stage, region, cellSize)
);
}
}
}
@@ -1082,7 +1139,10 @@ public sealed class AnimalHealthSystem : BaseSystem
foreach (var disease in _ambient)
{
if (!state.Has(disease.DefName) && _rng.NextSingle() < disease.AmbientPerDay * days)
if (
!state.Has(disease.DefName)
&& _rng.NextSingle() < disease.AmbientPerDay * days
)
{
state.Add(disease);
}