Senses stage 2: multi-sense detection (sight/hearing/smell) for threat & prey
Predator/prey detection now uses three senses instead of one radius. Per observer, SenseRanges = sight (vision gene x Sight capacity), hearing (x1.4, Hearing gene/cap), smell (x1.8, Smell gene/cap). Detect(): sight beats the target''s camouflage and hearing scales with its noise (both give exact position); smell reaches furthest but only a COARSE zone -- the position snaps to a smell cell, so a predator tracking by scent heads roughly toward prey until sight takes over. Damaged eyes/ears/nose or hediff capMods shrink the matching sense; detectability genes (camouflage/scent/noise) ride in the animal grid entry. Deterministic (snap, not noise). Mate/forage stay on sight for now. Build + --check-content clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
70e79a5c30
commit
3031bf2954
@@ -268,10 +268,13 @@ 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 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 +295,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 +365,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,18 +392,22 @@ 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
|
||||
)
|
||||
)
|
||||
{
|
||||
brain.Action = AnimalActions.Flee;
|
||||
brain.TargetPlant = -1;
|
||||
@@ -401,7 +416,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 +436,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
||||
when eatsMeat
|
||||
&& TryFindKill(
|
||||
pos,
|
||||
seeRadius,
|
||||
ranges,
|
||||
o[i].Species,
|
||||
o[i].Traits,
|
||||
self.Id,
|
||||
@@ -547,18 +562,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 +585,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 +619,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 +686,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 +714,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 +728,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 +831,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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user