diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json index c2ce0ed..f6323d8 100644 --- a/Mods/Core/Defs/Animals/Animals.json +++ b/Mods/Core/Defs/Animals/Animals.json @@ -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, diff --git a/Mods/Core/Defs/Bodies/Bodies.json b/Mods/Core/Defs/Bodies/Bodies.json index c688659..ab1d939 100644 --- a/Mods/Core/Defs/Bodies/Bodies.json +++ b/Mods/Core/Defs/Bodies/Bodies.json @@ -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, diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json index 31dc673..869122d 100644 --- a/Mods/Core/Defs/Genes/Animal.json +++ b/Mods/Core/Defs/Genes/Animal.json @@ -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" } }, diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 85fe1ce..80d01bd 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -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", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index fbe410d..6e4652e 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -70,6 +70,8 @@ "cap.moving": "движение", "cap.sight": "зрение", "cap.hearing": "слух", + "cap.smell": "обоняние", + "cap.touch": "осязание", "cap.talking": "речь", "cap.eating": "питание", "cap.breathing": "дыхание", diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 6529dd8..6716b19 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -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("Rut")) ); diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 1163751..1ebfe88 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -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; } + /// Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это). + public const float MemoryPerBrain = 16f; + // Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам. private static int SkillSeed(Genome genome) { diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs index 2a53ad3..6d7b81e 100644 --- a/src/LittleSim/Sim/AnimalOrganism.cs +++ b/src/LittleSim/Sim/AnimalOrganism.cs @@ -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 /// Дальность зрения (множитель радиуса поиска). public float Vision; + /// Острота слуха (множитель радиуса). + public float Hearing; + + /// Острота обоняния (множитель радиуса). + public float Smell; + + /// Острота осязания (множитель радиуса; чувство ближнего контакта). + public float Touch; + + /// Камуфляж [0..1] — снижает обнаружение этой особи зрением (заметность = 1−camouflage). + public float Camouflage; + + /// Запах [0..1] — повышает обнаружение этой особи обонянием. + public float Scent; + + /// Шумность [0..1] — повышает обнаружение этой особи слухом при движении. + public float Noise; + /// Размер мозга (0..1) — основа интеллекта. public float BrainSize; @@ -76,6 +95,8 @@ public struct AnimalPhenotype public static AnimalPhenotype FromTraits(IReadOnlyDictionary 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; } +/// Вид запомненного: угроза (избегать), корм/вода/партнёр (идти) — расширяемо. +public enum MemoryKind : byte +{ + Threat, + Food, + Water, + Mate, +} + +/// Запись памяти: что, где (воспринятая позиция — грубая, если по запаху), насколько уверенно +/// (падает со временем — забывание), сколько дней назад. +public struct MemoryEntry +{ + public MemoryKind Kind; + public Vector2 Pos; + public float Confidence; + public float AgeDays; +} + +/// +/// Память особи (объём ∝ мозгу): ограниченный набор записей о замеченном (сейчас — угрозы). Чувства +/// пишут сюда воспринятое; со временем уверенность падает и запись забывается; ИИ использует память, +/// когда цель не воспринимается сейчас (избегает зоны недавнего хищника). Список — managed-ссылка. +/// +public struct Memory : IComponent +{ + /// Активные записи (упорядочены по добавлению; слабейшая вытесняется при переполнении). + public List Entries; + + /// Сколько записей вмещает (∝ размеру мозга; 0 — память отсутствует). + public int Capacity; + + /// Запоминает/освежает наблюдение: близкое того же вида — обновляет, иначе добавляет (с + /// вытеснением слабейшего при переполнении). Вне ёмкости (Capacity 0) — ничего не помнит. + 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, + } + ); + } +} + /// Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса). public static class AnimalSkills { diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 595d782..68cf691 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -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 _plantGrid; private readonly List _plantBuf = []; private readonly SpatialGrid _animalGrid; private readonly List _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(cellSize * GridCells); _animalGrid = new SpatialGrid(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(); @@ -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().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(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; } + /// Радиусы восприятия особи по чувствам (уже с остротой генов и способностью органов). + 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 } } } + +/// +/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие +/// наблюдения записывает/освежает ; эта система только «выветривает» +/// память, давая ей конечный срок. +/// +public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem +{ + 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; + } + } + } + } + } +} diff --git a/src/LittleSim/Sim/CapacityCalc.cs b/src/LittleSim/Sim/CapacityCalc.cs index ea120d5..5d87cea 100644 --- a/src/LittleSim/Sim/CapacityCalc.cs +++ b/src/LittleSim/Sim/CapacityCalc.cs @@ -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, diff --git a/src/LittleSim/Sim/SpatialGrid.cs b/src/LittleSim/Sim/SpatialGrid.cs index 856265a..5958629 100644 --- a/src/LittleSim/Sim/SpatialGrid.cs +++ b/src/LittleSim/Sim/SpatialGrid.cs @@ -14,8 +14,8 @@ public interface ISpatialEntry public readonly record struct ForageEntry(int Id, Vector2 Pos, float Toxicity, float Palatability) : ISpatialEntry; -/// Сосед-животное для ИИ: позиция/id + предпосчитанные признаки (вид, размер, мясоед, пол, взрослость), -/// чтобы запросы угрозы/добычи/партнёра/стада не дорефетчили компоненты. +/// Сосед-животное для ИИ: позиция/id + предпосчитанные признаки (вид, размер, мясоед, пол, взрослость) +/// и заметность (камуфляж/запах/шум) — чтобы запросы угрозы/добычи/партнёра/стада не дорефетчили компоненты. 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; ///