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/Mods/Core/Defs/Hediffs/Hediffs.json b/Mods/Core/Defs/Hediffs/Hediffs.json index f684124..954059a 100644 --- a/Mods/Core/Defs/Hediffs/Hediffs.json +++ b/Mods/Core/Defs/Hediffs/Hediffs.json @@ -32,6 +32,29 @@ "defName": "Poisoned", "label": "hediff.poisoned", "initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2, "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 } } ] } diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index a63e94b..2f0a270 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -73,6 +73,9 @@ "hediff.fever": "fever", "hediff.bleeding": "bleeding", "hediff.poisoned": "poisoned", + "hediff.hypothermia": "hypothermia", + "hediff.heatstroke": "heatstroke", + "hediff.infection": "infection", "inspect.hint": "LMB — select · RMB/Esc — clear", "hud.paused": "PAUSED", "menu.title": "LittleSim", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 8cb7bef..0d93144 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -73,6 +73,9 @@ "hediff.fever": "лихорадка", "hediff.bleeding": "кровотечение", "hediff.poisoned": "отравление", + "hediff.hypothermia": "переохлаждение", + "hediff.heatstroke": "тепловой удар", + "hediff.infection": "заражение", "inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять", "hud.paused": "ПАУЗА", "menu.title": "LittleSim", diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 8ecf0a0..15310e8 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -242,7 +242,11 @@ public sealed class WorldScene : Scene CellSize, Context.Clock, SecondsPerDay, + climate, content.Defs.All().Where(h => h.AmbientPerDay > 0f).ToArray(), + content.Defs.TryGet("Hypothermia", out var hypo) ? hypo : null, + content.Defs.TryGet("Heatstroke", out var heat) ? heat : null, + content.Defs.TryGet("Infection", out var infe) ? infe : null, _config.Seed + 0x3EAD ) ); @@ -305,6 +309,19 @@ public sealed class WorldScene : Scene _config.Seed + 0x6E66 ) ); + UpdateSystems.Add( + new SeedDispersalSystem( + Store, + _plants, + Context.Clock, + SecondsPerDay, + _config.Width, + _config.Height, + CellSize, + _cellFertility, + _cellLand + ) + ); UpdateSystems.Add( 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)")}, " + $"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 73acdcc..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"), @@ -484,6 +488,23 @@ public struct Egg : IComponent public float IncubateDays; } +/// +/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт +/// его семя в кишечнике, затем «высаживает» его в другом месте () — +/// зверь становится разносчиком флоры. Геном — managed-ссылка (клон родительского растения). +/// +public struct GutSeed : IComponent +{ + /// Индекс вида растения в . + public int PlantSpecies; + + /// Геном будущего ростка (клон съеденного растения). + public Genome Genome; + + /// Остаток времени до высадки в игровых днях; ≤0 — росток падает на землю. + public float DropInDays; +} + /// Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания. public struct ThoughtInstance { diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 90d2974..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( @@ -791,6 +870,8 @@ public sealed class AnimalActionSystem : BaseSystem private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась» private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает + private const float SeedIngestChance = 0.15f; // шанс проглотить семя при выедании зрелого растения + private const float GutSeedDropDays = 2f; // через сколько дней проглоченное семя высаживается private readonly EntityStore _store; private readonly PlantSet _plants; @@ -815,6 +896,7 @@ public sealed class AnimalActionSystem : BaseSystem private readonly List _consumed = []; // трупы, доеденные до нуля private readonly List _kills = []; // добыча, забитая в этом проходе (смерть после итерации) private readonly List<(Entity Self, Entity Partner)> _matings = []; + private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория) public AnimalActionSystem( EntityStore store, @@ -859,6 +941,7 @@ public sealed class AnimalActionSystem : BaseSystem _consumed.Clear(); _kills.Clear(); _matings.Clear(); + _ingested.Clear(); foreach ( var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks @@ -905,6 +988,8 @@ public sealed class AnimalActionSystem : BaseSystem o[i].Traits.ToxinTolerance, days ); + // Эндозоохория: шанс проглотить семя зрелого растения (высадит позже). + TryIngestSeed(brain.TargetPlant, entities.EntityAt(i)); } break; @@ -984,9 +1069,55 @@ public sealed class AnimalActionSystem : BaseSystem AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп } + foreach (var (eater, seed) in _ingested) + { + if (!eater.IsNull && !eater.HasComponent()) + { + eater.AddComponent(seed); // структурное добавление — после прохода + } + } + ApplyMatings(); } + // Эндозоохория: при выедании ЗРЕЛОГО растения шанс проглотить его семя — зверь понесёт его и + // высадит позже в другом месте (SeedDispersalSystem). Геном клонируется (отдельная особь-росток). + private void TryIngestSeed(int plantId, Entity eater) + { + if ( + eater.IsNull + || eater.HasComponent() + || plantId < 0 + || _rng.NextSingle() >= SeedIngestChance + || !_store.TryGetEntityById(plantId, out var plant) + || plant.IsNull + || !plant.HasComponent() + || !plant.HasComponent() + ) + { + return; + } + + ref readonly var grow = ref plant.GetComponent(); + if (grow.Stage < _plants[grow.Species].Stages.Length - 1) + { + return; // только зрелое растение даёт семя + } + + var genome = new Genome(plant.GetComponent().Genome.ToDictionary()); + _ingested.Add( + ( + eater, + new GutSeed + { + PlantSpecies = grow.Species, + Genome = genome, + DropInDays = GutSeedDropDays, + } + ) + ); + } + // Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо), // либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый // травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die). @@ -1833,7 +1964,11 @@ public sealed class AnimalHealthSystem : BaseSystem private readonly int _cellSize; private readonly GameClock _clock; private readonly float _secondsPerDay; + private readonly Climate _climate; private readonly HediffDef[] _ambient; + private readonly HediffDef? _hypothermia; + private readonly HediffDef? _heatstroke; + private readonly HediffDef? _infection; private readonly Random _rng; private readonly ArchetypeQuery _query; private readonly List _deaths = []; @@ -1844,13 +1979,28 @@ public sealed class AnimalHealthSystem : BaseSystem private const float SenescenceStart = 0.6f; 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( EntityStore store, AnimalSet animals, int cellSize, GameClock clock, float secondsPerDay, + Climate climate, HediffDef[] ambient, + HediffDef? hypothermia, + HediffDef? heatstroke, + HediffDef? infection, int seed ) { @@ -1859,7 +2009,11 @@ public sealed class AnimalHealthSystem : BaseSystem _cellSize = cellSize; _clock = clock; _secondsPerDay = secondsPerDay; + _climate = climate; _ambient = ambient; + _hypothermia = hypothermia; + _heatstroke = heatstroke; + _infection = infection; _rng = new Random(seed); _query = store.Query(); } @@ -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( days, state.Capacity(AnimalCapacities.BloodFiltration) @@ -1958,3 +2152,102 @@ public sealed class AnimalHealthSystem : BaseSystem } } } + +/// +/// Разнос семян (эндозоохория): тикает таймер проглоченного семени (); по +/// истечении высаживает росток того вида растения там, где сейчас зверь (если клетка — суша), +/// геномом-клоном съеденного растения, и снимает компонент. Делает травоядных разносчиками флоры — +/// растения расселяются вдоль кормовых маршрутов. +/// +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 _query; + private readonly List _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(); + } + + 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()) + { + eater.RemoveComponent(); + } + } + } +}