diff --git a/Mods/Core/Defs/Animals/Animals.json b/Mods/Core/Defs/Animals/Animals.json
index ed10555..4a235e6 100644
--- a/Mods/Core/Defs/Animals/Animals.json
+++ b/Mods/Core/Defs/Animals/Animals.json
@@ -31,7 +31,8 @@
"GeneGestationDays": 30,
"GeneLitterSize": 1,
"GeneHerbivory": 1.0,
- "GeneToxinTolerance": 0.1
+ "GeneToxinTolerance": 0.1,
+ "GeneSociability": 0.8
}
},
@@ -64,7 +65,8 @@
"GeneBreedingSeason": 3,
"GeneGestationDays": 35,
"GeneLitterSize": 4,
- "GeneCarnivory": 1.0
+ "GeneCarnivory": 1.0,
+ "GeneSociability": 0.6
}
},
@@ -99,7 +101,8 @@
"GeneHerbivory": 0.4,
"GeneCarnivory": 0.2,
"GeneOmnivory": 0.85,
- "GeneToxinTolerance": 0.25
+ "GeneToxinTolerance": 0.25,
+ "GeneSociability": 0.3
}
},
@@ -129,7 +132,8 @@
"GeneLitterSize": 6,
"GeneHerbivory": 0.8,
"GeneToxinTolerance": 0.15,
- "GeneEggLaying": 1.0
+ "GeneEggLaying": 1.0,
+ "GeneSociability": 0.55
}
}
]
diff --git a/Mods/Core/Defs/Genes/Animal.json b/Mods/Core/Defs/Genes/Animal.json
index cc54db5..31dc673 100644
--- a/Mods/Core/Defs/Genes/Animal.json
+++ b/Mods/Core/Defs/Genes/Animal.json
@@ -59,6 +59,12 @@
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
+ // Социальность [0..1]: стадные/стайные виды держатся вместе (когезия при блуждании) и спокойнее в
+ // группе («безопасность в числе» снижает дальность реакции на хищника). См. AnimalDecisionSystem.
+ { "defName": "GeneSociability", "parent": "BaseNumericGene", "label": "gene.sociability",
+ "default": 0.3, "min": 0.0, "max": 1.0, "spread": 0.05, "tags": ["animal", "behaviour"],
+ "effects": { "sociability": "value" } },
+
// Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
// яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
// вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 92954ad..15310e8 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -1208,6 +1208,9 @@ public sealed class WorldScene : Scene
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
);
+ console.WriteLine(
+ $" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}"
+ );
}
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 8f93ae5..4bca504 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -51,6 +51,9 @@ public struct AnimalPhenotype
/// Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.
public float ToxinTolerance;
+ /// Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).
+ public float Sociability;
+
/// Продолжительность жизни (игровых дней).
public float Lifespan;
@@ -88,6 +91,7 @@ public struct AnimalPhenotype
Carnivory = T("carnivory"),
Omnivory = T("omnivory"),
ToxinTolerance = T("toxinTolerance"),
+ Sociability = T("sociability"),
Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"),
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index c16c642..c6dcbb8 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -232,6 +232,10 @@ public sealed class AnimalDecisionSystem : BaseSystem
{
private const float Interval = 0.6f;
private const float MateMoodFloor = 0.35f; // ниже — стресс/истощение, зверь не спаривается (A8)
+ private const float HerdMinSociability = 0.3f; // ниже — вид не стадный (когезия/безопасность выкл.)
+ private const float SafetyInNumbers = 0.5f; // макс. снижение радиуса реакции на угрозу в группе
+ private const int HerdFullCount = 6; // размер группы, при котором эффект «безопасность в числе» полон
+ private const float HerdSeparation = 1.5f; // ближе этого (в клетках) к центру — не сходимся плотнее
private readonly UtilityAi _brain;
private readonly GameClock _clock;
@@ -345,9 +349,28 @@ public sealed class AnimalDecisionSystem : BaseSystem
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
var mood = self.GetComponent();
+ // Стадность (boids): социальные виды считают центр/размер ближайшей группы соплеменников —
+ // для когезии при блуждании и «безопасности в числе» (спокойнее на угрозу в большой группе).
+ var sociability = o[i].Traits.Sociability;
+ var herdCount = 0;
+ var herdCenter = pos;
+ if (sociability >= HerdMinSociability)
+ {
+ HerdScan(pos, seeRadius, o[i].Species, self.Id, out herdCenter, out herdCount);
+ }
+
+ var threatRadius =
+ herdCount > 0
+ ? senseRadius
+ * (
+ 1f
+ - SafetyInNumbers * MathF.Min(1f, herdCount / (float)HerdFullCount)
+ )
+ : senseRadius;
+
// Бегство (предатор-кластер): жертва, заметившая рядом хищника, спасается — это важнее
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. IsThreatTo).
- if (TryFindThreat(pos, senseRadius, o[i].Species, o[i].Traits, out var threatPos))
+ if (TryFindThreat(pos, threatRadius, o[i].Species, o[i].Traits, out var threatPos))
{
brain.Action = AnimalActions.Flee;
brain.TargetPlant = -1;
@@ -431,11 +454,31 @@ public sealed class AnimalDecisionSystem : BaseSystem
default:
brain.Action = AnimalActions.Wander;
brain.TargetPlant = -1;
- var angle = _rng.NextSingle() * MathF.Tau;
- brain.Target =
- pos
- + new Vector2(MathF.Cos(angle), MathF.Sin(angle))
- * (_cellSize * (4f + _rng.NextSingle() * 6f));
+ // Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом —
+ // мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание.
+ var toCenter = herdCenter - pos;
+ var centerDist = toCenter.Length();
+ if (
+ sociability >= HerdMinSociability
+ && herdCount > 0
+ && centerDist > HerdSeparation * _cellSize
+ )
+ {
+ brain.Target =
+ pos
+ + toCenter
+ / centerDist
+ * (_cellSize * (3f + _rng.NextSingle() * 3f));
+ }
+ else
+ {
+ var angle = _rng.NextSingle() * MathF.Tau;
+ brain.Target =
+ pos
+ + new Vector2(MathF.Cos(angle), MathF.Sin(angle))
+ * (_cellSize * (4f + _rng.NextSingle() * 6f));
+ }
+
break;
}
}
@@ -446,6 +489,42 @@ 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;
+ // Центр и размер ближайшей группы соплеменников в радиусе (для когезии/безопасности). O(n) на особь,
+ // как и прочие сканы. Сам исключается по id; центр — средняя позиция соседей того же вида.
+ private void HerdScan(
+ Vector2 from,
+ float radius,
+ int species,
+ int selfId,
+ out Vector2 center,
+ out int count
+ )
+ {
+ var sum = Vector2.Zero;
+ count = 0;
+ var radiusSq = radius * radius;
+ foreach (var (_, _, organisms, _, transforms, entities) in _animals.Chunks)
+ {
+ var oo = organisms.Span;
+ var tt = transforms.Span;
+ for (var i = 0; i < tt.Length; i++)
+ {
+ if (oo[i].Species != species || entities.EntityAt(i).Id == selfId)
+ {
+ continue;
+ }
+
+ if (Vector2.DistanceSquared(from, tt[i].Position) <= radiusSq)
+ {
+ sum += tt[i].Position;
+ count++;
+ }
+ }
+ }
+
+ center = count > 0 ? sum / count : from;
+ }
+
// Ближайший хищник-угроза в радиусе восприятия (для бегства жертвы). Угроза = другой вид, который
// ест мясо и достаточно крупный (см. IsThreatTo). O(n) на особь — как поиск корма.
private bool TryFindThreat(