Merge branch 'senses-memory' into main

Animal senses (sight/hearing/smell/touch) and memory: smell/touch capacities +
organs (reduced by hediffs/damage), acuity + detectability genes, multi-sense
threat/prey detection with a coarse smell zone, and a brain-bounded memory that
records threats and steers wandering animals away from danger zones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-15 00:08:52 +03:00
co-authored by Claude Opus 4.8
11 changed files with 389 additions and 31 deletions
+24
View File
@@ -23,6 +23,12 @@
"GeneMoveSpeed": 1.2,
"GeneBloodVolume": 1.4,
"GeneVision": 1.3,
"GeneHearing": 1.4,
"GeneSmell": 1.2,
"GeneTouch": 1.0,
"GeneCamouflage": 0.35,
"GeneScent": 0.5,
"GeneNoise": 0.4,
"GeneBrainSize": 0.45,
"GeneInsulation": 0.55,
"GeneFurColor": 0.45,
@@ -59,6 +65,12 @@
"GeneMoveSpeed": 1.4,
"GeneBloodVolume": 1.3,
"GeneVision": 1.5,
"GeneHearing": 1.5,
"GeneSmell": 1.8,
"GeneTouch": 1.0,
"GeneCamouflage": 0.2,
"GeneScent": 0.6,
"GeneNoise": 0.5,
"GeneBrainSize": 0.5,
"GeneInsulation": 0.6,
"GeneFurColor": 0.7,
@@ -93,6 +105,12 @@
"GeneMoveSpeed": 1.0,
"GeneBloodVolume": 1.2,
"GeneVision": 1.0,
"GeneHearing": 1.2,
"GeneSmell": 1.6,
"GeneTouch": 1.0,
"GeneCamouflage": 0.2,
"GeneScent": 0.7,
"GeneNoise": 0.7,
"GeneBrainSize": 0.42,
"GeneInsulation": 0.5,
"GeneFurColor": 0.85,
@@ -126,6 +144,12 @@
"GeneMoveSpeed": 0.9,
"GeneBloodVolume": 0.7,
"GeneVision": 1.1,
"GeneHearing": 1.0,
"GeneSmell": 0.6,
"GeneTouch": 1.0,
"GeneCamouflage": 0.2,
"GeneScent": 0.4,
"GeneNoise": 0.6,
"GeneBrainSize": 0.3,
"GeneInsulation": 0.45,
"GeneFurColor": 0.6,
+4
View File
@@ -33,6 +33,10 @@
"capacities": { "Hearing": 0.5 } },
{ "name": "earRight", "parent": "head", "coverage": 0.01, "maxHp": 8,
"capacities": { "Hearing": 0.5 } },
{ "name": "nose", "parent": "head", "coverage": 0.01, "maxHp": 8,
"capacities": { "Smell": 1.0 } },
{ "name": "skin", "coverage": 0.05, "maxHp": 20,
"capacities": { "Touch": 1.0 } },
{ "name": "jaw", "parent": "head", "coverage": 0.02, "maxHp": 10,
"capacities": { "Eating": 1.0, "Talking": 0.5 } },
{ "name": "tongue", "parent": "head", "coverage": 0.01, "maxHp": 8,
+23
View File
@@ -18,6 +18,29 @@
{ "defName": "GeneVision", "parent": "BaseNumericGene", "label": "gene.vision",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "vision": "value" } },
// --- Чувства (острота = множитель радиуса восприятия) и заметность (как видят/слышат/чуют ЭТУ особь) ---
// Острота работает вместе со способностью органа (глаза/уши/нос/кожа): ген × capacity × радиус чувства.
{ "defName": "GeneHearing", "parent": "BaseNumericGene", "label": "gene.hearing",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "hearing": "value" } },
{ "defName": "GeneSmell", "parent": "BaseNumericGene", "label": "gene.smell",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "smell": "value" } },
{ "defName": "GeneTouch", "parent": "BaseNumericGene", "label": "gene.touch",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "touch": "value" } },
// Заметность цели: камуфляж снижает обнаружение зрением (1−camouflage), запах повышает обонянием,
// шум при движении повышает обнаружение слухом. См. формулу радиуса в системе восприятия.
{ "defName": "GeneCamouflage", "parent": "BaseNumericGene", "label": "gene.camouflage",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
"effects": { "camouflage": "value" } },
{ "defName": "GeneScent", "parent": "BaseNumericGene", "label": "gene.scent",
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
"effects": { "scent": "value" } },
{ "defName": "GeneNoise", "parent": "BaseNumericGene", "label": "gene.noise",
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
"effects": { "noise": "value" } },
{ "defName": "GeneBrainSize", "parent": "BaseNumericGene", "label": "gene.brainSize",
"default": 0.45, "min": 0.0, "max": 1.0, "spread": 0.03, "tags": ["animal", "brain"],
"effects": { "brainSize": "value" } },
+2
View File
@@ -70,6 +70,8 @@
"cap.moving": "moving",
"cap.sight": "sight",
"cap.hearing": "hearing",
"cap.smell": "smell",
"cap.touch": "touch",
"cap.talking": "talking",
"cap.eating": "eating",
"cap.breathing": "breathing",
+2
View File
@@ -70,6 +70,8 @@
"cap.moving": "движение",
"cap.sight": "зрение",
"cap.hearing": "слух",
"cap.smell": "обоняние",
"cap.touch": "осязание",
"cap.talking": "речь",
"cap.eating": "питание",
"cap.breathing": "дыхание",
+1
View File
@@ -270,6 +270,7 @@ public sealed class WorldScene : Scene
var thoughts = new ThoughtSet(content);
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
UpdateSystems.Add(new AnimalMemorySystem(Context.Clock, SecondsPerDay));
UpdateSystems.Add(
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
);
+12
View File
@@ -83,9 +83,21 @@ public static class AnimalFactory
Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
}
);
// Память: объём ∝ размеру мозга (примитивные ~0 — ничего не помнят).
entity.AddComponent(
new Memory
{
Entries = [],
Capacity = (int)MathF.Round(MathF.Max(0f, traits.BrainSize) * MemoryPerBrain),
}
);
return entity;
}
/// <summary>Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это).</summary>
public const float MemoryPerBrain = 16f;
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
private static int SkillSeed(Genome genome)
{
+110
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Friflo.Engine.ECS;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.Genetics;
namespace LittleSim.Sim;
@@ -27,6 +28,24 @@ public struct AnimalPhenotype
/// <summary>Дальность зрения (множитель радиуса поиска).</summary>
public float Vision;
/// <summary>Острота слуха (множитель радиуса).</summary>
public float Hearing;
/// <summary>Острота обоняния (множитель радиуса).</summary>
public float Smell;
/// <summary>Острота осязания (множитель радиуса; чувство ближнего контакта).</summary>
public float Touch;
/// <summary>Камуфляж [0..1] — снижает обнаружение этой особи зрением (заметность = 1camouflage).</summary>
public float Camouflage;
/// <summary>Запах [0..1] — повышает обнаружение этой особи обонянием.</summary>
public float Scent;
/// <summary>Шумность [0..1] — повышает обнаружение этой особи слухом при движении.</summary>
public float Noise;
/// <summary>Размер мозга (0..1) — основа интеллекта.</summary>
public float BrainSize;
@@ -76,6 +95,8 @@ public struct AnimalPhenotype
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{
float T(string name) => traits.GetValueOrDefault(name);
// Острота чувств по умолчанию 1 (вид без гена не «слепнет»); заметность — мягкие дефолты.
float Acuity(string name) => traits.TryGetValue(name, out var v) ? v : 1f;
return new AnimalPhenotype
{
BodySize = T("bodySize"),
@@ -83,6 +104,12 @@ public struct AnimalPhenotype
MoveSpeed = T("moveSpeed"),
BloodVolume = T("bloodVolume"),
Vision = T("vision"),
Hearing = Acuity("hearing"),
Smell = Acuity("smell"),
Touch = Acuity("touch"),
Camouflage = T("camouflage"),
Scent = traits.GetValueOrDefault("scent", 0.5f),
Noise = traits.GetValueOrDefault("noise", 0.5f),
BrainSize = T("brainSize"),
Insulation = T("insulation"),
FurHue = T("furHue"),
@@ -483,6 +510,89 @@ public struct Egg : IComponent
public float IncubateDays;
}
/// <summary>Вид запомненного: угроза (избегать), корм/вода/партнёр (идти) — расширяемо.</summary>
public enum MemoryKind : byte
{
Threat,
Food,
Water,
Mate,
}
/// <summary>Запись памяти: что, где (воспринятая позиция — грубая, если по запаху), насколько уверенно
/// (падает со временем — забывание), сколько дней назад.</summary>
public struct MemoryEntry
{
public MemoryKind Kind;
public Vector2 Pos;
public float Confidence;
public float AgeDays;
}
/// <summary>
/// Память особи (объём ∝ мозгу): ограниченный набор записей о замеченном (сейчас — угрозы). Чувства
/// пишут сюда воспринятое; со временем уверенность падает и запись забывается; ИИ использует память,
/// когда цель не воспринимается сейчас (избегает зоны недавнего хищника). Список — managed-ссылка.
/// </summary>
public struct Memory : IComponent
{
/// <summary>Активные записи (упорядочены по добавлению; слабейшая вытесняется при переполнении).</summary>
public List<MemoryEntry> Entries;
/// <summary>Сколько записей вмещает (∝ размеру мозга; 0 — память отсутствует).</summary>
public int Capacity;
/// <summary>Запоминает/освежает наблюдение: близкое того же вида — обновляет, иначе добавляет (с
/// вытеснением слабейшего при переполнении). Вне ёмкости (Capacity 0) — ничего не помнит.</summary>
public readonly void Remember(MemoryKind kind, Vector2 pos, float mergeDist)
{
if (Entries is null || Capacity <= 0)
{
return;
}
var mergeSq = mergeDist * mergeDist;
for (var i = 0; i < Entries.Count; i++)
{
if (Entries[i].Kind == kind && Vector2.DistanceSquared(Entries[i].Pos, pos) <= mergeSq)
{
Entries[i] = new MemoryEntry
{
Kind = kind,
Pos = pos,
Confidence = 1f,
AgeDays = 0f,
};
return;
}
}
if (Entries.Count >= Capacity)
{
var weakest = 0;
for (var i = 1; i < Entries.Count; i++)
{
if (Entries[i].Confidence < Entries[weakest].Confidence)
{
weakest = i;
}
}
Entries.RemoveAt(weakest);
}
Entries.Add(
new MemoryEntry
{
Kind = kind,
Pos = pos,
Confidence = 1f,
AgeDays = 0f,
}
);
}
}
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
public static class AnimalSkills
{
+199 -28
View File
@@ -268,10 +268,14 @@ public sealed class AnimalDecisionSystem : BaseSystem
// корма/угрозы/добычи/партнёра/стада берёт только ближние сущности вместо прохода по всем (было
// O(растений)+O(животных) на каждое решение). В ячейке — предпосчитанные признаки (без дорефетча).
private const int GridCells = 10; // размер ячейки сетки в клетках мира (~радиус восприятия)
private const float HearingRangeMult = 1.4f; // слух шире зрения
private const float SmellRangeMult = 1.8f; // запах — дальше всех (но грубый, см. Detect)
private const float RememberedThreatAvoidCells = 14f; // в этой зоне от памяти об угрозе зверь уходит прочь
private readonly SpatialGrid<ForageEntry> _plantGrid;
private readonly List<ForageEntry> _plantBuf = [];
private readonly SpatialGrid<NeighborEntry> _animalGrid;
private readonly List<NeighborEntry> _animalBuf = [];
private readonly float _smellSnap; // размер «обонятельной» ячейки (грубая локализация по запаху)
public AnimalDecisionSystem(
EntityStore store,
@@ -292,6 +296,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
_rng = new Random(seed);
_plantGrid = new SpatialGrid<ForageEntry>(cellSize * GridCells);
_animalGrid = new SpatialGrid<NeighborEntry>(cellSize * GridCells);
_smellSnap = cellSize * GridCells;
_animals = store.Query<
AnimalNeeds,
AnimalBrain,
@@ -361,13 +366,20 @@ public sealed class AnimalDecisionSystem : BaseSystem
// Эффективный интеллект (мозг × сознание) гейтит доступные нужды/действия (фаза A7):
// больной/раненый зверь с упавшим сознанием теряет высшие нужды (сон/секс) → рефлексы.
var intelligence = AnimalFactory.Intelligence(o[i].Traits, health);
// Радиус восприятия по способностям: зрение ищет корм/партнёра/добычу, угрозу замечаем
// зрением ИЛИ слухом (повреждённые глаза/уши сужают восприятие).
// Радиусы восприятия по чувствам: зрение (корм/партнёр), плюс слух и обоняние (угроза/добыча).
// Острота — гены чувств × способность органа (раны/хедифы их режут). Зрение узкое, слух шире,
// запах дальше всех, но грубый (см. Detect). Радиусы детерминированы.
var baseRadius = VisionRadius(o[i].Species, o[i].Traits.Vision);
var sight = health?.Capacity(AnimalCapacities.Sight) ?? 1f;
var hearing = health?.Capacity(AnimalCapacities.Hearing) ?? 1f;
var smell = health?.Capacity(AnimalCapacities.Smell) ?? 1f;
var seeRadius = baseRadius * MathF.Max(0.2f, sight);
var senseRadius = baseRadius * MathF.Max(0.2f, MathF.Max(sight, hearing));
var senseBase = _animalSet[o[i].Species].Def.VisionCells * _cellSize;
var ranges = new SenseRanges(
seeRadius,
senseBase * o[i].Traits.Hearing * MathF.Max(0.2f, hearing) * HearingRangeMult,
senseBase * o[i].Traits.Smell * MathF.Max(0.2f, smell) * SmellRangeMult
);
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent<Mood>();
@@ -381,19 +393,24 @@ public sealed class AnimalDecisionSystem : BaseSystem
HerdScan(pos, seeRadius, o[i].Species, self.Id, out herdCenter, out herdCount);
}
var threatRadius =
var safety =
herdCount > 0
? senseRadius
* (
1f
- SafetyInNumbers * MathF.Min(1f, herdCount / (float)HerdFullCount)
)
: senseRadius;
? 1f - SafetyInNumbers * MathF.Min(1f, herdCount / (float)HerdFullCount)
: 1f;
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
if (TryFindThreat(pos, threatRadius, o[i].Species, o[i].Traits, out var threatPos))
if (
TryFindThreat(
pos,
ranges.Scaled(safety),
o[i].Species,
o[i].Traits,
out var threatPos
)
)
{
self.GetComponent<Memory>().Remember(MemoryKind.Threat, threatPos, _smellSnap);
brain.Action = AnimalActions.Flee;
brain.TargetPlant = -1;
brain.TargetPrey = -1;
@@ -401,7 +418,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
var len = away.Length();
brain.Target =
len > 0.001f
? pos + away / len * (senseRadius + _cellSize)
? pos + away / len * (ranges.Max() + _cellSize)
: pos + new Vector2(_cellSize, 0f);
continue;
}
@@ -421,7 +438,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
when eatsMeat
&& TryFindKill(
pos,
seeRadius,
ranges,
o[i].Species,
o[i].Traits,
self.Id,
@@ -476,11 +493,21 @@ public sealed class AnimalDecisionSystem : BaseSystem
default:
brain.Action = AnimalActions.Wander;
brain.TargetPlant = -1;
// Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом —
// мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание.
var toCenter = herdCenter - pos;
var centerDist = toCenter.Length();
if (
if (TryRememberedThreat(self, pos, out var dangerPos))
{
// Память: вблизи места недавнего хищника зверь уходит прочь (опасная зона).
var flee = pos - dangerPos;
var d = flee.Length();
brain.Target =
d > 0.001f
? pos + flee / d * (_cellSize * (4f + _rng.NextSingle() * 4f))
: pos + new Vector2(_cellSize, 0f);
}
// Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом —
// мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание.
else if (
sociability >= HerdMinSociability
&& herdCount > 0
&& centerDist > HerdSeparation * _cellSize
@@ -511,6 +538,37 @@ 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;
// Ближайшая запомненная угроза в зоне избегания (для блуждания — уйти от опасного места).
private bool TryRememberedThreat(Entity self, Vector2 pos, out Vector2 threatPos)
{
threatPos = default;
if (!self.TryGetComponent<Memory>(out var mem) || mem.Entries is null)
{
return false;
}
var bestSq = RememberedThreatAvoidCells * _cellSize;
bestSq *= bestSq;
var found = false;
foreach (var e in mem.Entries)
{
if (e.Kind != MemoryKind.Threat)
{
continue;
}
var sq = Vector2.DistanceSquared(pos, e.Pos);
if (sq < bestSq)
{
bestSq = sq;
threatPos = e.Pos;
found = true;
}
}
return found;
}
// Центр и размер ближайшей группы соплеменников в радиусе (для когезии/безопасности). O(n) на особь,
// как и прочие сканы. Сам исключается по id; центр — средняя позиция соседей того же вида.
private void HerdScan(
@@ -547,18 +605,18 @@ public sealed class AnimalDecisionSystem : BaseSystem
// ест мясо и достаточно крупный (см. IsThreatTo). O(n) на особь — как поиск корма.
private bool TryFindThreat(
Vector2 from,
float radius,
in SenseRanges ranges,
int selfSpecies,
in AnimalPhenotype selfTraits,
out Vector2 threatPos
)
{
var bestSq = radius * radius;
var bestSq = float.MaxValue;
threatPos = default;
var threatId = -1;
var (_, selfEatsMeat) = AnimalFactory.Diet(selfTraits, _animalSet[selfSpecies].Def);
var selfBody = selfTraits.BodySize;
_animalGrid.Collect(from, radius, _animalBuf);
_animalGrid.Collect(from, ranges.Max(), _animalBuf);
foreach (var e in _animalBuf)
{
if (
@@ -570,10 +628,16 @@ public sealed class AnimalDecisionSystem : BaseSystem
}
var sq = Vector2.DistanceSquared(from, e.Pos);
// Обнаружение по чувствам: зрение (минус камуфляж) / слух (× шум) / запах (× запах, грубая зона).
if (!Detect(sq, e.Pos, ranges, e.Camouflage, e.Scent, e.Noise, out var perceived))
{
continue;
}
if (sq < bestSq || (sq == bestSq && e.Id < threatId))
{
bestSq = sq;
threatPos = e.Pos;
threatPos = perceived;
threatId = e.Id;
}
}
@@ -598,11 +662,62 @@ public sealed class AnimalDecisionSystem : BaseSystem
return selfEatsMeat ? otherBody > selfBody * 1.1f : otherBody >= selfBody * 0.9f;
}
/// <summary>Радиусы восприятия особи по чувствам (уже с остротой генов и способностью органов).</summary>
private readonly record struct SenseRanges(float Sight, float Hearing, float Smell)
{
public float Max() => MathF.Max(Sight, MathF.Max(Hearing, Smell));
public SenseRanges Scaled(float f) => new(Sight * f, Hearing * f, Smell * f);
}
// Обнаружена ли цель и где она «видится» наблюдателю. Зрение/слух дают точную позицию (зрение глушит
// камуфляж цели, слух усиливает её шум); запах добивает дальше всех, но даёт лишь ГРУБУЮ зону —
// позиция округляется до ячейки запаха (зверь идёт «примерно туда», пока ближе не сработает зрение).
private bool Detect(
float dist2,
Vector2 truePos,
in SenseRanges r,
float camouflage,
float scent,
float noise,
out Vector2 perceived
)
{
perceived = truePos;
var sight = r.Sight * (1f - Math.Clamp(camouflage, 0f, 1f));
if (sight > 0f && dist2 <= sight * sight)
{
return true;
}
var hear = r.Hearing * Math.Clamp(noise, 0f, 1f);
if (hear > 0f && dist2 <= hear * hear)
{
return true;
}
var smell = r.Smell * Math.Clamp(scent, 0f, 1f);
if (smell > 0f && dist2 <= smell * smell)
{
perceived = SnapToSmellCell(truePos); // грубая зона запаха
return true;
}
return false;
}
// Округляет позицию до центра «обонятельной» ячейки — детерминированная грубая локализация по запаху.
private Vector2 SnapToSmellCell(Vector2 p) =>
new(
MathF.Floor(p.X / _smellSnap) * _smellSnap + _smellSnap * 0.5f,
MathF.Floor(p.Y / _smellSnap) * _smellSnap + _smellSnap * 0.5f
);
// Ближайшая «мясная» цель: труп (падаль, приоритет — даровая еда без риска) или живая добыча.
// Возвращает id, позицию и флаг трупа. Добыча — другой вид не крупнее охотника (см. IsPreyFor).
private bool TryFindKill(
Vector2 from,
float radius,
in SenseRanges ranges,
int selfSpecies,
in AnimalPhenotype selfTraits,
int selfId,
@@ -614,9 +729,10 @@ public sealed class AnimalDecisionSystem : BaseSystem
targetId = -1;
position = default;
isCorpse = false;
var reach = ranges.Max();
// 1) Падаль с остатком мяса.
var bestCorpseSq = radius * radius;
// 1) Падаль с остатком мяса — находится в пределах любого чувства (запах хорошо ведёт к падали).
var bestCorpseSq = reach * reach;
var corpseId = -1;
var corpsePos = default(Vector2);
foreach (var (transforms, corpses, entities) in _corpses.Chunks)
@@ -641,12 +757,12 @@ public sealed class AnimalDecisionSystem : BaseSystem
}
}
// 2) Живая добыча (из сетки животных).
var bestPreySq = radius * radius;
// 2) Живая добыча (из сетки животных, по чувствам).
var bestPreySq = float.MaxValue;
var preyId = -1;
var preyPos = default(Vector2);
var selfBody = selfTraits.BodySize;
_animalGrid.Collect(from, radius, _animalBuf);
_animalGrid.Collect(from, reach, _animalBuf);
foreach (var e in _animalBuf)
{
if (e.Species == selfSpecies || e.Id == selfId || !IsPreyFor(selfBody, e.BodySize))
@@ -655,11 +771,16 @@ public sealed class AnimalDecisionSystem : BaseSystem
}
var sq = Vector2.DistanceSquared(from, e.Pos);
if (!Detect(sq, e.Pos, ranges, e.Camouflage, e.Scent, e.Noise, out var perceived))
{
continue;
}
if (sq < bestPreySq || (sq == bestPreySq && e.Id < preyId))
{
bestPreySq = sq;
preyId = e.Id;
preyPos = e.Pos;
preyPos = perceived;
}
}
@@ -753,7 +874,10 @@ public sealed class AnimalDecisionSystem : BaseSystem
o[i].Traits.BodySize,
eatsMeat,
o[i].IsMale,
AnimalFactory.IsAdult(sp, g[i].Stage)
AnimalFactory.IsAdult(sp, g[i].Stage),
o[i].Traits.Camouflage,
o[i].Traits.Scent,
o[i].Traits.Noise
)
);
}
@@ -2504,3 +2628,50 @@ public sealed class AnimalSkillSystem(GameClock clock, float secondsPerDay, Skil
}
}
}
/// <summary>
/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие
/// наблюдения записывает/освежает <see cref="AnimalDecisionSystem"/>; эта система только «выветривает»
/// память, давая ей конечный срок.
/// </summary>
public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem<Memory>
{
private const float ForgetPerDay = 0.5f; // уверенность падает (память живёт ~2 дня без подтверждения)
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
foreach (var (memories, _) in Query.Chunks)
{
var m = memories.Span;
for (var i = 0; i < m.Length; i++)
{
var entries = m[i].Entries;
if (entries is null)
{
continue;
}
for (var k = entries.Count - 1; k >= 0; k--)
{
var e = entries[k];
e.Confidence -= ForgetPerDay * days;
e.AgeDays += days;
if (e.Confidence <= 0f)
{
entries.RemoveAt(k);
}
else
{
entries[k] = e;
}
}
}
}
}
}
+6
View File
@@ -12,6 +12,8 @@ public static class AnimalCapacities
public const string BloodFiltration = "BloodFiltration";
public const string Sight = "Sight";
public const string Hearing = "Hearing";
public const string Smell = "Smell";
public const string Touch = "Touch";
public const string Talking = "Talking";
public const string Eating = "Eating";
public const string Digestion = "Digestion";
@@ -23,6 +25,8 @@ public static class AnimalCapacities
Moving,
Sight,
Hearing,
Smell,
Touch,
Talking,
Eating,
Breathing,
@@ -99,6 +103,8 @@ public static class CapacityCalc
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump,
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons,
[AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons,
[AnimalCapacities.Smell] = Raw(AnimalCapacities.Smell) * cons,
[AnimalCapacities.Touch] = Raw(AnimalCapacities.Touch) * cons,
[AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons,
[AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump,
+6 -3
View File
@@ -14,8 +14,8 @@ public interface ISpatialEntry
public readonly record struct ForageEntry(int Id, Vector2 Pos, float Toxicity, float Palatability)
: ISpatialEntry;
/// <summary>Сосед-животное для ИИ: позиция/id + предпосчитанные признаки (вид, размер, мясоед, пол, взрослость),
/// чтобы запросы угрозы/добычи/партнёра/стада не дорефетчили компоненты.</summary>
/// <summary>Сосед-животное для ИИ: позиция/id + предпосчитанные признаки (вид, размер, мясоед, пол, взрослость)
/// и заметность (камуфляж/запах/шум) — чтобы запросы угрозы/добычи/партнёра/стада не дорефетчили компоненты.</summary>
public readonly record struct NeighborEntry(
int Id,
Vector2 Pos,
@@ -23,7 +23,10 @@ public readonly record struct NeighborEntry(
float BodySize,
bool EatsMeat,
bool IsMale,
bool IsAdult
bool IsAdult,
float Camouflage,
float Scent,
float Noise
) : ISpatialEntry;
/// <summary>