Merge branch 'ecology-thermal-social' into main
Thermoregulation (revives GeneInsulation) + wound infection, seed dispersal by herbivores (endozoochory), and herding/flocking via GeneSociability. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,8 @@
|
|||||||
"GeneGestationDays": 30,
|
"GeneGestationDays": 30,
|
||||||
"GeneLitterSize": 1,
|
"GeneLitterSize": 1,
|
||||||
"GeneHerbivory": 1.0,
|
"GeneHerbivory": 1.0,
|
||||||
"GeneToxinTolerance": 0.1
|
"GeneToxinTolerance": 0.1,
|
||||||
|
"GeneSociability": 0.8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -64,7 +65,8 @@
|
|||||||
"GeneBreedingSeason": 3,
|
"GeneBreedingSeason": 3,
|
||||||
"GeneGestationDays": 35,
|
"GeneGestationDays": 35,
|
||||||
"GeneLitterSize": 4,
|
"GeneLitterSize": 4,
|
||||||
"GeneCarnivory": 1.0
|
"GeneCarnivory": 1.0,
|
||||||
|
"GeneSociability": 0.6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -99,7 +101,8 @@
|
|||||||
"GeneHerbivory": 0.4,
|
"GeneHerbivory": 0.4,
|
||||||
"GeneCarnivory": 0.2,
|
"GeneCarnivory": 0.2,
|
||||||
"GeneOmnivory": 0.85,
|
"GeneOmnivory": 0.85,
|
||||||
"GeneToxinTolerance": 0.25
|
"GeneToxinTolerance": 0.25,
|
||||||
|
"GeneSociability": 0.3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -129,7 +132,8 @@
|
|||||||
"GeneLitterSize": 6,
|
"GeneLitterSize": 6,
|
||||||
"GeneHerbivory": 0.8,
|
"GeneHerbivory": 0.8,
|
||||||
"GeneToxinTolerance": 0.15,
|
"GeneToxinTolerance": 0.15,
|
||||||
"GeneEggLaying": 1.0
|
"GeneEggLaying": 1.0,
|
||||||
|
"GeneSociability": 0.55
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,6 +59,12 @@
|
|||||||
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
|
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
|
||||||
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "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) вынашивают и рожают живых детёнышей;
|
// Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
|
||||||
// яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
|
// яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
|
||||||
// вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
|
// вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
|
||||||
|
|||||||
@@ -32,6 +32,29 @@
|
|||||||
"defName": "Poisoned", "label": "hediff.poisoned",
|
"defName": "Poisoned", "label": "hediff.poisoned",
|
||||||
"initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2,
|
"initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2,
|
||||||
"capMods": { "Moving": 0.85, "Consciousness": 0.9, "Digestion": 0.7 }
|
"capMods": { "Moving": 0.85, "Consciousness": 0.9, "Digestion": 0.7 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Терморегуляция (как Poisoned-паттерн): сами не прогрессируют — тяжесть копится, пока зверю
|
||||||
|
// холодно/жарко (см. AnimalHealthSystem, зависит от GeneInsulation + размера тела), а в комфорте
|
||||||
|
// immunityPerDay их рассасывает. Тяжесть 1 = смерть от переохлаждения/перегрева.
|
||||||
|
{
|
||||||
|
"defName": "Hypothermia", "label": "hediff.hypothermia",
|
||||||
|
"initialSeverity": 0.0, "immunityPerDay": 0.5, "lethalSeverity": 1.0, "pain": 0.15,
|
||||||
|
"capMods": { "Moving": 0.7, "Consciousness": 0.8 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defName": "Heatstroke", "label": "hediff.heatstroke",
|
||||||
|
"initialSeverity": 0.0, "immunityPerDay": 0.5, "lethalSeverity": 1.0, "pain": 0.15,
|
||||||
|
"capMods": { "Moving": 0.75, "Consciousness": 0.8 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Заражение раны (предатор/шипы → кровотечение → инфекция): прогрессирует, иммунитет (× фильтрация
|
||||||
|
// крови) душит. Слабая фильтрация/много ран → выше риск и хуже исход.
|
||||||
|
{
|
||||||
|
"defName": "Infection", "label": "hediff.infection",
|
||||||
|
"initialSeverity": 0.08, "severityPerDay": 0.2, "immunityPerDay": 0.28,
|
||||||
|
"lethalSeverity": 1.0, "pain": 0.25,
|
||||||
|
"capMods": { "Moving": 0.8, "Consciousness": 0.85, "BloodFiltration": 0.8 }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,9 @@
|
|||||||
"hediff.fever": "fever",
|
"hediff.fever": "fever",
|
||||||
"hediff.bleeding": "bleeding",
|
"hediff.bleeding": "bleeding",
|
||||||
"hediff.poisoned": "poisoned",
|
"hediff.poisoned": "poisoned",
|
||||||
|
"hediff.hypothermia": "hypothermia",
|
||||||
|
"hediff.heatstroke": "heatstroke",
|
||||||
|
"hediff.infection": "infection",
|
||||||
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
||||||
"hud.paused": "PAUSED",
|
"hud.paused": "PAUSED",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
|
|||||||
@@ -73,6 +73,9 @@
|
|||||||
"hediff.fever": "лихорадка",
|
"hediff.fever": "лихорадка",
|
||||||
"hediff.bleeding": "кровотечение",
|
"hediff.bleeding": "кровотечение",
|
||||||
"hediff.poisoned": "отравление",
|
"hediff.poisoned": "отравление",
|
||||||
|
"hediff.hypothermia": "переохлаждение",
|
||||||
|
"hediff.heatstroke": "тепловой удар",
|
||||||
|
"hediff.infection": "заражение",
|
||||||
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
||||||
"hud.paused": "ПАУЗА",
|
"hud.paused": "ПАУЗА",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
|
|||||||
@@ -242,7 +242,11 @@ public sealed class WorldScene : Scene
|
|||||||
CellSize,
|
CellSize,
|
||||||
Context.Clock,
|
Context.Clock,
|
||||||
SecondsPerDay,
|
SecondsPerDay,
|
||||||
|
climate,
|
||||||
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(),
|
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(),
|
||||||
|
content.Defs.TryGet<HediffDef>("Hypothermia", out var hypo) ? hypo : null,
|
||||||
|
content.Defs.TryGet<HediffDef>("Heatstroke", out var heat) ? heat : null,
|
||||||
|
content.Defs.TryGet<HediffDef>("Infection", out var infe) ? infe : null,
|
||||||
_config.Seed + 0x3EAD
|
_config.Seed + 0x3EAD
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -305,6 +309,19 @@ public sealed class WorldScene : Scene
|
|||||||
_config.Seed + 0x6E66
|
_config.Seed + 0x6E66
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new SeedDispersalSystem(
|
||||||
|
Store,
|
||||||
|
_plants,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
CellSize,
|
||||||
|
_cellFertility,
|
||||||
|
_cellLand
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||||
@@ -1191,6 +1208,9 @@ public sealed class WorldScene : Scene
|
|||||||
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
|
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
|
||||||
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
|
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
|
||||||
);
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
|
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ public struct AnimalPhenotype
|
|||||||
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
|
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
|
||||||
public float ToxinTolerance;
|
public float ToxinTolerance;
|
||||||
|
|
||||||
|
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
|
||||||
|
public float Sociability;
|
||||||
|
|
||||||
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||||
public float Lifespan;
|
public float Lifespan;
|
||||||
|
|
||||||
@@ -88,6 +91,7 @@ public struct AnimalPhenotype
|
|||||||
Carnivory = T("carnivory"),
|
Carnivory = T("carnivory"),
|
||||||
Omnivory = T("omnivory"),
|
Omnivory = T("omnivory"),
|
||||||
ToxinTolerance = T("toxinTolerance"),
|
ToxinTolerance = T("toxinTolerance"),
|
||||||
|
Sociability = T("sociability"),
|
||||||
Lifespan = T("lifespan"),
|
Lifespan = T("lifespan"),
|
||||||
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||||
GestationDays = T("gestationDays"),
|
GestationDays = T("gestationDays"),
|
||||||
@@ -484,6 +488,23 @@ public struct Egg : IComponent
|
|||||||
public float IncubateDays;
|
public float IncubateDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
|
||||||
|
/// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) —
|
||||||
|
/// зверь становится разносчиком флоры. Геном — managed-ссылка (клон родительского растения).
|
||||||
|
/// </summary>
|
||||||
|
public struct GutSeed : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Индекс вида растения в <see cref="Content.PlantSet"/>.</summary>
|
||||||
|
public int PlantSpecies;
|
||||||
|
|
||||||
|
/// <summary>Геном будущего ростка (клон съеденного растения).</summary>
|
||||||
|
public Genome Genome;
|
||||||
|
|
||||||
|
/// <summary>Остаток времени до высадки в игровых днях; ≤0 — росток падает на землю.</summary>
|
||||||
|
public float DropInDays;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
|
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
|
||||||
public struct ThoughtInstance
|
public struct ThoughtInstance
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
{
|
{
|
||||||
private const float Interval = 0.6f;
|
private const float Interval = 0.6f;
|
||||||
private const float MateMoodFloor = 0.35f; // ниже — стресс/истощение, зверь не спаривается (A8)
|
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<AnimalContext> _brain;
|
private readonly UtilityAi<AnimalContext> _brain;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
@@ -345,9 +349,28 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
|
// Настроение (A8): стресс/истощение (низкое настроение) подавляет тягу к спариванию.
|
||||||
var mood = self.GetComponent<Mood>();
|
var mood = self.GetComponent<Mood>();
|
||||||
|
|
||||||
|
// Стадность (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).
|
// любых нужд (инстинкт выживания). Сам хищник от добычи не бежит (см. 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.Action = AnimalActions.Flee;
|
||||||
brain.TargetPlant = -1;
|
brain.TargetPlant = -1;
|
||||||
@@ -431,11 +454,31 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
default:
|
default:
|
||||||
brain.Action = AnimalActions.Wander;
|
brain.Action = AnimalActions.Wander;
|
||||||
brain.TargetPlant = -1;
|
brain.TargetPlant = -1;
|
||||||
|
// Стадность: социальный зверь вдали от группы бредёт к её центру (когезия), рядом —
|
||||||
|
// мельтешит случайно (с расхождением). Несоциальный — обычное случайное блуждание.
|
||||||
|
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;
|
var angle = _rng.NextSingle() * MathF.Tau;
|
||||||
brain.Target =
|
brain.Target =
|
||||||
pos
|
pos
|
||||||
+ new Vector2(MathF.Cos(angle), MathF.Sin(angle))
|
+ new Vector2(MathF.Cos(angle), MathF.Sin(angle))
|
||||||
* (_cellSize * (4f + _rng.NextSingle() * 6f));
|
* (_cellSize * (4f + _rng.NextSingle() * 6f));
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,6 +489,42 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
private float VisionRadius(int species, float vision) =>
|
private float VisionRadius(int species, float vision) =>
|
||||||
MathF.Max(1f, _animalSet[species].Def.VisionCells) * MathF.Max(0.2f, vision) * _cellSize;
|
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) на особь — как поиск корма.
|
// ест мясо и достаточно крупный (см. IsThreatTo). O(n) на особь — как поиск корма.
|
||||||
private bool TryFindThreat(
|
private bool TryFindThreat(
|
||||||
@@ -791,6 +870,8 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
|
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
|
||||||
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
|
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
|
||||||
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
|
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
|
||||||
|
private const float SeedIngestChance = 0.15f; // шанс проглотить семя при выедании зрелого растения
|
||||||
|
private const float GutSeedDropDays = 2f; // через сколько дней проглоченное семя высаживается
|
||||||
|
|
||||||
private readonly EntityStore _store;
|
private readonly EntityStore _store;
|
||||||
private readonly PlantSet _plants;
|
private readonly PlantSet _plants;
|
||||||
@@ -815,6 +896,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
|
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
|
||||||
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
|
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
|
||||||
private readonly List<(Entity Self, Entity Partner)> _matings = [];
|
private readonly List<(Entity Self, Entity Partner)> _matings = [];
|
||||||
|
private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория)
|
||||||
|
|
||||||
public AnimalActionSystem(
|
public AnimalActionSystem(
|
||||||
EntityStore store,
|
EntityStore store,
|
||||||
@@ -859,6 +941,7 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
_consumed.Clear();
|
_consumed.Clear();
|
||||||
_kills.Clear();
|
_kills.Clear();
|
||||||
_matings.Clear();
|
_matings.Clear();
|
||||||
|
_ingested.Clear();
|
||||||
|
|
||||||
foreach (
|
foreach (
|
||||||
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
|
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
|
||||||
@@ -905,6 +988,8 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
o[i].Traits.ToxinTolerance,
|
o[i].Traits.ToxinTolerance,
|
||||||
days
|
days
|
||||||
);
|
);
|
||||||
|
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
|
||||||
|
TryIngestSeed(brain.TargetPlant, entities.EntityAt(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -984,9 +1069,55 @@ public sealed class AnimalActionSystem : BaseSystem
|
|||||||
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
|
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var (eater, seed) in _ingested)
|
||||||
|
{
|
||||||
|
if (!eater.IsNull && !eater.HasComponent<GutSeed>())
|
||||||
|
{
|
||||||
|
eater.AddComponent(seed); // структурное добавление — после прохода
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ApplyMatings();
|
ApplyMatings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Эндозоохория: при выедании ЗРЕЛОГО растения шанс проглотить его семя — зверь понесёт его и
|
||||||
|
// высадит позже в другом месте (SeedDispersalSystem). Геном клонируется (отдельная особь-росток).
|
||||||
|
private void TryIngestSeed(int plantId, Entity eater)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
eater.IsNull
|
||||||
|
|| eater.HasComponent<GutSeed>()
|
||||||
|
|| plantId < 0
|
||||||
|
|| _rng.NextSingle() >= SeedIngestChance
|
||||||
|
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||||
|
|| plant.IsNull
|
||||||
|
|| !plant.HasComponent<PlantGrowth>()
|
||||||
|
|| !plant.HasComponent<PlantOrganism>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref readonly var grow = ref plant.GetComponent<PlantGrowth>();
|
||||||
|
if (grow.Stage < _plants[grow.Species].Stages.Length - 1)
|
||||||
|
{
|
||||||
|
return; // только зрелое растение даёт семя
|
||||||
|
}
|
||||||
|
|
||||||
|
var genome = new Genome(plant.GetComponent<PlantOrganism>().Genome.ToDictionary());
|
||||||
|
_ingested.Add(
|
||||||
|
(
|
||||||
|
eater,
|
||||||
|
new GutSeed
|
||||||
|
{
|
||||||
|
PlantSpecies = grow.Species,
|
||||||
|
Genome = genome,
|
||||||
|
DropInDays = GutSeedDropDays,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
|
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
|
||||||
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
|
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
|
||||||
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
|
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
|
||||||
@@ -1833,7 +1964,11 @@ public sealed class AnimalHealthSystem : BaseSystem
|
|||||||
private readonly int _cellSize;
|
private readonly int _cellSize;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
private readonly float _secondsPerDay;
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly Climate _climate;
|
||||||
private readonly HediffDef[] _ambient;
|
private readonly HediffDef[] _ambient;
|
||||||
|
private readonly HediffDef? _hypothermia;
|
||||||
|
private readonly HediffDef? _heatstroke;
|
||||||
|
private readonly HediffDef? _infection;
|
||||||
private readonly Random _rng;
|
private readonly Random _rng;
|
||||||
private readonly ArchetypeQuery<Health, AnimalOrganism, AnimalGrowth> _query;
|
private readonly ArchetypeQuery<Health, AnimalOrganism, AnimalGrowth> _query;
|
||||||
private readonly List<Entity> _deaths = [];
|
private readonly List<Entity> _deaths = [];
|
||||||
@@ -1844,13 +1979,28 @@ public sealed class AnimalHealthSystem : BaseSystem
|
|||||||
private const float SenescenceStart = 0.6f;
|
private const float SenescenceStart = 0.6f;
|
||||||
private const float SenescenceMax = 0.45f;
|
private const float SenescenceMax = 0.45f;
|
||||||
|
|
||||||
|
// Термокомфорт (°C) при теплоизоляции 0.5 и размере тела 1; границы сдвигаются мехом и размером тела.
|
||||||
|
private const float ColdComfort = 2f; // ниже — мёрзнет
|
||||||
|
private const float HeatComfort = 30f; // выше — перегревается
|
||||||
|
private const float ColdInsulationBonus = 30f; // мех (insulation 0..1) расширяет холодоустойчивость
|
||||||
|
private const float HeatInsulationPenalty = 14f; // тот же мех ухудшает жароустойчивость
|
||||||
|
private const float ColdBodyBonus = 8f; // крупное тело (тепловая инерция) держит холод лучше
|
||||||
|
private const float ThermalRatePerDegree = 0.06f; // прирост тяжести за °C за пределом комфорта в день
|
||||||
|
|
||||||
|
// Заражение раны: шанс в день при кровотечении тяжести 1 подхватить инфекцию.
|
||||||
|
private const float InfectionChancePerDay = 0.25f;
|
||||||
|
|
||||||
public AnimalHealthSystem(
|
public AnimalHealthSystem(
|
||||||
EntityStore store,
|
EntityStore store,
|
||||||
AnimalSet animals,
|
AnimalSet animals,
|
||||||
int cellSize,
|
int cellSize,
|
||||||
GameClock clock,
|
GameClock clock,
|
||||||
float secondsPerDay,
|
float secondsPerDay,
|
||||||
|
Climate climate,
|
||||||
HediffDef[] ambient,
|
HediffDef[] ambient,
|
||||||
|
HediffDef? hypothermia,
|
||||||
|
HediffDef? heatstroke,
|
||||||
|
HediffDef? infection,
|
||||||
int seed
|
int seed
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -1859,7 +2009,11 @@ public sealed class AnimalHealthSystem : BaseSystem
|
|||||||
_cellSize = cellSize;
|
_cellSize = cellSize;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
_secondsPerDay = secondsPerDay;
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_climate = climate;
|
||||||
_ambient = ambient;
|
_ambient = ambient;
|
||||||
|
_hypothermia = hypothermia;
|
||||||
|
_heatstroke = heatstroke;
|
||||||
|
_infection = infection;
|
||||||
_rng = new Random(seed);
|
_rng = new Random(seed);
|
||||||
_query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
|
_query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
|
||||||
}
|
}
|
||||||
@@ -1912,8 +2066,48 @@ public sealed class AnimalHealthSystem : BaseSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Терморегуляция: вне комфорта (зависит от GeneInsulation + размера тела) копится тяжесть
|
||||||
|
// гипотермии/теплового удара; в комфорте immunityPerDay их рассасывает. Мех расширяет
|
||||||
|
// холодоустойчивость, но ухудшает жароустойчивость; крупное тело держит холод.
|
||||||
|
var insul = Math.Clamp(o[i].Traits.Insulation, 0f, 1f);
|
||||||
|
var coldLimit =
|
||||||
|
ColdComfort
|
||||||
|
- ColdInsulationBonus * insul
|
||||||
|
- ColdBodyBonus * MathF.Max(0f, o[i].Traits.BodySize - 1f);
|
||||||
|
var heatLimit = HeatComfort - HeatInsulationPenalty * insul;
|
||||||
|
var temp = _climate.Temperature;
|
||||||
|
if (temp < coldLimit && _hypothermia is not null)
|
||||||
|
{
|
||||||
|
state.Intensify(_hypothermia, (coldLimit - temp) * ThermalRatePerDegree * days);
|
||||||
|
}
|
||||||
|
else if (temp > heatLimit && _heatstroke is not null)
|
||||||
|
{
|
||||||
|
state.Intensify(_heatstroke, (temp - heatLimit) * ThermalRatePerDegree * days);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заражение раны: пока есть кровотечение, шанс (∝ его тяжести) подхватить инфекцию.
|
||||||
|
if (_infection is not null && !state.Has(_infection.DefName))
|
||||||
|
{
|
||||||
|
var bleedSeverity = 0f;
|
||||||
|
foreach (var hd in state.Hediffs)
|
||||||
|
{
|
||||||
|
if (hd.Def.BloodLossPerDay > 0f && hd.Severity > bleedSeverity)
|
||||||
|
{
|
||||||
|
bleedSeverity = hd.Severity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bleedSeverity > 0f
|
||||||
|
&& _rng.NextSingle() < InfectionChancePerDay * bleedSeverity * days
|
||||||
|
)
|
||||||
|
{
|
||||||
|
state.Add(_infection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Фильтрация крови (почки/печень) ускоряет вывод токсинов и набор иммунитета: повреждённый
|
// Фильтрация крови (почки/печень) ускоряет вывод токсинов и набор иммунитета: повреждённый
|
||||||
// орган → медленнее выздоровление от яда/болезни (для тела без неё Capacity вернёт 1).
|
// орган → медленнее выздоровление от яда/болезни/инфекции (для тела без неё Capacity вернёт 1).
|
||||||
var lethal = state.AdvanceHediffs(
|
var lethal = state.AdvanceHediffs(
|
||||||
days,
|
days,
|
||||||
state.Capacity(AnimalCapacities.BloodFiltration)
|
state.Capacity(AnimalCapacities.BloodFiltration)
|
||||||
@@ -1958,3 +2152,102 @@ public sealed class AnimalHealthSystem : BaseSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Разнос семян (эндозоохория): тикает таймер проглоченного семени (<see cref="GutSeed"/>); по
|
||||||
|
/// истечении высаживает росток того вида растения там, где сейчас зверь (если клетка — суша),
|
||||||
|
/// геномом-клоном съеденного растения, и снимает компонент. Делает травоядных разносчиками флоры —
|
||||||
|
/// растения расселяются вдоль кормовых маршрутов.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SeedDispersalSystem : BaseSystem
|
||||||
|
{
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly int _width;
|
||||||
|
private readonly int _height;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly float[] _cellFertility;
|
||||||
|
private readonly bool[] _cellLand;
|
||||||
|
private readonly ArchetypeQuery<GutSeed, Transform2D> _query;
|
||||||
|
private readonly List<Entity> _dropped = [];
|
||||||
|
|
||||||
|
public SeedDispersalSystem(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int cellSize,
|
||||||
|
float[] cellFertility,
|
||||||
|
bool[] cellLand
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_plants = plants;
|
||||||
|
_clock = clock;
|
||||||
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_width = width;
|
||||||
|
_height = height;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_cellFertility = cellFertility;
|
||||||
|
_cellLand = cellLand;
|
||||||
|
_query = store.Query<GutSeed, Transform2D>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
var days = _clock.DeltaTime / _secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropped.Clear();
|
||||||
|
foreach (var (seeds, transforms, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var s = seeds.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < s.Length; i++)
|
||||||
|
{
|
||||||
|
ref var seed = ref s[i];
|
||||||
|
seed.DropInDays -= days;
|
||||||
|
if (seed.DropInDays > 0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropped.Add(entities.EntityAt(i));
|
||||||
|
var pos = t[i].Position;
|
||||||
|
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
||||||
|
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
||||||
|
var cell = cy * _width + cx;
|
||||||
|
if (!_cellLand[cell])
|
||||||
|
{
|
||||||
|
continue; // упало в воду — семя пропадает
|
||||||
|
}
|
||||||
|
|
||||||
|
PlantFactory.Create(
|
||||||
|
_store,
|
||||||
|
_plants,
|
||||||
|
seed.PlantSpecies,
|
||||||
|
pos,
|
||||||
|
ageDays: 0f,
|
||||||
|
seed.Genome,
|
||||||
|
_cellFertility[cell],
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var eater in _dropped)
|
||||||
|
{
|
||||||
|
if (!eater.IsNull && eater.HasComponent<GutSeed>())
|
||||||
|
{
|
||||||
|
eater.RemoveComponent<GutSeed>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user