Compare commits
10
Commits
d196437462
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba8e590941 | ||
|
|
9f5f5e2ca9 | ||
|
|
010d401b75 | ||
|
|
d0a0714563 | ||
|
|
92d0302138 | ||
|
|
36b6d576a1 | ||
|
|
02dcd01c0f | ||
|
|
56f2c22932 | ||
|
|
9f6b8a0554 | ||
|
|
ccb7c960f9 |
@@ -417,4 +417,7 @@ FodyWeavers.xsd
|
||||
|
||||
# LittleSim
|
||||
Cache/
|
||||
# Рантайм-файлы (пишутся игрой в рабочем каталоге) — не версионируем
|
||||
/settings.json
|
||||
/Saves/
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"GeneLitterSize": 1,
|
||||
"GeneHerbivory": 1.0,
|
||||
"GeneToxinTolerance": 0.1,
|
||||
"GeneFeedingStyle": 0.15,
|
||||
"GeneSociability": 0.8
|
||||
}
|
||||
},
|
||||
@@ -55,9 +56,10 @@
|
||||
"baseSpeed": 32,
|
||||
"visionCells": 18,
|
||||
"attackRangeCells": 1.3,
|
||||
"attackDamage": 80,
|
||||
"attackBleed": 3.0,
|
||||
"attackBloodLoss": 5.0,
|
||||
"attackIntervalSec": 1.4,
|
||||
"attackDamage": 16,
|
||||
"attackBleed": 0.16,
|
||||
"attackBloodLoss": 0.06,
|
||||
"baseMassKg": 45,
|
||||
"genome": {
|
||||
"GeneMaxBodySize": 1.6,
|
||||
@@ -80,6 +82,7 @@
|
||||
"GeneGestationDays": 35,
|
||||
"GeneLitterSize": 4,
|
||||
"GeneCarnivory": 1.0,
|
||||
"GeneFeedingStyle": 0.9,
|
||||
"GeneSociability": 0.6
|
||||
}
|
||||
},
|
||||
@@ -95,9 +98,10 @@
|
||||
"baseSpeed": 26,
|
||||
"visionCells": 13,
|
||||
"attackRangeCells": 1.2,
|
||||
"attackDamage": 45,
|
||||
"attackBleed": 1.8,
|
||||
"attackBloodLoss": 3.0,
|
||||
"attackIntervalSec": 2.0,
|
||||
"attackDamage": 9,
|
||||
"attackBleed": 0.1,
|
||||
"attackBloodLoss": 0.035,
|
||||
"baseMassKg": 80,
|
||||
"genome": {
|
||||
"GeneMaxBodySize": 1.3,
|
||||
@@ -123,6 +127,7 @@
|
||||
"GeneCarnivory": 0.2,
|
||||
"GeneOmnivory": 0.85,
|
||||
"GeneToxinTolerance": 0.25,
|
||||
"GeneFeedingStyle": 0.55,
|
||||
"GeneSociability": 0.3
|
||||
}
|
||||
},
|
||||
@@ -161,6 +166,7 @@
|
||||
"GeneHerbivory": 0.8,
|
||||
"GeneToxinTolerance": 0.15,
|
||||
"GeneEggLaying": 1.0,
|
||||
"GeneFeedingStyle": 0.2,
|
||||
"GeneSociability": 0.55
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,13 @@
|
||||
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||
"effects": { "toxinTolerance": "value" } },
|
||||
|
||||
// Стиль питания [0..1]: 0 — пастьба (graze, частые мелкие приёмы, держит сытость высокой),
|
||||
// 1 — обжорство (gorge, ищет еду лишь при сильном голоде и ест до полного — редкие крупные приёмы,
|
||||
// как у хищников). Задаёт пороги начала/конца кормёжки в AnimalDecisionSystem. Эволюционирует.
|
||||
{ "defName": "GeneFeedingStyle", "parent": "BaseNumericGene", "label": "gene.feedingStyle",
|
||||
"default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.05, "tags": ["animal", "diet"],
|
||||
"effects": { "feedingStyle": "value" } },
|
||||
|
||||
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
|
||||
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
|
||||
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"season.autumn": "autumn",
|
||||
"season.winter": "winter",
|
||||
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
|
||||
"hud.inspector": "F1 — inspector",
|
||||
"hud.spawner": "F9 — spawner",
|
||||
"inspect.tab.overview": "Overview",
|
||||
"inspect.tab.genes": "Genes",
|
||||
"inspect.tab.products": "Products",
|
||||
@@ -27,7 +29,7 @@
|
||||
"inspect.state.dormanthot": "dormant — too hot",
|
||||
"inspect.stress": "Stress: {0:0.0} d to death",
|
||||
"inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
|
||||
"inspect.genes": "— Genes —",
|
||||
"inspect.genes": "Genes",
|
||||
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d",
|
||||
"inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}",
|
||||
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
|
||||
@@ -48,14 +50,17 @@
|
||||
"skill.hunting": "hunting",
|
||||
"skill.evasion": "evasion",
|
||||
"inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%",
|
||||
"inspect.health.caps": "— Capacities —",
|
||||
"inspect.health.vitals": "Vitals",
|
||||
"inspect.vital.blood": "blood",
|
||||
"inspect.vital.pain": "pain",
|
||||
"inspect.health.caps": "Capacities",
|
||||
"inspect.health.cap": "{0}: {1:0}%",
|
||||
"inspect.health.hediffs": "— Conditions —",
|
||||
"inspect.health.hediffs": "Conditions",
|
||||
"inspect.health.hediffline": "{0}: severity {1:0}%",
|
||||
"inspect.health.none": "Healthy, no wounds or illness",
|
||||
"inspect.mood.value": "Mood: {0:0}%",
|
||||
"inspect.mood.none": "No mood (primitive brain)",
|
||||
"inspect.mood.thoughts": "— Thoughts —",
|
||||
"inspect.mood.thoughts": "Thoughts",
|
||||
"inspect.mood.thoughtline": "{0}: {1}",
|
||||
"inspect.mood.calm": "Calm, nothing on its mind",
|
||||
"animalstage.baby": "baby",
|
||||
@@ -117,6 +122,8 @@
|
||||
"settings.volume": "Volume",
|
||||
"settings.uiscale": "UI scale",
|
||||
"settings.devmode": "Developer mode",
|
||||
"settings.on": "On",
|
||||
"settings.off": "Off",
|
||||
"dev.title": "Dev spawner",
|
||||
"dev.hint": "Pick an entry · F9 to hide",
|
||||
"dev.armed": "In hand: {0} · LMB to spawn, RMB to clear",
|
||||
@@ -135,7 +142,7 @@
|
||||
"pause.mainmenu": "Main menu",
|
||||
"pause.quit": "Quit",
|
||||
"pause.saved": "Saved: {0}",
|
||||
"speed.pause": "⏸",
|
||||
"speed.pause": "Pause",
|
||||
"credits.title": "Credits",
|
||||
"credits.author": "mrleo1nid",
|
||||
"credits.role": "Concept, code, mrgameeng engine",
|
||||
@@ -191,6 +198,43 @@
|
||||
"thought.gaveBirth": "gave birth",
|
||||
"thought.mated": "mated",
|
||||
"thought.quenchedThirst": "drank its fill",
|
||||
"gene.feedingStyle": "feeding style",
|
||||
"inspect.animal.feeding": "Feeding: {0}",
|
||||
"feeding.gorge": "gorger (rare, to the full)",
|
||||
"feeding.graze": "grazer (often, little by little)",
|
||||
"feeding.mixed": "moderate",
|
||||
"inspect.tab.log": "Log",
|
||||
"log.empty": "No history yet",
|
||||
"log.stamp": "D{0} {1:00}:{2:00}",
|
||||
"hud.events": "Events",
|
||||
"hud.perf": "Perf",
|
||||
"events.title": "Event log",
|
||||
"events.empty": "Nothing has happened yet",
|
||||
"events.hint": "Double-click — jump to the creature",
|
||||
"perf.title": "Performance",
|
||||
"perf.fps": "{0} FPS · {1:0.0} ms/frame",
|
||||
"perf.root": "{0}: {1:0.000} ms · systems {2}",
|
||||
"perf.systemline": "{0} {1:0.000} ms {2:0}%",
|
||||
"perf.hint": "Refreshes ~5/s · costliest systems shown",
|
||||
"event.mated": "Pair: {0} + {1}",
|
||||
"event.pregnant": "{0}: pregnant",
|
||||
"event.birth": "{0}: birth ×{1}",
|
||||
"event.hatch": "{0}: hatched ×{1}",
|
||||
"event.hunt": "Hunt: {0} → {1}",
|
||||
"event.injured": "Wound: {0} → {1}",
|
||||
"event.struck": "Hit: {0} → {1} ({2:0})",
|
||||
"event.tookhit": "Hit: {1} → {0} ({2:0})",
|
||||
"event.killed": "Kill: {0} → {1}",
|
||||
"event.ate": "{0} is feeding",
|
||||
"event.drank": "{0} is drinking",
|
||||
"event.rest": "{0} is resting",
|
||||
"event.dig": "{0} digs for food",
|
||||
"event.wallow": "{0} wallows in mud",
|
||||
"event.fled": "{0} is fleeing",
|
||||
"event.grew": "{0}: new stage — {1}",
|
||||
"event.death.starve": "Death: {0} (hunger/thirst)",
|
||||
"event.death.age": "Death: {0} (old age)",
|
||||
"event.death.disease": "Death: {0} (disease)",
|
||||
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
||||
"net.connecting": "connecting…",
|
||||
"net.connected": "connected",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"season.autumn": "осень",
|
||||
"season.winter": "зима",
|
||||
"hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
|
||||
"hud.inspector": "F1 — инспектор",
|
||||
"hud.spawner": "F9 — спавнер",
|
||||
"inspect.tab.overview": "Обзор",
|
||||
"inspect.tab.genes": "Гены",
|
||||
"inspect.tab.products": "Продукты",
|
||||
@@ -27,7 +29,7 @@
|
||||
"inspect.state.dormanthot": "покой — слишком жарко",
|
||||
"inspect.stress": "Стресс: {0:0.0} дн до гибели",
|
||||
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
|
||||
"inspect.genes": "— Гены —",
|
||||
"inspect.genes": "Гены",
|
||||
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
|
||||
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
|
||||
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
|
||||
@@ -48,14 +50,17 @@
|
||||
"skill.hunting": "охота",
|
||||
"skill.evasion": "уклонение",
|
||||
"inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%",
|
||||
"inspect.health.caps": "— Способности —",
|
||||
"inspect.health.vitals": "Жизненные показатели",
|
||||
"inspect.vital.blood": "кровь",
|
||||
"inspect.vital.pain": "боль",
|
||||
"inspect.health.caps": "Способности",
|
||||
"inspect.health.cap": "{0}: {1:0}%",
|
||||
"inspect.health.hediffs": "— Состояния —",
|
||||
"inspect.health.hediffs": "Состояния",
|
||||
"inspect.health.hediffline": "{0}: тяжесть {1:0}%",
|
||||
"inspect.health.none": "Здоров, ран и болезней нет",
|
||||
"inspect.mood.value": "Настроение: {0:0}%",
|
||||
"inspect.mood.none": "Настроения нет (примитивный мозг)",
|
||||
"inspect.mood.thoughts": "— Мысли —",
|
||||
"inspect.mood.thoughts": "Мысли",
|
||||
"inspect.mood.thoughtline": "{0}: {1}",
|
||||
"inspect.mood.calm": "Спокоен, особых мыслей нет",
|
||||
"animalstage.baby": "детёныш",
|
||||
@@ -117,6 +122,8 @@
|
||||
"settings.volume": "Громкость",
|
||||
"settings.uiscale": "Масштаб UI",
|
||||
"settings.devmode": "Режим разработчика",
|
||||
"settings.on": "Вкл",
|
||||
"settings.off": "Откл",
|
||||
"dev.title": "Дев-спавнер",
|
||||
"dev.hint": "Выбери пункт · F9 — скрыть",
|
||||
"dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
|
||||
@@ -135,7 +142,7 @@
|
||||
"pause.mainmenu": "Главное меню",
|
||||
"pause.quit": "Выход",
|
||||
"pause.saved": "Сохранено: {0}",
|
||||
"speed.pause": "⏸",
|
||||
"speed.pause": "Пауза",
|
||||
"credits.title": "Авторы",
|
||||
"credits.author": "mrleo1nid",
|
||||
"credits.role": "Идея, код, движок mrgameeng",
|
||||
@@ -191,6 +198,43 @@
|
||||
"thought.gaveBirth": "родила потомство",
|
||||
"thought.mated": "спарилась",
|
||||
"thought.quenchedThirst": "напилась вволю",
|
||||
"gene.feedingStyle": "стиль питания",
|
||||
"inspect.animal.feeding": "Питание: {0}",
|
||||
"feeding.gorge": "обжора (редко, до отвала)",
|
||||
"feeding.graze": "пастьба (часто, понемногу)",
|
||||
"feeding.mixed": "умеренное",
|
||||
"inspect.tab.log": "История",
|
||||
"log.empty": "История пуста",
|
||||
"log.stamp": "Д{0} {1:00}:{2:00}",
|
||||
"hud.events": "События",
|
||||
"hud.perf": "Произв.",
|
||||
"events.title": "Журнал событий",
|
||||
"events.empty": "Пока ничего не произошло",
|
||||
"events.hint": "Двойной клик — перейти к существу",
|
||||
"perf.title": "Производительность",
|
||||
"perf.fps": "{0} FPS · {1:0.0} мс/кадр",
|
||||
"perf.root": "{0}: {1:0.000} мс · систем {2}",
|
||||
"perf.systemline": "{0} {1:0.000} мс {2:0}%",
|
||||
"perf.hint": "Обновляется ~5 раз/с · показаны самые дорогие системы",
|
||||
"event.mated": "Пара: {0} + {1}",
|
||||
"event.pregnant": "{0}: беременность",
|
||||
"event.birth": "{0}: роды ×{1}",
|
||||
"event.hatch": "{0}: вылупление ×{1}",
|
||||
"event.hunt": "Охота: {0} → {1}",
|
||||
"event.injured": "Ранение: {0} → {1}",
|
||||
"event.struck": "Удар: {0} → {1} ({2:0})",
|
||||
"event.tookhit": "Удар: {1} → {0} ({2:0})",
|
||||
"event.killed": "Добыча: {0} → {1}",
|
||||
"event.ate": "{0} кормится",
|
||||
"event.drank": "{0} пьёт воду",
|
||||
"event.rest": "{0} отдыхает",
|
||||
"event.dig": "{0} роет землю в поисках корма",
|
||||
"event.wallow": "{0} валяется в грязи",
|
||||
"event.fled": "{0} спасается бегством",
|
||||
"event.grew": "{0}: новая стадия — {1}",
|
||||
"event.death.starve": "Гибель: {0} (голод/жажда)",
|
||||
"event.death.age": "Гибель: {0} (старость)",
|
||||
"event.death.disease": "Гибель: {0} (болезнь)",
|
||||
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||
"net.connecting": "подключение…",
|
||||
"net.connected": "подключено",
|
||||
|
||||
+1
-1
Submodule engine updated: 96e19c7c61...46f3931f85
@@ -0,0 +1,17 @@
|
||||
namespace LittleSim.Web;
|
||||
|
||||
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
|
||||
public enum ConnectionStatus
|
||||
{
|
||||
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>Идёт первая попытка подключения.</summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>Соединение установлено, снапшоты приходят.</summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
|
||||
Reconnecting,
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
<Compile Include="Pages\Index.razor.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="LittleSimWebGame.cs" />
|
||||
<Compile Include="ConnectionStatus.cs" />
|
||||
<Compile Include="NetContract.cs" />
|
||||
<Compile Include="WorldViewScene.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Friflo.Engine.ECS;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Net;
|
||||
@@ -36,7 +38,34 @@ public static class WebNetSchema
|
||||
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
|
||||
public const int DefaultPort = 9050;
|
||||
|
||||
// Те же wire-размеры, что и в десктопном NetSchema.cs — проверяем, что зеркало не разошлось по раскладке.
|
||||
private const int TransformWireSize = 20; // Vector2(8) + float(4) + Vector2(8)
|
||||
private const int PawnNeedsWireSize = 4; // float(4)
|
||||
|
||||
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
|
||||
public static ReplicationSchema Create() =>
|
||||
new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
|
||||
public static ReplicationSchema Create()
|
||||
{
|
||||
VerifyLayout();
|
||||
return new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
|
||||
}
|
||||
|
||||
/// <summary>Сверяет байт-раскладку зеркальных структур с десктопным контрактом (NetSchema.cs).</summary>
|
||||
private static void VerifyLayout()
|
||||
{
|
||||
Require<NetTransform>(TransformWireSize);
|
||||
Require<NetPawnNeeds>(PawnNeedsWireSize);
|
||||
|
||||
static void Require<T>(int expected)
|
||||
where T : struct
|
||||
{
|
||||
var actual = Unsafe.SizeOf<T>();
|
||||
if (actual != expected)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Размер зеркального {typeof(T).Name} = {actual} Б, ожидался {expected} Б: "
|
||||
+ "wire-контракт разошёлся — синхронизируй NetContract.cs и LittleSim/Net/NetSchema.cs."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,6 @@ using MrGameEng.Net;
|
||||
|
||||
namespace LittleSim.Web;
|
||||
|
||||
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
|
||||
public enum ConnectionStatus
|
||||
{
|
||||
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>Идёт первая попытка подключения.</summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>Соединение установлено, снапшоты приходят.</summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
|
||||
Reconnecting,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
||||
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Audio;
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>
|
||||
@@ -35,62 +30,3 @@ public sealed class GameSettings
|
||||
/// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
|
||||
public bool DeveloperMode { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||
public static class GameSettingsStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary>
|
||||
public static GameSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(GamePaths.SettingsFile))
|
||||
{
|
||||
return JsonSerializer.Deserialize<GameSettings>(
|
||||
File.ReadAllText(GamePaths.SettingsFile),
|
||||
JsonOptions
|
||||
) ?? new GameSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or JsonException)
|
||||
{
|
||||
// Битый файл настроек не должен ронять запуск — откатываемся к умолчаниям.
|
||||
}
|
||||
|
||||
return new GameSettings();
|
||||
}
|
||||
|
||||
/// <summary>Сохраняет настройки в <see cref="GamePaths.SettingsFile"/>.</summary>
|
||||
public static void Save(GameSettings settings)
|
||||
{
|
||||
Directory.CreateDirectory(GamePaths.Root);
|
||||
File.WriteAllText(GamePaths.SettingsFile, JsonSerializer.Serialize(settings, JsonOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Применяет настройки к движку: язык (<paramref name="languages"/>), полноэкранный
|
||||
/// режим/VSync/размер окна (<paramref name="graphics"/>) и громкость (<paramref name="audio"/>).
|
||||
/// </summary>
|
||||
public static void Apply(
|
||||
GameSettings settings,
|
||||
GraphicsDeviceManager graphics,
|
||||
LanguageManager languages,
|
||||
AudioManager audio
|
||||
)
|
||||
{
|
||||
languages.SetLanguage(settings.Language);
|
||||
audio.MasterVolume = settings.Volume;
|
||||
|
||||
graphics.IsFullScreen = settings.Fullscreen;
|
||||
graphics.SynchronizeWithVerticalRetrace = settings.VSync;
|
||||
graphics.PreferredBackBufferWidth = settings.Width;
|
||||
graphics.PreferredBackBufferHeight = settings.Height;
|
||||
graphics.ApplyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Audio;
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||
public static class GameSettingsStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary>
|
||||
public static GameSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(GamePaths.SettingsFile))
|
||||
{
|
||||
return JsonSerializer.Deserialize<GameSettings>(
|
||||
File.ReadAllText(GamePaths.SettingsFile),
|
||||
JsonOptions
|
||||
) ?? new GameSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or JsonException)
|
||||
{
|
||||
// Битый файл настроек не должен ронять запуск — откатываемся к умолчаниям.
|
||||
}
|
||||
|
||||
return new GameSettings();
|
||||
}
|
||||
|
||||
/// <summary>Сохраняет настройки в <see cref="GamePaths.SettingsFile"/>.</summary>
|
||||
public static void Save(GameSettings settings)
|
||||
{
|
||||
Directory.CreateDirectory(GamePaths.Root);
|
||||
File.WriteAllText(GamePaths.SettingsFile, JsonSerializer.Serialize(settings, JsonOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Применяет настройки к движку: язык (<paramref name="languages"/>), полноэкранный
|
||||
/// режим/VSync/размер окна (<paramref name="graphics"/>) и громкость (<paramref name="audio"/>).
|
||||
/// </summary>
|
||||
public static void Apply(
|
||||
GameSettings settings,
|
||||
GraphicsDeviceManager graphics,
|
||||
LanguageManager languages,
|
||||
AudioManager audio
|
||||
)
|
||||
{
|
||||
languages.SetLanguage(settings.Language);
|
||||
audio.MasterVolume = settings.Volume;
|
||||
|
||||
graphics.IsFullScreen = settings.Fullscreen;
|
||||
graphics.SynchronizeWithVerticalRetrace = settings.VSync;
|
||||
graphics.PreferredBackBufferWidth = settings.Width;
|
||||
graphics.PreferredBackBufferHeight = settings.Height;
|
||||
graphics.ApplyChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using MrGameEng.DevConsole;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>
|
||||
/// Дев-инструмент: per-system тайминги Friflo. Включает мониторинг (<c>SetMonitorPerf</c>) на корнях
|
||||
/// систем и печатает в дев-консоль командой <c>perf</c> отсортированную по времени таблицу систем
|
||||
/// (среднее мс/кадр по последним кадрам + доля от корня). Доступно только в режиме разработчика.
|
||||
/// </summary>
|
||||
public static class PerfReport
|
||||
{
|
||||
private const int DefaultTop = 15; // сколько самых дорогих систем печатать по умолчанию
|
||||
|
||||
/// <summary>Окно усреднения времени (последние N обновлений) — используют и консоль, и perf-окно.</summary>
|
||||
public const int AvgFrames = 30;
|
||||
|
||||
private static readonly List<(string Name, double AvgMs, int Updates)> Scratch = []; // без аллокаций на вызов
|
||||
|
||||
/// <summary>Включает мониторинг на корнях и регистрирует команду <c>perf [N]</c> (топ-N систем).</summary>
|
||||
public static void Register(DevConsole console, params SystemRoot[] roots)
|
||||
{
|
||||
EnableMonitoring(roots);
|
||||
console.Register(
|
||||
"perf",
|
||||
"perf [N] — тайминги систем (среднее мс/кадр, топ-N по времени)",
|
||||
(c, args) =>
|
||||
{
|
||||
// Мониторинг пере-утверждаем на случай систем, добавленных после Register (идемпотентно).
|
||||
EnableMonitoring(roots);
|
||||
var top =
|
||||
args.Length > 0 && int.TryParse(args[0], out var n) && n > 0 ? n : DefaultTop;
|
||||
foreach (var root in roots)
|
||||
{
|
||||
WriteRoot(c, root, top);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Включает per-system мониторинг Friflo на корнях (идемпотентно; можно звать повторно для
|
||||
/// систем, добавленных позже). Публично — чтобы perf-окно тоже могло его утвердить.</summary>
|
||||
public static void EnableMonitoring(params SystemRoot[] roots)
|
||||
{
|
||||
foreach (var root in roots)
|
||||
{
|
||||
root.SetMonitorPerf(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Заполняет <paramref name="into"/> строками (имя, среднее мс/кадр, число обновлений) по листовым
|
||||
/// системам корня, сортированными по времени убыв.; возвращает суммарное среднее мс/кадр корня
|
||||
/// (0, если данных ещё нет). Общий сбор для консольной команды и perf-окна — без дублирования.
|
||||
/// </summary>
|
||||
public static double Snapshot(
|
||||
SystemRoot root,
|
||||
List<(string Name, double AvgMs, int Updates)> into
|
||||
)
|
||||
{
|
||||
into.Clear();
|
||||
Collect(root, into);
|
||||
into.Sort((a, b) => b.AvgMs.CompareTo(a.AvgMs));
|
||||
return root.Perf.UpdateCount == 0 ? 0.0 : root.Perf.LastAvgMs(AvgFrames);
|
||||
}
|
||||
|
||||
private static void WriteRoot(DevConsole console, SystemRoot root, int top)
|
||||
{
|
||||
var total = Snapshot(root, Scratch);
|
||||
if (root.Perf.UpdateCount == 0)
|
||||
{
|
||||
console.WriteLine($"[{root.Name}] нет данных — подожди кадр и повтори 'perf'.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.WriteLine($"[{root.Name}] {total:0.000} мс/кадр · систем: {Scratch.Count}");
|
||||
var shown = System.Math.Min(top, Scratch.Count);
|
||||
for (var i = 0; i < shown; i++)
|
||||
{
|
||||
var (name, avg, _) = Scratch[i];
|
||||
var pct = total > 0.0001 ? avg / total * 100.0 : 0.0;
|
||||
console.WriteLine($" {name, -30} {avg, 8:0.000} мс {pct, 3:0}%");
|
||||
}
|
||||
|
||||
if (Scratch.Count > shown)
|
||||
{
|
||||
console.WriteLine($" … ещё {Scratch.Count - shown} систем(ы)");
|
||||
}
|
||||
}
|
||||
|
||||
// Рекурсивно собирает листовые системы (группы разворачиваются — интересны конкретные системы).
|
||||
private static void Collect(SystemGroup group, List<(string, double, int)> into)
|
||||
{
|
||||
foreach (var child in group.ChildSystems)
|
||||
{
|
||||
if (child is SystemGroup sub)
|
||||
{
|
||||
Collect(sub, into);
|
||||
}
|
||||
else
|
||||
{
|
||||
into.Add((child.Name, child.Perf.LastAvgMs(AvgFrames), child.Perf.UpdateCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>
|
||||
/// Сериализуемое состояние одного животного (фазы A1–A8): вид, позиция, геном (источник признаков и
|
||||
/// пола), поколение, стадия/возраст, нужды, здоровье (кровь, HP частей, хедифы), настроение и
|
||||
/// беременность. Спрайт пересобирается фабрикой из дефа. Чтобы популяция (эволюция/состояние)
|
||||
/// переживала сейв/лоад, а не пересыпалась заново из сида.
|
||||
/// </summary>
|
||||
public sealed class AnimalSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся индекс/стадии/текстуры/тело.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Номер поколения (для popstats/дрейфа).</summary>
|
||||
public int Generation { get; set; }
|
||||
|
||||
/// <summary>Текущая стадия роста.</summary>
|
||||
public int Stage { get; set; }
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays { get; set; }
|
||||
|
||||
/// <summary>Геном особи: аллели по id генов (переменного состава; несёт и пол через GeneSex).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
|
||||
/// <summary>Значения нужд по индексам NeedSet.</summary>
|
||||
public float[] Needs { get; set; } = [];
|
||||
|
||||
/// <summary>Накоплено дней истощения.</summary>
|
||||
public float Starve { get; set; }
|
||||
|
||||
/// <summary>Уровень крови [0,1].</summary>
|
||||
public float BloodLevel { get; set; } = 1f;
|
||||
|
||||
/// <summary>HP частей тела по порядку анатомии вида (пусто — у вида нет тела).</summary>
|
||||
public float[] PartHp { get; set; } = [];
|
||||
|
||||
/// <summary>Активные хедифы (гон/болезни).</summary>
|
||||
public List<HediffSave> Hediffs { get; set; } = [];
|
||||
|
||||
/// <summary>Текущее настроение [0,1].</summary>
|
||||
public float Mood { get; set; }
|
||||
|
||||
/// <summary>Активно ли настроение (тир 4+).</summary>
|
||||
public bool MoodActive { get; set; }
|
||||
|
||||
/// <summary>Активные событийные мысли.</summary>
|
||||
public List<ThoughtSave> Thoughts { get; set; } = [];
|
||||
|
||||
/// <summary>Беременна ли самка.</summary>
|
||||
public bool Pregnant { get; set; }
|
||||
|
||||
/// <summary>Геном отца (если беременна) — «слепок» на момент зачатия.</summary>
|
||||
public Dictionary<string, Allele> FatherGenome { get; set; } = new();
|
||||
|
||||
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||
public float DueInDays { get; set; }
|
||||
|
||||
/// <summary>Размер помёта.</summary>
|
||||
public int Litter { get; set; }
|
||||
|
||||
/// <summary>Уровни навыков [0..1] по индексам SkillSet.</summary>
|
||||
public float[] SkillLevels { get; set; } = [];
|
||||
|
||||
/// <summary>Страсть к навыкам (0/1/2) по индексам SkillSet.</summary>
|
||||
public int[] SkillPassions { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary>
|
||||
public sealed class CorpseSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся спрайт трупа.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||
public float RotDays { get; set; }
|
||||
|
||||
/// <summary>Размер тела (для масштаба спрайта).</summary>
|
||||
public float BodySize { get; set; }
|
||||
|
||||
/// <summary>Доступное мясо.</summary>
|
||||
public float Meat { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.</summary>
|
||||
public sealed class EggSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Поколение будущего детёныша.</summary>
|
||||
public int Generation { get; set; }
|
||||
|
||||
/// <summary>Остаток инкубации в игровых днях.</summary>
|
||||
public float IncubateDays { get; set; }
|
||||
|
||||
/// <summary>Геном будущего детёныша (уже скрещён на момент кладки).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемый хедиф особи (фаза A6): деф (по имени), тяжесть и набранный иммунитет.</summary>
|
||||
public sealed class HediffSave
|
||||
{
|
||||
/// <summary>Имя дефа хедифа (гон/болезнь).</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Текущая тяжесть.</summary>
|
||||
public float Severity { get; set; }
|
||||
|
||||
/// <summary>Набранный иммунитет [0,1].</summary>
|
||||
public float Immunity { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
|
||||
public sealed class PlantSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays { get; set; }
|
||||
|
||||
/// <summary>Текущая стадия роста.</summary>
|
||||
public int Stage { get; set; }
|
||||
|
||||
/// <summary>Плодородность клетки.</summary>
|
||||
public float CellFertility { get; set; }
|
||||
|
||||
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
|
||||
public sealed class SaveStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
|
||||
};
|
||||
|
||||
private readonly string _directory;
|
||||
|
||||
/// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
|
||||
public SaveStore(string? directory = null) =>
|
||||
_directory = directory ?? GamePaths.SavesDirectory;
|
||||
|
||||
/// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
|
||||
public readonly record struct Entry(string FileName, WorldSave Save);
|
||||
|
||||
/// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
|
||||
public IReadOnlyList<Entry> List()
|
||||
{
|
||||
if (!Directory.Exists(_directory))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var entries = new List<Entry>();
|
||||
foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var save = JsonSerializer.Deserialize<WorldSave>(
|
||||
File.ReadAllText(file),
|
||||
JsonOptions
|
||||
);
|
||||
// Сейв из будущей (несовместимой) версии прочитать корректно нельзя — пропускаем,
|
||||
// чтобы не выдать пользователю молча битый мир.
|
||||
if (save is not null && save.Version <= WorldSave.CurrentVersion)
|
||||
{
|
||||
entries.Add(new Entry(Path.GetFileName(file), save));
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or JsonException)
|
||||
{
|
||||
// Битый файл сохранения не должен ломать список — пропускаем.
|
||||
}
|
||||
}
|
||||
|
||||
entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
|
||||
public string Write(WorldSave save)
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
var fileName = Sanitize(save.Name) + ".json";
|
||||
File.WriteAllText(
|
||||
Path.Combine(_directory, fileName),
|
||||
JsonSerializer.Serialize(save, JsonOptions)
|
||||
);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>Удаляет файл сохранения по имени.</summary>
|
||||
public void Delete(string fileName)
|
||||
{
|
||||
var path = Path.Combine(_directory, fileName);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Набор недопустимых для имени файла символов — кэшируем один раз (раньше массив аллоцировался
|
||||
// и линейно сканировался на КАЖДЫЙ символ имени).
|
||||
private static readonly HashSet<char> InvalidFileNameChars = new(
|
||||
Path.GetInvalidFileNameChars()
|
||||
);
|
||||
|
||||
private static string Sanitize(string name)
|
||||
{
|
||||
var trimmed = name.Trim();
|
||||
var safe = string.Concat(trimmed.Select(c => InvalidFileNameChars.Contains(c) ? '_' : c));
|
||||
return string.IsNullOrEmpty(safe) ? "world" : safe;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания.</summary>
|
||||
public sealed class ThoughtSave
|
||||
{
|
||||
/// <summary>Имя дефа мысли.</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||
public float RemainingDays { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>
|
||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||
/// детерминированно регенерируется из сида при загрузке.
|
||||
/// </summary>
|
||||
public sealed class WorldSave
|
||||
{
|
||||
/// <summary>Текущая версия схемы сейва (поднимать при несовместимых изменениях полей).</summary>
|
||||
public const int CurrentVersion = 1;
|
||||
|
||||
/// <summary>Версия схемы файла. Старые сейвы без поля совместимы (схема аддитивна) и читаются как
|
||||
/// текущая версия; проверка нужна, чтобы отклонить файл из БУДУЩЕЙ (несовместимой) версии.</summary>
|
||||
public int Version { get; set; } = CurrentVersion;
|
||||
|
||||
/// <summary>Имя мира (и основа имени файла).</summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>Ширина мира в клетках.</summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>Высота мира в клетках.</summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>Сид генерации.</summary>
|
||||
public int Seed { get; set; }
|
||||
|
||||
/// <summary>Число проходов сглаживания рельефа.</summary>
|
||||
public int SmoothPasses { get; set; } = 4;
|
||||
|
||||
/// <summary>Население на момент сохранения.</summary>
|
||||
public int Population { get; set; }
|
||||
|
||||
/// <summary>Момент сохранения (UTC).</summary>
|
||||
public DateTime SavedUtc { get; set; }
|
||||
|
||||
/// <summary>Прошедшее время симуляции в секундах.</summary>
|
||||
public double ElapsedSeconds { get; set; }
|
||||
|
||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||
public List<PlantSave> Plants { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность).</summary>
|
||||
public List<AnimalSave> Animals { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
|
||||
public List<CorpseSave> Corpses { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
|
||||
public List<EggSave> Eggs { get; set; } = [];
|
||||
|
||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||
public WorldConfig ToConfig() =>
|
||||
new()
|
||||
{
|
||||
Name = Name,
|
||||
Width = Width,
|
||||
Height = Height,
|
||||
Seed = Seed,
|
||||
SmoothPasses = SmoothPasses,
|
||||
Population = Population,
|
||||
};
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.App;
|
||||
|
||||
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
|
||||
public sealed class PlantSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays { get; set; }
|
||||
|
||||
/// <summary>Текущая стадия роста.</summary>
|
||||
public int Stage { get; set; }
|
||||
|
||||
/// <summary>Плодородность клетки.</summary>
|
||||
public float CellFertility { get; set; }
|
||||
|
||||
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
|
||||
public sealed class PawnSave
|
||||
{
|
||||
/// <summary>Имя дефа жителя — по нему при загрузке берётся текстура из атласов.</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Масштаб трансформа (зависит от размера дефа и клетки).</summary>
|
||||
public float Scale { get; set; } = 1f;
|
||||
|
||||
/// <summary>Запас энергии [0, 1].</summary>
|
||||
public float Energy { get; set; }
|
||||
|
||||
/// <summary>Текущее действие (значение <see cref="Sim.PawnAction"/>).</summary>
|
||||
public int Action { get; set; }
|
||||
|
||||
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
||||
public float DecideIn { get; set; }
|
||||
|
||||
/// <summary>Направление блуждания X.</summary>
|
||||
public float DirX { get; set; }
|
||||
|
||||
/// <summary>Направление блуждания Y.</summary>
|
||||
public float DirY { get; set; }
|
||||
|
||||
/// <summary>Секунды до смены направления.</summary>
|
||||
public float ChangeIn { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемый хедиф особи (фаза A6): деф (по имени), тяжесть и набранный иммунитет.</summary>
|
||||
public sealed class HediffSave
|
||||
{
|
||||
/// <summary>Имя дефа хедифа (гон/болезнь).</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Текущая тяжесть.</summary>
|
||||
public float Severity { get; set; }
|
||||
|
||||
/// <summary>Набранный иммунитет [0,1].</summary>
|
||||
public float Immunity { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания.</summary>
|
||||
public sealed class ThoughtSave
|
||||
{
|
||||
/// <summary>Имя дефа мысли.</summary>
|
||||
public string DefName { get; set; } = "";
|
||||
|
||||
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||
public float RemainingDays { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сериализуемое состояние одного животного (фазы A1–A8): вид, позиция, геном (источник признаков и
|
||||
/// пола), поколение, стадия/возраст, нужды, здоровье (кровь, HP частей, хедифы), настроение и
|
||||
/// беременность. Спрайт пересобирается фабрикой из дефа. Чтобы популяция (эволюция/состояние)
|
||||
/// переживала сейв/лоад, а не пересыпалась заново из сида.
|
||||
/// </summary>
|
||||
public sealed class AnimalSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся индекс/стадии/текстуры/тело.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Номер поколения (для popstats/дрейфа).</summary>
|
||||
public int Generation { get; set; }
|
||||
|
||||
/// <summary>Текущая стадия роста.</summary>
|
||||
public int Stage { get; set; }
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays { get; set; }
|
||||
|
||||
/// <summary>Геном особи: аллели по id генов (переменного состава; несёт и пол через GeneSex).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
|
||||
/// <summary>Значения нужд по индексам NeedSet.</summary>
|
||||
public float[] Needs { get; set; } = [];
|
||||
|
||||
/// <summary>Накоплено дней истощения.</summary>
|
||||
public float Starve { get; set; }
|
||||
|
||||
/// <summary>Уровень крови [0,1].</summary>
|
||||
public float BloodLevel { get; set; } = 1f;
|
||||
|
||||
/// <summary>HP частей тела по порядку анатомии вида (пусто — у вида нет тела).</summary>
|
||||
public float[] PartHp { get; set; } = [];
|
||||
|
||||
/// <summary>Активные хедифы (гон/болезни).</summary>
|
||||
public List<HediffSave> Hediffs { get; set; } = [];
|
||||
|
||||
/// <summary>Текущее настроение [0,1].</summary>
|
||||
public float Mood { get; set; }
|
||||
|
||||
/// <summary>Активно ли настроение (тир 4+).</summary>
|
||||
public bool MoodActive { get; set; }
|
||||
|
||||
/// <summary>Активные событийные мысли.</summary>
|
||||
public List<ThoughtSave> Thoughts { get; set; } = [];
|
||||
|
||||
/// <summary>Беременна ли самка.</summary>
|
||||
public bool Pregnant { get; set; }
|
||||
|
||||
/// <summary>Геном отца (если беременна) — «слепок» на момент зачатия.</summary>
|
||||
public Dictionary<string, Allele> FatherGenome { get; set; } = new();
|
||||
|
||||
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||
public float DueInDays { get; set; }
|
||||
|
||||
/// <summary>Размер помёта.</summary>
|
||||
public int Litter { get; set; }
|
||||
|
||||
/// <summary>Уровни навыков [0..1] по индексам SkillSet.</summary>
|
||||
public float[] SkillLevels { get; set; } = [];
|
||||
|
||||
/// <summary>Страсть к навыкам (0/1/2) по индексам SkillSet.</summary>
|
||||
public int[] SkillPassions { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary>
|
||||
public sealed class CorpseSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся спрайт трупа.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||
public float RotDays { get; set; }
|
||||
|
||||
/// <summary>Размер тела (для масштаба спрайта).</summary>
|
||||
public float BodySize { get; set; }
|
||||
|
||||
/// <summary>Доступное мясо.</summary>
|
||||
public float Meat { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.</summary>
|
||||
public sealed class EggSave
|
||||
{
|
||||
/// <summary>Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.</summary>
|
||||
public string Species { get; set; } = "";
|
||||
|
||||
/// <summary>Позиция X в мировых координатах.</summary>
|
||||
public float X { get; set; }
|
||||
|
||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||
public float Y { get; set; }
|
||||
|
||||
/// <summary>Поколение будущего детёныша.</summary>
|
||||
public int Generation { get; set; }
|
||||
|
||||
/// <summary>Остаток инкубации в игровых днях.</summary>
|
||||
public float IncubateDays { get; set; }
|
||||
|
||||
/// <summary>Геном будущего детёныша (уже скрещён на момент кладки).</summary>
|
||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||
/// детерминированно регенерируется из сида при загрузке.
|
||||
/// </summary>
|
||||
public sealed class WorldSave
|
||||
{
|
||||
/// <summary>Имя мира (и основа имени файла).</summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>Ширина мира в клетках.</summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>Высота мира в клетках.</summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>Сид генерации.</summary>
|
||||
public int Seed { get; set; }
|
||||
|
||||
/// <summary>Число проходов сглаживания рельефа.</summary>
|
||||
public int SmoothPasses { get; set; } = 4;
|
||||
|
||||
/// <summary>Население на момент сохранения.</summary>
|
||||
public int Population { get; set; }
|
||||
|
||||
/// <summary>Момент сохранения (UTC).</summary>
|
||||
public DateTime SavedUtc { get; set; }
|
||||
|
||||
/// <summary>Прошедшее время симуляции в секундах.</summary>
|
||||
public double ElapsedSeconds { get; set; }
|
||||
|
||||
/// <summary>Снимок жителей.</summary>
|
||||
public List<PawnSave> Pawns { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||
public List<PlantSave> Plants { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность).</summary>
|
||||
public List<AnimalSave> Animals { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
|
||||
public List<CorpseSave> Corpses { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
|
||||
public List<EggSave> Eggs { get; set; } = [];
|
||||
|
||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||
public WorldConfig ToConfig() =>
|
||||
new()
|
||||
{
|
||||
Name = Name,
|
||||
Width = Width,
|
||||
Height = Height,
|
||||
Seed = Seed,
|
||||
SmoothPasses = SmoothPasses,
|
||||
Population = Population,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
|
||||
public sealed class SaveStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
|
||||
};
|
||||
|
||||
private readonly string _directory;
|
||||
|
||||
/// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
|
||||
public SaveStore(string? directory = null) =>
|
||||
_directory = directory ?? GamePaths.SavesDirectory;
|
||||
|
||||
/// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
|
||||
public readonly record struct Entry(string FileName, WorldSave Save);
|
||||
|
||||
/// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
|
||||
public IReadOnlyList<Entry> List()
|
||||
{
|
||||
if (!Directory.Exists(_directory))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var entries = new List<Entry>();
|
||||
foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var save = JsonSerializer.Deserialize<WorldSave>(
|
||||
File.ReadAllText(file),
|
||||
JsonOptions
|
||||
);
|
||||
if (save is not null)
|
||||
{
|
||||
entries.Add(new Entry(Path.GetFileName(file), save));
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or JsonException)
|
||||
{
|
||||
// Битый файл сохранения не должен ломать список — пропускаем.
|
||||
}
|
||||
}
|
||||
|
||||
entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
|
||||
public string Write(WorldSave save)
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
var fileName = Sanitize(save.Name) + ".json";
|
||||
File.WriteAllText(
|
||||
Path.Combine(_directory, fileName),
|
||||
JsonSerializer.Serialize(save, JsonOptions)
|
||||
);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>Удаляет файл сохранения по имени.</summary>
|
||||
public void Delete(string fileName)
|
||||
{
|
||||
var path = Path.Combine(_directory, fileName);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Sanitize(string name)
|
||||
{
|
||||
var trimmed = name.Trim();
|
||||
var safe = string.Concat(
|
||||
trimmed.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)
|
||||
);
|
||||
return string.IsNullOrEmpty(safe) ? "world" : safe;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Вид-животное (Defs/Animals/): подтип <see cref="PawnDef"/> (текстура/размер/вид) плюс геном
|
||||
/// вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе дефа —
|
||||
/// существующие 9 животных-<see cref="PawnDef"/> остаются валидными.
|
||||
/// </summary>
|
||||
public sealed class AnimalDef : PawnDef
|
||||
{
|
||||
/// <summary>Текстура взрослого самца (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/> (самка/общая).</summary>
|
||||
public string MaleTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура детёныша/подростка (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||
public string BabyTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||
public string CorpseTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
|
||||
public string EggTexture { get; init; } = "things/plant/seed_default";
|
||||
|
||||
/// <summary>Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³.</summary>
|
||||
public float BaseMassKg { get; init; } = 40f;
|
||||
|
||||
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
|
||||
public string Body { get; init; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
|
||||
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
|
||||
/// кода; разброс берётся из самого гена (<c>GeneDef.Spread</c>). Дискретные гены (напр. пол) сюда
|
||||
/// не пишутся — назначаются генерацией.
|
||||
/// </summary>
|
||||
public Dictionary<string, float> Genome { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Диета как ДАННЫЕ: что вид ест ("plant" — травоядное; "meat" — хищник/падальщик; оба — всеядное).
|
||||
/// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода.
|
||||
/// </summary>
|
||||
public string[] Diet { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Стадии роста (данные); пусто — встроенный набор Baby→Juvenile→Adult→Senior по умолчанию.
|
||||
/// </summary>
|
||||
public AnimalStageDef[] Stages { get; init; } = [];
|
||||
|
||||
/// <summary>Плотность стартового спавна: голов на 1000 клеток суши (0 — вид не спавнится сам).</summary>
|
||||
public float SpawnPer1000Cells { get; init; }
|
||||
|
||||
/// <summary>Базовая скорость передвижения (мировых единиц/сек при гене moveSpeed = 1).</summary>
|
||||
public float BaseSpeed { get; init; } = 28f;
|
||||
|
||||
/// <summary>Базовый радиус восприятия в клетках (× ген vision) — поиск корма/воды/партнёра.</summary>
|
||||
public float VisionCells { get; init; } = 14f;
|
||||
|
||||
/// <summary>Сила выедания: на сколько игровых дней роста убавляется растение за день кормёжки.</summary>
|
||||
public float ForageBiteDays { get; init; } = 40f;
|
||||
|
||||
/// <summary>Дальность атаки в клетках: на этом расстоянии хищник кусает добычу (контакт).</summary>
|
||||
public float AttackRangeCells { get; init; } = 1.2f;
|
||||
|
||||
/// <summary>Игровые секунды между ударами (кулдаун дискретной атаки, как в RimWorld).</summary>
|
||||
public float AttackIntervalSec { get; init; } = 1.8f;
|
||||
|
||||
/// <summary>Урон части тела добычи ЗА ОДИН УДАР (×силу: размер×навык). 0 — вид не атакует.</summary>
|
||||
public float AttackDamage { get; init; }
|
||||
|
||||
/// <summary>Прирост тяжести раны-кровотечения у добычи за один удар (×силу).</summary>
|
||||
public float AttackBleed { get; init; }
|
||||
|
||||
/// <summary>Острая кровопотеря добычи за один удар (доля объёма крови, ×силу) — основной путь к смерти.</summary>
|
||||
public float AttackBloodLoss { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Стадия роста животного как ДАННЫЕ (вложенный объект <see cref="AnimalDef.Stages"/>): возраст входа =
|
||||
/// <see cref="EnterAt"/> × якорь (<see cref="RelativeTo"/> — созревание или продолжительность жизни),
|
||||
/// масштаб тела и набор текстур ("baby"/"adult"). Стадии перечисляются по возрастанию возраста входа.
|
||||
/// Модер задаёт любой набор стадий (личинка/куколка/имаго и т.п.) без правки кода.
|
||||
/// </summary>
|
||||
public sealed class AnimalStageDef
|
||||
{
|
||||
/// <summary>Имя стадии (для тултипов/отладки).</summary>
|
||||
public string Name { get; init; } = "";
|
||||
|
||||
/// <summary>Множитель к якорю: возраст входа = EnterAt × (созревание | продолжительность жизни).</summary>
|
||||
public float EnterAt { get; init; }
|
||||
|
||||
/// <summary>Якорь возраста входа: "maturity" (созревание) или "lifespan" (продолжительность жизни).</summary>
|
||||
public string RelativeTo { get; init; } = "maturity";
|
||||
|
||||
/// <summary>Масштаб размера тела на этой стадии (× ген размера).</summary>
|
||||
public float Scale { get; init; } = 1f;
|
||||
|
||||
/// <summary>Набор текстур: "baby" (детёныш) или "adult" (по полу — самец/самка).</summary>
|
||||
public string Texture { get; init; } = "adult";
|
||||
|
||||
/// <summary>Взрослая стадия — способна размножаться.</summary>
|
||||
public bool Adult { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Анатомия вида (Defs/Bodies/): дерево частей тела/органов. Используется системой здоровья (фаза
|
||||
/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities).
|
||||
/// </summary>
|
||||
public sealed class BodyDef : Def
|
||||
{
|
||||
/// <summary>Части тела/органы вида.</summary>
|
||||
public BodyPartDef[] Parts { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
|
||||
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
|
||||
/// способности (capacity id → доля). Модер задаёт любую анатомию (8 ног, два сердца) без правки кода.
|
||||
/// </summary>
|
||||
public sealed class BodyPartDef
|
||||
{
|
||||
/// <summary>Имя части (уникально в теле); на него ссылается <see cref="Parent"/> детей.</summary>
|
||||
public string Name { get; init; } = "";
|
||||
|
||||
/// <summary>Имя родительской части ("" — корневая, напр. торс).</summary>
|
||||
public string Parent { get; init; } = "";
|
||||
|
||||
/// <summary>Вес попадания (доля тела) — для будущего выбора задетой части.</summary>
|
||||
public float Coverage { get; init; } = 0.1f;
|
||||
|
||||
/// <summary>Базовый максимум HP (× размер тела при создании особи).</summary>
|
||||
public float MaxHp { get; init; } = 10f;
|
||||
|
||||
/// <summary>Жизненно важная: уничтожение → смерть.</summary>
|
||||
public bool Vital { get; init; }
|
||||
|
||||
/// <summary>Вклады в способности: capacity id (см. <c>AnimalCapacities</c>) → доля (сумма по телу ≈ 1).</summary>
|
||||
public Dictionary<string, float> Capacities { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Пункт дев-инструмента (Defs/DevTools/) — ЧТОБЫ МОДЫ ДОБАВЛЯЛИ кнопки в дев-спавнер без правки кода.
|
||||
/// <see cref="Kind"/> — id обработчика из реестра (spawn-animal/spawn-plant/infect/damage/kill/…),
|
||||
/// <see cref="Target"/> — параметр (defName вида/хедифа). Виды животных/растений спавнер берёт из их
|
||||
/// дефов сам (модовый контент появляется автоматически); DevToolDef добавляет ПРОЧИЕ действия.
|
||||
/// </summary>
|
||||
public sealed class DevToolDef : Def
|
||||
{
|
||||
/// <summary>Категория для группировки в панели (например "tool").</summary>
|
||||
public string Category { get; init; } = "tool";
|
||||
|
||||
/// <summary>Id обработчика-инструмента из реестра кода (spawn-animal/spawn-plant/infect/damage/kill).</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
|
||||
/// <summary>Параметр инструмента: defName вида/хедифа и т.п. (по смыслу Kind).</summary>
|
||||
public string Target { get; init; } = "";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
|
||||
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
|
||||
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
|
||||
/// </summary>
|
||||
public sealed class GenomeDef
|
||||
{
|
||||
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
|
||||
public float OptimalLight { get; init; } = 0.7f;
|
||||
|
||||
/// <summary>Ширина толерантности по свету.</summary>
|
||||
public float LightTolerance { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
||||
public float OptimalTemperature { get; init; } = 18f;
|
||||
|
||||
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
|
||||
public float TemperatureTolerance { get; init; } = 12f;
|
||||
|
||||
/// <summary>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
|
||||
public float ColdHardiness { get; init; } = 8f;
|
||||
|
||||
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
|
||||
public float HeatHardiness { get; init; } = 10f;
|
||||
|
||||
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
||||
public float OptimalFertility { get; init; } = 1f;
|
||||
|
||||
/// <summary>Ширина толерантности по плодородности.</summary>
|
||||
public float FertilityTolerance { get; init; } = 0.8f;
|
||||
|
||||
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
|
||||
public float Vigor { get; init; } = 1f;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
||||
public float Lifespan { get; init; } = 80f;
|
||||
|
||||
/// <summary>Дальность расселения семян (в клетках).</summary>
|
||||
public float DispersalRange { get; init; } = 2f;
|
||||
|
||||
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
|
||||
public float ReproduceInterval { get; init; } = 8f;
|
||||
|
||||
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
|
||||
public float SelfPollination { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
|
||||
public float MutationRate { get; init; } = 0.05f;
|
||||
|
||||
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
|
||||
public float VariantChance { get; init; } = 0.15f;
|
||||
|
||||
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||
public float Spread { get; init; } = 0.08f;
|
||||
|
||||
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
|
||||
public float FruitYield { get; init; }
|
||||
|
||||
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
|
||||
public float FruitSeason { get; init; } = 1f;
|
||||
|
||||
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
|
||||
public float HarvestAmount { get; init; } = 1f;
|
||||
|
||||
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||
public float LeafHue { get; init; } = 0.33f;
|
||||
|
||||
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
|
||||
public float Toxicity { get; init; }
|
||||
|
||||
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
|
||||
public float Thorns { get; init; }
|
||||
|
||||
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||
public float Palatability { get; init; } = 1f;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние/хедиф организма (Defs/Hediffs/): гон, болезни, позже раны/возрастные эффекты. Фаза A6:
|
||||
/// болезнь прогрессирует (<see cref="SeverityPerDay"/>), параллельно растёт иммунитет
|
||||
/// (<see cref="ImmunityPerDay"/>) — гонка: иммунитет→1 раньше = выздоровление, severity→
|
||||
/// <see cref="LethalSeverity"/> раньше = смерть. Снижает способности (<see cref="CapMods"/>) и даёт боль.
|
||||
/// </summary>
|
||||
public sealed class HediffDef : Def
|
||||
{
|
||||
/// <summary>Начальная тяжесть при навешивании (гон — 1; болезнь — малая, напр. 0.05).</summary>
|
||||
public float InitialSeverity { get; init; } = 1f;
|
||||
|
||||
/// <summary>Прирост тяжести за игровой день (0 — статичный хедиф вроде гона).</summary>
|
||||
public float SeverityPerDay { get; init; }
|
||||
|
||||
/// <summary>Прирост иммунитета за день (0 — иммунитета нет, не выздоравливает сам).</summary>
|
||||
public float ImmunityPerDay { get; init; }
|
||||
|
||||
/// <summary>Тяжесть, при которой наступает смерть (0 — не летально).</summary>
|
||||
public float LethalSeverity { get; init; }
|
||||
|
||||
/// <summary>Боль при тяжести 1 (масштабируется тяжестью) — вход в сознание/настроение.</summary>
|
||||
public float Pain { get; init; }
|
||||
|
||||
/// <summary>Шанс подхватить фоном за день (0 — не фоновая болезнь).</summary>
|
||||
public float AmbientPerDay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Кровопотеря за игровой день при тяжести 1 (масштабируется тяжестью): доля объёма крови, которую
|
||||
/// рана теряет в сутки. 0 — хедиф не вызывает кровотечения. Кровь на нуле → смерть (см. система здоровья).
|
||||
/// </summary>
|
||||
public float BloodLossPerDay { get; init; }
|
||||
|
||||
/// <summary>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
|
||||
public Dictionary<string, float> CapMods { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Нужда (Defs/Needs/) как ДАННЫЕ: имя, тип (deplete — убывает, низкое = срочно; drive — растёт под
|
||||
/// условием, высокое = срочно), действие-исполнитель (по id, см. реестр поведений), темпы и порог
|
||||
/// интеллекта. Модер добавляет нужду виду/всем одной записью; если она ссылается на существующее
|
||||
/// действие — без правки кода. Хранилище нужд особи — динамическое (см. <c>NeedSet</c>/<c>AnimalNeeds</c>).
|
||||
/// </summary>
|
||||
public sealed class NeedDef : Def
|
||||
{
|
||||
/// <summary>Тип: "deplete" (убывает, утоляется действием) или "drive" (растёт под условием).</summary>
|
||||
public string Kind { get; init; } = "deplete";
|
||||
|
||||
/// <summary>Id действия-исполнителя, удовлетворяющего нужду (Eat/Drink/Sleep/Mate/…).</summary>
|
||||
public string Action { get; init; } = "";
|
||||
|
||||
/// <summary>Темп убывания за игровой день (drive: темп спада, когда условие отсутствует).</summary>
|
||||
public float DecayPerDay { get; init; } = 0.5f;
|
||||
|
||||
/// <summary>drive: темп роста за день, пока активно условие <see cref="RisesUnder"/>.</summary>
|
||||
public float RisePerDay { get; init; } = 1f;
|
||||
|
||||
/// <summary>deplete: насколько за день восполняет нужду исполнение действия.</summary>
|
||||
public float FeedPerDay { get; init; } = 4f;
|
||||
|
||||
/// <summary>drive: id хедифа, под которым нужда растёт (напр. "Rut"); null — растёт всегда.</summary>
|
||||
public string? RisesUnder { get; init; }
|
||||
|
||||
/// <summary>deplete: на нуле копит счётчик голодной смерти (A3).</summary>
|
||||
public bool Lethal { get; init; }
|
||||
|
||||
/// <summary>Порог размера мозга, с которого нужда активна (гейт интеллектом — фаза A7).</summary>
|
||||
public float MinBrain { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное). Не sealed — <see cref="AnimalDef"/> наследует.</summary>
|
||||
public class PawnDef : Def
|
||||
{
|
||||
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
||||
public const string KindBeing = "being";
|
||||
|
||||
/// <summary>Вид животного.</summary>
|
||||
public const string KindAnimal = "animal";
|
||||
|
||||
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
|
||||
public string Texture { get; init; } = "";
|
||||
|
||||
/// <summary>Размер спрайта в клетках.</summary>
|
||||
public float SizeCells { get; init; } = 1f;
|
||||
|
||||
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||
public sealed class PlantDef : Def
|
||||
{
|
||||
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
||||
public string Texture { get; init; } = "";
|
||||
|
||||
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
|
||||
public float SizeCells { get; init; } = 1f;
|
||||
|
||||
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
|
||||
public float TrunkRadiusCells { get; init; }
|
||||
|
||||
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
|
||||
public List<PlantGrowthStage> Stages { get; init; } = [];
|
||||
|
||||
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
|
||||
public GenomeDef Genome { get; init; } = new();
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
|
||||
public string? HarvestProduct { get; init; }
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||
public string? FruitProduct { get; init; }
|
||||
|
||||
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
|
||||
public float BaseMassKg { get; init; } = 20f;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Одна стадия роста растения (вложенный объект <see cref="PlantDef.Stages"/>): свой спрайт и
|
||||
/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
|
||||
/// Растёт на месте — последняя стадия терминальная (её <see cref="GrowDays"/> игнорируется).
|
||||
/// </summary>
|
||||
public sealed class PlantGrowthStage
|
||||
{
|
||||
/// <summary>Ключ текстуры стадии; null — берётся базовая <see cref="PlantDef.Texture"/>.</summary>
|
||||
public string? Texture { get; init; }
|
||||
|
||||
/// <summary>Размер спрайта в клетках на этой стадии; 0 — берётся базовый <see cref="PlantDef.SizeCells"/>.</summary>
|
||||
public float SizeCells { get; init; }
|
||||
|
||||
/// <summary>Сколько игровых дней расти на этой стадии до перехода к следующей.</summary>
|
||||
public float GrowDays { get; init; }
|
||||
|
||||
/// <summary>Ключ локализации названия стадии (опционально, для будущих тултипов).</summary>
|
||||
public string? Label { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||
/// </summary>
|
||||
public sealed class ProductDef : Def
|
||||
{
|
||||
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
|
||||
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
|
||||
public float MassKg { get; init; } = 1f;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>Запись скаттера: с вероятностью <see cref="Chance"/> на клетку ставится одно растение из <see cref="Options"/>.</summary>
|
||||
public sealed class ScatterEntry
|
||||
{
|
||||
/// <summary>Вероятность на клетку (0..1); записи бросаются независимо.</summary>
|
||||
public float Chance { get; init; }
|
||||
|
||||
/// <summary>Имена <see cref="PlantDef"/>, из которых выбирается равновероятно.</summary>
|
||||
public List<string> Options { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
|
||||
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
|
||||
/// код по его id (как у нужд/действий): добыча→выпас, охота→урон, уклонение→бегство. Модер добавляет
|
||||
/// навык строкой JSON; новый эффект — код. <see cref="Def.Label"/> — ключ локализации (skill.*).
|
||||
/// </summary>
|
||||
public sealed class SkillDef : Def
|
||||
{
|
||||
/// <summary>Категория (для группировки в UI), напр. "survival".</summary>
|
||||
public string Category { get; init; } = "";
|
||||
|
||||
/// <summary>Скорость обучения за игровой день непрерывной практики (при страсти ×1).</summary>
|
||||
public float LearnRate { get; init; } = 0.5f;
|
||||
|
||||
/// <summary>Угасание уровня за игровой день без практики.</summary>
|
||||
public float DecayPerDay { get; init; } = 0.02f;
|
||||
|
||||
/// <summary>Пол угасания (ниже не падает) — «не забывается совсем».</summary>
|
||||
public float FloorLevel { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
||||
public sealed class TerrainDef : Def
|
||||
{
|
||||
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
||||
public float MaxHeight { get; init; }
|
||||
|
||||
/// <summary>Цвет клетки [r, g, b] для сцен с тонированными квадратами и тайлов без поверхности.</summary>
|
||||
[JsonPropertyName("color")]
|
||||
public List<int> Rgb { get; init; } = [];
|
||||
|
||||
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
|
||||
public bool IsLand { get; init; }
|
||||
|
||||
/// <summary>Светонепроницаемый террейн — базовый окклюдер лайтмапа/теней (сейчас такого нет).</summary>
|
||||
public bool BlocksLight { get; init; }
|
||||
|
||||
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
|
||||
public string? Surface { get; init; }
|
||||
|
||||
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
|
||||
public List<ScatterEntry> Scatter { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
|
||||
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
|
||||
/// </summary>
|
||||
public float Fertility { get; init; } = 1f;
|
||||
|
||||
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
|
||||
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Мысль настроения (Defs/Thoughts/) как ДАННЫЕ (фаза A8, модель thoughts RimWorld): событие
|
||||
/// (роды/спаривание/утоление жажды/…) вешает на особь временный сдвиг настроения, затухающий за
|
||||
/// <see cref="DurationDays"/>. Триггеры событий — код (реестр <c>AnimalThoughts</c>), а величины/метка —
|
||||
/// данные: модер правит силу и длительность мысли одной записью JSON, без пересборки.
|
||||
/// </summary>
|
||||
public sealed class ThoughtDef : Def
|
||||
{
|
||||
/// <summary>Сдвиг настроения [-1,1] (положительный — радость, отрицательный — горе).</summary>
|
||||
public float MoodOffset { get; init; }
|
||||
|
||||
/// <summary>Сколько игровых дней мысль живёт, прежде чем угаснуть.</summary>
|
||||
public float DurationDays { get; init; } = 1f;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
|
||||
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
||||
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
||||
/// </summary>
|
||||
public sealed class WorldPresetDef : Def
|
||||
{
|
||||
/// <summary>Ширина мира в клетках.</summary>
|
||||
public int Width { get; init; }
|
||||
|
||||
/// <summary>Высота мира в клетках.</summary>
|
||||
public int Height { get; init; }
|
||||
|
||||
/// <summary>Число жителей при генерации.</summary>
|
||||
public int Population { get; init; }
|
||||
|
||||
/// <summary>Порядок в списке UI (по возрастанию).</summary>
|
||||
public int Order { get; init; }
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Mods;
|
||||
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>Запись скаттера: с вероятностью <see cref="Chance"/> на клетку ставится одно растение из <see cref="Options"/>.</summary>
|
||||
public sealed class ScatterEntry
|
||||
{
|
||||
/// <summary>Вероятность на клетку (0..1); записи бросаются независимо.</summary>
|
||||
public float Chance { get; init; }
|
||||
|
||||
/// <summary>Имена <see cref="PlantDef"/>, из которых выбирается равновероятно.</summary>
|
||||
public List<string> Options { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
||||
public sealed class TerrainDef : Def
|
||||
{
|
||||
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
||||
public float MaxHeight { get; init; }
|
||||
|
||||
/// <summary>Цвет клетки [r, g, b] для сцен с тонированными квадратами и тайлов без поверхности.</summary>
|
||||
[JsonPropertyName("color")]
|
||||
public List<int> Rgb { get; init; } = [];
|
||||
|
||||
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
|
||||
public bool IsLand { get; init; }
|
||||
|
||||
/// <summary>Светонепроницаемый террейн — базовый окклюдер лайтмапа/теней (сейчас такого нет).</summary>
|
||||
public bool BlocksLight { get; init; }
|
||||
|
||||
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
|
||||
public string? Surface { get; init; }
|
||||
|
||||
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
|
||||
public List<ScatterEntry> Scatter { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
|
||||
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
|
||||
/// </summary>
|
||||
public float Fertility { get; init; } = 1f;
|
||||
|
||||
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
|
||||
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Одна стадия роста растения (вложенный объект <see cref="PlantDef.Stages"/>): свой спрайт и
|
||||
/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
|
||||
/// Растёт на месте — последняя стадия терминальная (её <see cref="GrowDays"/> игнорируется).
|
||||
/// </summary>
|
||||
public sealed class PlantGrowthStage
|
||||
{
|
||||
/// <summary>Ключ текстуры стадии; null — берётся базовая <see cref="PlantDef.Texture"/>.</summary>
|
||||
public string? Texture { get; init; }
|
||||
|
||||
/// <summary>Размер спрайта в клетках на этой стадии; 0 — берётся базовый <see cref="PlantDef.SizeCells"/>.</summary>
|
||||
public float SizeCells { get; init; }
|
||||
|
||||
/// <summary>Сколько игровых дней расти на этой стадии до перехода к следующей.</summary>
|
||||
public float GrowDays { get; init; }
|
||||
|
||||
/// <summary>Ключ локализации названия стадии (опционально, для будущих тултипов).</summary>
|
||||
public string? Label { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||
/// </summary>
|
||||
public sealed class ProductDef : Def
|
||||
{
|
||||
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
|
||||
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
|
||||
public float MassKg { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
|
||||
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
|
||||
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
|
||||
/// </summary>
|
||||
public sealed class GenomeDef
|
||||
{
|
||||
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
|
||||
public float OptimalLight { get; init; } = 0.7f;
|
||||
|
||||
/// <summary>Ширина толерантности по свету.</summary>
|
||||
public float LightTolerance { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
||||
public float OptimalTemperature { get; init; } = 18f;
|
||||
|
||||
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
|
||||
public float TemperatureTolerance { get; init; } = 12f;
|
||||
|
||||
/// <summary>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
|
||||
public float ColdHardiness { get; init; } = 8f;
|
||||
|
||||
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
|
||||
public float HeatHardiness { get; init; } = 10f;
|
||||
|
||||
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
||||
public float OptimalFertility { get; init; } = 1f;
|
||||
|
||||
/// <summary>Ширина толерантности по плодородности.</summary>
|
||||
public float FertilityTolerance { get; init; } = 0.8f;
|
||||
|
||||
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
|
||||
public float Vigor { get; init; } = 1f;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
||||
public float Lifespan { get; init; } = 80f;
|
||||
|
||||
/// <summary>Дальность расселения семян (в клетках).</summary>
|
||||
public float DispersalRange { get; init; } = 2f;
|
||||
|
||||
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
|
||||
public float ReproduceInterval { get; init; } = 8f;
|
||||
|
||||
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
|
||||
public float SelfPollination { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
|
||||
public float MutationRate { get; init; } = 0.05f;
|
||||
|
||||
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
|
||||
public float VariantChance { get; init; } = 0.15f;
|
||||
|
||||
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||
public float Spread { get; init; } = 0.08f;
|
||||
|
||||
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
|
||||
public float FruitYield { get; init; }
|
||||
|
||||
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
|
||||
public float FruitSeason { get; init; } = 1f;
|
||||
|
||||
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
|
||||
public float HarvestAmount { get; init; } = 1f;
|
||||
|
||||
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||
public float LeafHue { get; init; } = 0.33f;
|
||||
|
||||
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
|
||||
public float Toxicity { get; init; }
|
||||
|
||||
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
|
||||
public float Thorns { get; init; }
|
||||
|
||||
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||
public float Palatability { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||
public sealed class PlantDef : Def
|
||||
{
|
||||
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
||||
public string Texture { get; init; } = "";
|
||||
|
||||
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
|
||||
public float SizeCells { get; init; } = 1f;
|
||||
|
||||
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
|
||||
public float TrunkRadiusCells { get; init; }
|
||||
|
||||
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
|
||||
public List<PlantGrowthStage> Stages { get; init; } = [];
|
||||
|
||||
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
|
||||
public GenomeDef Genome { get; init; } = new();
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
|
||||
public string? HarvestProduct { get; init; }
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||
public string? FruitProduct { get; init; }
|
||||
|
||||
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
|
||||
public float BaseMassKg { get; init; } = 20f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
|
||||
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
||||
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
||||
/// </summary>
|
||||
public sealed class WorldPresetDef : Def
|
||||
{
|
||||
/// <summary>Ширина мира в клетках.</summary>
|
||||
public int Width { get; init; }
|
||||
|
||||
/// <summary>Высота мира в клетках.</summary>
|
||||
public int Height { get; init; }
|
||||
|
||||
/// <summary>Число жителей при генерации.</summary>
|
||||
public int Population { get; init; }
|
||||
|
||||
/// <summary>Порядок в списке UI (по возрастанию).</summary>
|
||||
public int Order { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное). Не sealed — <see cref="AnimalDef"/> наследует.</summary>
|
||||
public class PawnDef : Def
|
||||
{
|
||||
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
||||
public const string KindBeing = "being";
|
||||
|
||||
/// <summary>Вид животного.</summary>
|
||||
public const string KindAnimal = "animal";
|
||||
|
||||
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
|
||||
public string Texture { get; init; } = "";
|
||||
|
||||
/// <summary>Размер спрайта в клетках.</summary>
|
||||
public float SizeCells { get; init; } = 1f;
|
||||
|
||||
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Стадия роста животного как ДАННЫЕ (вложенный объект <see cref="AnimalDef.Stages"/>): возраст входа =
|
||||
/// <see cref="EnterAt"/> × якорь (<see cref="RelativeTo"/> — созревание или продолжительность жизни),
|
||||
/// масштаб тела и набор текстур ("baby"/"adult"). Стадии перечисляются по возрастанию возраста входа.
|
||||
/// Модер задаёт любой набор стадий (личинка/куколка/имаго и т.п.) без правки кода.
|
||||
/// </summary>
|
||||
public sealed class AnimalStageDef
|
||||
{
|
||||
/// <summary>Имя стадии (для тултипов/отладки).</summary>
|
||||
public string Name { get; init; } = "";
|
||||
|
||||
/// <summary>Множитель к якорю: возраст входа = EnterAt × (созревание | продолжительность жизни).</summary>
|
||||
public float EnterAt { get; init; }
|
||||
|
||||
/// <summary>Якорь возраста входа: "maturity" (созревание) или "lifespan" (продолжительность жизни).</summary>
|
||||
public string RelativeTo { get; init; } = "maturity";
|
||||
|
||||
/// <summary>Масштаб размера тела на этой стадии (× ген размера).</summary>
|
||||
public float Scale { get; init; } = 1f;
|
||||
|
||||
/// <summary>Набор текстур: "baby" (детёныш) или "adult" (по полу — самец/самка).</summary>
|
||||
public string Texture { get; init; } = "adult";
|
||||
|
||||
/// <summary>Взрослая стадия — способна размножаться.</summary>
|
||||
public bool Adult { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вид-животное (Defs/Animals/): подтип <see cref="PawnDef"/> (текстура/размер/вид) плюс геном
|
||||
/// вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе дефа —
|
||||
/// существующие 9 животных-<see cref="PawnDef"/> остаются валидными.
|
||||
/// </summary>
|
||||
public sealed class AnimalDef : PawnDef
|
||||
{
|
||||
/// <summary>Текстура взрослого самца (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/> (самка/общая).</summary>
|
||||
public string MaleTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура детёныша/подростка (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||
public string BabyTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||
public string CorpseTexture { get; init; } = "";
|
||||
|
||||
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
|
||||
public string EggTexture { get; init; } = "things/plant/seed_default";
|
||||
|
||||
/// <summary>Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³.</summary>
|
||||
public float BaseMassKg { get; init; } = 40f;
|
||||
|
||||
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
|
||||
public string Body { get; init; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
|
||||
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
|
||||
/// кода; разброс берётся из самого гена (<c>GeneDef.Spread</c>). Дискретные гены (напр. пол) сюда
|
||||
/// не пишутся — назначаются генерацией.
|
||||
/// </summary>
|
||||
public Dictionary<string, float> Genome { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Диета как ДАННЫЕ: что вид ест ("plant" — травоядное; "meat" — хищник/падальщик; оба — всеядное).
|
||||
/// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода.
|
||||
/// </summary>
|
||||
public string[] Diet { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Стадии роста (данные); пусто — встроенный набор Baby→Juvenile→Adult→Senior по умолчанию.
|
||||
/// </summary>
|
||||
public AnimalStageDef[] Stages { get; init; } = [];
|
||||
|
||||
/// <summary>Плотность стартового спавна: голов на 1000 клеток суши (0 — вид не спавнится сам).</summary>
|
||||
public float SpawnPer1000Cells { get; init; }
|
||||
|
||||
/// <summary>Базовая скорость передвижения (мировых единиц/сек при гене moveSpeed = 1).</summary>
|
||||
public float BaseSpeed { get; init; } = 28f;
|
||||
|
||||
/// <summary>Базовый радиус восприятия в клетках (× ген vision) — поиск корма/воды/партнёра.</summary>
|
||||
public float VisionCells { get; init; } = 14f;
|
||||
|
||||
/// <summary>Сила выедания: на сколько игровых дней роста убавляется растение за день кормёжки.</summary>
|
||||
public float ForageBiteDays { get; init; } = 40f;
|
||||
|
||||
/// <summary>Дальность атаки в клетках: на этом расстоянии хищник кусает добычу (контакт).</summary>
|
||||
public float AttackRangeCells { get; init; } = 1.2f;
|
||||
|
||||
/// <summary>Урон части тела добычи за игровой день укусов (0 — вид не атакует, не хищник по бою).</summary>
|
||||
public float AttackDamage { get; init; }
|
||||
|
||||
/// <summary>Прирост тяжести раны-кровотечения у добычи за игровой день укусов.</summary>
|
||||
public float AttackBleed { get; init; }
|
||||
|
||||
/// <summary>Острая кровопотеря добычи за игровой день укусов (доля объёма крови) — основной путь к смерти.</summary>
|
||||
public float AttackBloodLoss { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Нужда (Defs/Needs/) как ДАННЫЕ: имя, тип (deplete — убывает, низкое = срочно; drive — растёт под
|
||||
/// условием, высокое = срочно), действие-исполнитель (по id, см. реестр поведений), темпы и порог
|
||||
/// интеллекта. Модер добавляет нужду виду/всем одной записью; если она ссылается на существующее
|
||||
/// действие — без правки кода. Хранилище нужд особи — динамическое (см. <c>NeedSet</c>/<c>AnimalNeeds</c>).
|
||||
/// </summary>
|
||||
public sealed class NeedDef : Def
|
||||
{
|
||||
/// <summary>Тип: "deplete" (убывает, утоляется действием) или "drive" (растёт под условием).</summary>
|
||||
public string Kind { get; init; } = "deplete";
|
||||
|
||||
/// <summary>Id действия-исполнителя, удовлетворяющего нужду (Eat/Drink/Sleep/Mate/…).</summary>
|
||||
public string Action { get; init; } = "";
|
||||
|
||||
/// <summary>Темп убывания за игровой день (drive: темп спада, когда условие отсутствует).</summary>
|
||||
public float DecayPerDay { get; init; } = 0.5f;
|
||||
|
||||
/// <summary>drive: темп роста за день, пока активно условие <see cref="RisesUnder"/>.</summary>
|
||||
public float RisePerDay { get; init; } = 1f;
|
||||
|
||||
/// <summary>deplete: насколько за день восполняет нужду исполнение действия.</summary>
|
||||
public float FeedPerDay { get; init; } = 4f;
|
||||
|
||||
/// <summary>drive: id хедифа, под которым нужда растёт (напр. "Rut"); null — растёт всегда.</summary>
|
||||
public string? RisesUnder { get; init; }
|
||||
|
||||
/// <summary>deplete: на нуле копит счётчик голодной смерти (A3).</summary>
|
||||
public bool Lethal { get; init; }
|
||||
|
||||
/// <summary>Порог размера мозга, с которого нужда активна (гейт интеллектом — фаза A7).</summary>
|
||||
public float MinBrain { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Мысль настроения (Defs/Thoughts/) как ДАННЫЕ (фаза A8, модель thoughts RimWorld): событие
|
||||
/// (роды/спаривание/утоление жажды/…) вешает на особь временный сдвиг настроения, затухающий за
|
||||
/// <see cref="DurationDays"/>. Триггеры событий — код (реестр <c>AnimalThoughts</c>), а величины/метка —
|
||||
/// данные: модер правит силу и длительность мысли одной записью JSON, без пересборки.
|
||||
/// </summary>
|
||||
public sealed class ThoughtDef : Def
|
||||
{
|
||||
/// <summary>Сдвиг настроения [-1,1] (положительный — радость, отрицательный — горе).</summary>
|
||||
public float MoodOffset { get; init; }
|
||||
|
||||
/// <summary>Сколько игровых дней мысль живёт, прежде чем угаснуть.</summary>
|
||||
public float DurationDays { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пункт дев-инструмента (Defs/DevTools/) — ЧТОБЫ МОДЫ ДОБАВЛЯЛИ кнопки в дев-спавнер без правки кода.
|
||||
/// <see cref="Kind"/> — id обработчика из реестра (spawn-animal/spawn-plant/infect/damage/kill/…),
|
||||
/// <see cref="Target"/> — параметр (defName вида/хедифа). Виды животных/растений спавнер берёт из их
|
||||
/// дефов сам (модовый контент появляется автоматически); DevToolDef добавляет ПРОЧИЕ действия.
|
||||
/// </summary>
|
||||
public sealed class DevToolDef : Def
|
||||
{
|
||||
/// <summary>Категория для группировки в панели (например "tool").</summary>
|
||||
public string Category { get; init; } = "tool";
|
||||
|
||||
/// <summary>Id обработчика-инструмента из реестра кода (spawn-animal/spawn-plant/infect/damage/kill).</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
|
||||
/// <summary>Параметр инструмента: defName вида/хедифа и т.п. (по смыслу Kind).</summary>
|
||||
public string Target { get; init; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
|
||||
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
|
||||
/// код по его id (как у нужд/действий): добыча→выпас, охота→урон, уклонение→бегство. Модер добавляет
|
||||
/// навык строкой JSON; новый эффект — код. <see cref="Def.Label"/> — ключ локализации (skill.*).
|
||||
/// </summary>
|
||||
public sealed class SkillDef : Def
|
||||
{
|
||||
/// <summary>Категория (для группировки в UI), напр. "survival".</summary>
|
||||
public string Category { get; init; } = "";
|
||||
|
||||
/// <summary>Скорость обучения за игровой день непрерывной практики (при страсти ×1).</summary>
|
||||
public float LearnRate { get; init; } = 0.5f;
|
||||
|
||||
/// <summary>Угасание уровня за игровой день без практики.</summary>
|
||||
public float DecayPerDay { get; init; } = 0.02f;
|
||||
|
||||
/// <summary>Пол угасания (ниже не падает) — «не забывается совсем».</summary>
|
||||
public float FloorLevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
|
||||
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
|
||||
/// способности (capacity id → доля). Модер задаёт любую анатомию (8 ног, два сердца) без правки кода.
|
||||
/// </summary>
|
||||
public sealed class BodyPartDef
|
||||
{
|
||||
/// <summary>Имя части (уникально в теле); на него ссылается <see cref="Parent"/> детей.</summary>
|
||||
public string Name { get; init; } = "";
|
||||
|
||||
/// <summary>Имя родительской части ("" — корневая, напр. торс).</summary>
|
||||
public string Parent { get; init; } = "";
|
||||
|
||||
/// <summary>Вес попадания (доля тела) — для будущего выбора задетой части.</summary>
|
||||
public float Coverage { get; init; } = 0.1f;
|
||||
|
||||
/// <summary>Базовый максимум HP (× размер тела при создании особи).</summary>
|
||||
public float MaxHp { get; init; } = 10f;
|
||||
|
||||
/// <summary>Жизненно важная: уничтожение → смерть.</summary>
|
||||
public bool Vital { get; init; }
|
||||
|
||||
/// <summary>Вклады в способности: capacity id (см. <c>AnimalCapacities</c>) → доля (сумма по телу ≈ 1).</summary>
|
||||
public Dictionary<string, float> Capacities { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Анатомия вида (Defs/Bodies/): дерево частей тела/органов. Используется системой здоровья (фаза
|
||||
/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities).
|
||||
/// </summary>
|
||||
public sealed class BodyDef : Def
|
||||
{
|
||||
/// <summary>Части тела/органы вида.</summary>
|
||||
public BodyPartDef[] Parts { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Состояние/хедиф организма (Defs/Hediffs/): гон, болезни, позже раны/возрастные эффекты. Фаза A6:
|
||||
/// болезнь прогрессирует (<see cref="SeverityPerDay"/>), параллельно растёт иммунитет
|
||||
/// (<see cref="ImmunityPerDay"/>) — гонка: иммунитет→1 раньше = выздоровление, severity→
|
||||
/// <see cref="LethalSeverity"/> раньше = смерть. Снижает способности (<see cref="CapMods"/>) и даёт боль.
|
||||
/// </summary>
|
||||
public sealed class HediffDef : Def
|
||||
{
|
||||
/// <summary>Начальная тяжесть при навешивании (гон — 1; болезнь — малая, напр. 0.05).</summary>
|
||||
public float InitialSeverity { get; init; } = 1f;
|
||||
|
||||
/// <summary>Прирост тяжести за игровой день (0 — статичный хедиф вроде гона).</summary>
|
||||
public float SeverityPerDay { get; init; }
|
||||
|
||||
/// <summary>Прирост иммунитета за день (0 — иммунитета нет, не выздоравливает сам).</summary>
|
||||
public float ImmunityPerDay { get; init; }
|
||||
|
||||
/// <summary>Тяжесть, при которой наступает смерть (0 — не летально).</summary>
|
||||
public float LethalSeverity { get; init; }
|
||||
|
||||
/// <summary>Боль при тяжести 1 (масштабируется тяжестью) — вход в сознание/настроение.</summary>
|
||||
public float Pain { get; init; }
|
||||
|
||||
/// <summary>Шанс подхватить фоном за день (0 — не фоновая болезнь).</summary>
|
||||
public float AmbientPerDay { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Кровопотеря за игровой день при тяжести 1 (масштабируется тяжестью): доля объёма крови, которую
|
||||
/// рана теряет в сутки. 0 — хедиф не вызывает кровотечения. Кровь на нуле → смерть (см. система здоровья).
|
||||
/// </summary>
|
||||
public float BloodLossPerDay { get; init; }
|
||||
|
||||
/// <summary>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
|
||||
public Dictionary<string, float> CapMods { get; init; } = new();
|
||||
}
|
||||
@@ -9,14 +9,17 @@ namespace LittleSim.Content;
|
||||
public sealed class NeedSet
|
||||
{
|
||||
private readonly NeedDef[] _defs;
|
||||
private readonly bool[] _isDrive; // предрасчёт типа нужды — горячие циклы не сравнивают строки на тик
|
||||
private readonly Dictionary<string, int> _byAction = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Строит реестр из всех <see cref="NeedDef"/> мода (порядок = индексы нужд).</summary>
|
||||
public NeedSet(GameContent content)
|
||||
{
|
||||
_defs = content.Defs.All<NeedDef>().ToArray();
|
||||
_isDrive = new bool[_defs.Length];
|
||||
for (var i = 0; i < _defs.Length; i++)
|
||||
{
|
||||
_isDrive[i] = string.Equals(_defs[i].Kind, "drive", StringComparison.Ordinal);
|
||||
if (!string.IsNullOrEmpty(_defs[i].Action))
|
||||
{
|
||||
_byAction[_defs[i].Action] = i;
|
||||
@@ -31,7 +34,7 @@ public sealed class NeedSet
|
||||
public NeedDef this[int index] => _defs[index];
|
||||
|
||||
/// <summary>Нужда-влечение (растёт под условием), а не убывающая.</summary>
|
||||
public bool IsDrive(int index) => string.Equals(_defs[index].Kind, "drive", StringComparison.Ordinal);
|
||||
public bool IsDrive(int index) => _isDrive[index];
|
||||
|
||||
/// <summary>Убывающая нужда (голод/жажда/сон) — низкое значение срочно.</summary>
|
||||
public bool IsDeplete(int index) => !IsDrive(index);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
|
||||
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
|
||||
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
|
||||
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
|
||||
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
|
||||
/// </summary>
|
||||
public struct NetLerp : IComponent
|
||||
{
|
||||
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
|
||||
public Vector2 Visual;
|
||||
|
||||
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
|
||||
public Vector2 Target;
|
||||
|
||||
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
|
||||
public bool Initialized;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using LittleSim.Sim;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Net;
|
||||
@@ -17,7 +18,36 @@ public static class NetSchema
|
||||
/// <summary>Порт сервера по умолчанию.</summary>
|
||||
public const int DefaultPort = 9050;
|
||||
|
||||
// Задокументированный wire-размер реплицируемых структур (см. NetContract.cs веб-зеркала). Репликация
|
||||
// блитит сырые байты структуры, поэтому добавленное/переставленное поле молча испортит десериализацию —
|
||||
// проверяем размеры при построении схемы, чтобы дрейф упал на старте, а не выдал битый мир.
|
||||
private const int TransformWireSize = 20; // Vector2(8) + float(4) + Vector2(8)
|
||||
private const int PawnNeedsWireSize = 4; // float(4)
|
||||
|
||||
/// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary>
|
||||
public static ReplicationSchema Create() =>
|
||||
new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
|
||||
public static ReplicationSchema Create()
|
||||
{
|
||||
VerifyLayout();
|
||||
return new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
|
||||
}
|
||||
|
||||
/// <summary>Сверяет фактическую байт-раскладку реплицируемых структур с контрактом (см. NetContract.cs).</summary>
|
||||
private static void VerifyLayout()
|
||||
{
|
||||
Require<Transform2D>(TransformWireSize);
|
||||
Require<PawnNeeds>(PawnNeedsWireSize);
|
||||
|
||||
static void Require<T>(int expected)
|
||||
where T : struct
|
||||
{
|
||||
var actual = Unsafe.SizeOf<T>();
|
||||
if (actual != expected)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Размер реплицируемого {typeof(T).Name} = {actual} Б, ожидался {expected} Б: "
|
||||
+ "wire-контракт изменился — синхронизируй NetSchema и LittleSim.Web/NetContract.cs."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,25 +5,6 @@ using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
|
||||
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
|
||||
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
|
||||
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
|
||||
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
|
||||
/// </summary>
|
||||
public struct NetLerp : IComponent
|
||||
{
|
||||
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
|
||||
public Vector2 Visual;
|
||||
|
||||
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
|
||||
public Vector2 Target;
|
||||
|
||||
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
|
||||
public bool Initialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
||||
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
||||
@@ -53,7 +53,7 @@ public sealed class BootScene : Scene
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
desktop.Root = Ui.Screen(_label);
|
||||
desktop.Root = Ui.MenuScreen(_label);
|
||||
|
||||
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
|
||||
_load = Task.Run(() => GameContent.Load());
|
||||
|
||||
@@ -2,58 +2,14 @@ using Friflo.Engine.ECS;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Lighting;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
||||
internal static class ScatterSpawner
|
||||
{
|
||||
/// <summary>
|
||||
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
||||
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
||||
/// разновозрастный. Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением
|
||||
/// управляет <see cref="PlantGrowthSystem"/>.
|
||||
/// </summary>
|
||||
public static void Spawn(
|
||||
Scene scene,
|
||||
GameContent content,
|
||||
PlantSet plants,
|
||||
TerrainDef terrain,
|
||||
Point cell,
|
||||
int cellSize,
|
||||
Random random
|
||||
)
|
||||
{
|
||||
foreach (var entry in terrain.Scatter)
|
||||
{
|
||||
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
||||
var index = plants.IndexOf(def);
|
||||
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||
var age = random.NextSingle() * plants[index].MaturityDays;
|
||||
|
||||
PlantFactory.Create(
|
||||
scene.Store,
|
||||
plants,
|
||||
index,
|
||||
new Vector2(px, py),
|
||||
age,
|
||||
plants[index].Template.Generate(random),
|
||||
terrain.Fertility,
|
||||
cellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary>
|
||||
internal static class ContentCommands
|
||||
{
|
||||
@@ -17,15 +17,15 @@ public sealed class CreditsScene : Scene
|
||||
var input = this.UseInput();
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
|
||||
var column = Ui.Column(12);
|
||||
var column = Ui.Card(10);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
||||
column.Widgets.Add(Ui.AccentRule());
|
||||
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
|
||||
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
|
||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 12 });
|
||||
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
|
||||
|
||||
desktop.Root = Ui.Screen(column);
|
||||
desktop.Root = Ui.MenuScreen(column);
|
||||
UpdateSystems.Add(
|
||||
new CallbackSystem(() =>
|
||||
{
|
||||
|
||||
@@ -41,9 +41,9 @@ public sealed class LoadGameScene : Scene
|
||||
private void Rebuild()
|
||||
{
|
||||
var lang = _content.Languages;
|
||||
var column = Ui.Column(8);
|
||||
var column = Ui.Card(8);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("load.title")));
|
||||
column.Widgets.Add(new Label { Height = 8 });
|
||||
column.Widgets.Add(Ui.AccentRule());
|
||||
|
||||
var saves = _store.List();
|
||||
if (saves.Count == 0)
|
||||
@@ -98,7 +98,7 @@ public sealed class LoadGameScene : Scene
|
||||
column.Widgets.Add(new Label { Height = 8 });
|
||||
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
|
||||
|
||||
_desktop.Root = Ui.Screen(column);
|
||||
_desktop.Root = Ui.MenuScreen(column);
|
||||
}
|
||||
|
||||
private void Back()
|
||||
|
||||
@@ -19,19 +19,19 @@ public sealed class MainMenuScene : Scene
|
||||
var lang = content.Languages;
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
|
||||
var column = Ui.Column(12);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
||||
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
||||
column.Widgets.Add(
|
||||
var card = Ui.Card(8);
|
||||
card.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||
card.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
||||
card.Widgets.Add(Ui.AccentRule());
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
||||
card.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
||||
card.Widgets.Add(
|
||||
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
|
||||
);
|
||||
|
||||
desktop.Root = Ui.Screen(column);
|
||||
desktop.Root = Ui.MenuScreen(card);
|
||||
}
|
||||
|
||||
private void Go(Scene scene)
|
||||
|
||||
@@ -38,9 +38,9 @@ public sealed class NewWorldScene : Scene
|
||||
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
||||
_preset = presets[0];
|
||||
|
||||
var column = Ui.Column(10);
|
||||
var column = Ui.Card(10);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
|
||||
column.Widgets.Add(new Label { Height = 8 });
|
||||
column.Widgets.Add(Ui.AccentRule());
|
||||
|
||||
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
|
||||
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
|
||||
@@ -106,7 +106,7 @@ public sealed class NewWorldScene : Scene
|
||||
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
|
||||
column.Widgets.Add(buttons);
|
||||
|
||||
desktop.Root = Ui.Screen(column);
|
||||
desktop.Root = Ui.MenuScreen(column);
|
||||
Refresh();
|
||||
|
||||
UpdateSystems.Add(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Lighting;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
||||
internal static class ScatterSpawner
|
||||
{
|
||||
// Ниже этой световой пригодности (при полном дневном свете клетки) растение не сеем: теневому виду
|
||||
// (грибы, optimalLight≈0.15) в открытой клетке не место — без полога крон ему тут не выжить, поэтому
|
||||
// он и не должен появляться, пока такой ниши нет. Солнечные виды (≥0.7 пригодности) проходят свободно.
|
||||
private const float ShadeSpawnThreshold = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
||||
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
||||
/// разновозрастный. Теневые виды не сеются в ярко освещённые клетки (см. <see cref="ShadeSpawnThreshold"/>).
|
||||
/// Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением управляет <see cref="PlantGrowthSystem"/>.
|
||||
/// </summary>
|
||||
public static void Spawn(
|
||||
Scene scene,
|
||||
GameContent content,
|
||||
PlantSet plants,
|
||||
TerrainDef terrain,
|
||||
Point cell,
|
||||
int cellSize,
|
||||
Random random
|
||||
)
|
||||
{
|
||||
// Статичная освещённость клетки в полдень: затенена только светонепроницаемым террейном (полог
|
||||
// крон сейчас не учитывается). По ней решаем, годится ли клетка теневому виду.
|
||||
var exposure = terrain.BlocksLight ? LightmapBuilder.OccluderShade : 1f;
|
||||
|
||||
foreach (var entry in terrain.Scatter)
|
||||
{
|
||||
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
||||
if (
|
||||
Suitability.Gaussian(exposure, def.Genome.OptimalLight, def.Genome.LightTolerance)
|
||||
< ShadeSpawnThreshold
|
||||
)
|
||||
{
|
||||
continue; // теневому виду в ярко освещённой клетке не место
|
||||
}
|
||||
|
||||
var index = plants.IndexOf(def);
|
||||
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||
var age = random.NextSingle() * plants[index].MaturityDays;
|
||||
|
||||
PlantFactory.Create(
|
||||
scene.Store,
|
||||
plants,
|
||||
index,
|
||||
new Vector2(px, py),
|
||||
age,
|
||||
plants[index].Template.Generate(random),
|
||||
terrain.Fertility,
|
||||
cellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,9 @@ public sealed class SettingsScene : Scene
|
||||
}
|
||||
|
||||
private void Rebuild() =>
|
||||
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back));
|
||||
_desktop.Root = Ui.MenuScreen(
|
||||
SettingsPanel.Build(_settings, _content.Languages, Apply, Back)
|
||||
);
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class WorldLoadingScene : Scene
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
desktop.Root = Ui.Screen(_label);
|
||||
desktop.Root = Ui.MenuScreen(_label);
|
||||
UpdateSystems.Add(new CallbackSystem(Tick));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,669 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.App;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using LittleSim.UI;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Assets;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Formulas;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Host;
|
||||
using MrGameEng.Input;
|
||||
using MrGameEng.Inspector;
|
||||
using MrGameEng.Lighting;
|
||||
using MrGameEng.Tilemaps;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D;
|
||||
using Myra.Graphics2D.Brushes;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
// Часть WorldScene: регистрация и обработчики команд дев-консоли.
|
||||
public sealed partial class WorldScene
|
||||
{
|
||||
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
|
||||
{
|
||||
ContentCommands.Register(console, content, atlases);
|
||||
console.Register(
|
||||
"regen",
|
||||
"regen [seed] — regenerate the world with a new seed",
|
||||
(c, args) =>
|
||||
{
|
||||
if (Context.Scenes.IsTransitioning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||
c.WriteLine($"regenerating world, seed {seed}");
|
||||
Context.Scenes.Switch(
|
||||
new WorldScene(_config with { Seed = seed }),
|
||||
Transitions.Fade(0.6f)
|
||||
);
|
||||
}
|
||||
);
|
||||
console.Register(
|
||||
"formula",
|
||||
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
|
||||
(c, args) =>
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
c.WriteLine(
|
||||
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var expression = string.Join(' ', args);
|
||||
try
|
||||
{
|
||||
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
|
||||
}
|
||||
catch (FormulaException e)
|
||||
{
|
||||
c.WriteLine($"error: {e.Message}");
|
||||
}
|
||||
}
|
||||
);
|
||||
console.Register(
|
||||
"gene",
|
||||
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
|
||||
(c, args) => RunGeneDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"plant",
|
||||
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
||||
(c, args) => RunPlantDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"animal",
|
||||
"animal <species> [seed] — sample an animal genome and show its gene-driven traits",
|
||||
(c, args) => RunAnimalDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"diet",
|
||||
"diet [species] — gene-derived diet (herbivory/carnivory/omnivory), role and hunting stats",
|
||||
(c, args) => RunDiet(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"popstats",
|
||||
"popstats [species] — live population trait means and generation span (selection drift)",
|
||||
(c, args) => RunPopStats(c, args)
|
||||
);
|
||||
console.Register(
|
||||
"body",
|
||||
"body <species> — dump a species body parts and a healthy individual's capacities",
|
||||
(c, args) => RunBodyDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"infect",
|
||||
"infect [hediff] — give every animal a disease (default Fever) to test the health system",
|
||||
(c, args) => RunInfect(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"intel",
|
||||
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
|
||||
(c, args) => RunIntel(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"mood",
|
||||
"mood [species] — live population mood (avg/min/max) and content/stressed counts",
|
||||
(c, args) => RunMood(c, args)
|
||||
);
|
||||
console.Register(
|
||||
"skills",
|
||||
"skills — mean skill levels across living animals (grow from practice, decay unused)",
|
||||
(c, _) => RunSkills(c)
|
||||
);
|
||||
console.Register(
|
||||
"menu",
|
||||
"menu — return to the main menu",
|
||||
(_, _) => Switch(new MainMenuScene())
|
||||
);
|
||||
}
|
||||
|
||||
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
|
||||
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
|
||||
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var genes = content.Defs.All<GeneDef>();
|
||||
if (genes.Count == 0)
|
||||
{
|
||||
console.WriteLine("no Gene defs loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||
var random = new Random(seed);
|
||||
|
||||
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
|
||||
var parentA = Genome.Generate(genes, random);
|
||||
foreach (var gene in genes)
|
||||
{
|
||||
var allele = parentA[gene.DefName];
|
||||
console.WriteLine(
|
||||
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
|
||||
);
|
||||
}
|
||||
|
||||
console.WriteLine("traits:");
|
||||
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
|
||||
{
|
||||
console.WriteLine($" {trait} = {value:0.###}");
|
||||
}
|
||||
|
||||
var parentB = Genome.Generate(genes, random);
|
||||
var child = Genome.Breed(parentA, parentB, registry, random);
|
||||
var childTraits = Phenotype.Compute(child, registry);
|
||||
console.WriteLine(
|
||||
$"bred child: {child.Alleles.Count} genes, "
|
||||
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
|
||||
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
|
||||
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
|
||||
);
|
||||
}
|
||||
|
||||
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
|
||||
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
|
||||
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Plant");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _plants.IndexOf(def);
|
||||
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||
var genome = _plants[index].Template.Generate(new Random(seed));
|
||||
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
|
||||
|
||||
console.WriteLine(
|
||||
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
||||
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
||||
);
|
||||
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||
console.WriteLine(
|
||||
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
|
||||
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, "
|
||||
+ $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})"
|
||||
);
|
||||
console.WriteLine($" mass (mature): {def.BaseMassKg:0.##} kg");
|
||||
|
||||
if (def.HarvestProduct is { } harvest)
|
||||
{
|
||||
console.WriteLine(
|
||||
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
|
||||
);
|
||||
}
|
||||
|
||||
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||||
{
|
||||
var season = content.Languages.Get(
|
||||
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.WriteLine(" barren (no fruit)");
|
||||
}
|
||||
}
|
||||
|
||||
// Демонстрация животного (фаза A1): по виду берём его набор генов, генерируем особь и печатаем
|
||||
// признаки, вычисленные формулами генов (тело/мозг/метаболизм/чувства/мех).
|
||||
private void RunAnimalDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine("usage: animal <species> [seed] e.g. animal Deer");
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _animals.IndexOf(def);
|
||||
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||
var genome = _animals.GenerateGenome(index, new Random(seed));
|
||||
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
|
||||
var sex = AnimalFactory.IsMaleGenome(genome) ? "male" : "female";
|
||||
|
||||
console.WriteLine($"{def.DefName} ({sex}, seed {seed}):");
|
||||
console.WriteLine(
|
||||
$" body {traits.BodySize:0.##} cells, maturity {traits.MaturityAge:0} d, "
|
||||
+ $"lifespan {traits.Lifespan:0} d, brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
|
||||
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, "
|
||||
+ $"toxinTolerance {traits.ToxinTolerance:0.##}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" 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.##}"
|
||||
);
|
||||
var adultMass = Mass.OfAnimal(def, traits, 1f);
|
||||
console.WriteLine(
|
||||
$" mass: {adultMass:0.#} kg (adult), carries ~{Mass.CarryCapacity(adultMass, 1f):0.#} kg"
|
||||
);
|
||||
}
|
||||
|
||||
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
|
||||
// выведенный рацион (ест растения/мясо), роль и боевые параметры — проверка, что хищники заданы.
|
||||
private void RunDiet(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var sp = _animals[s];
|
||||
if (
|
||||
args.Length > 0
|
||||
&& !string.Equals(sp.Def.DefName, args[0], StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var genome = _animals.GenerateGenome(s, new Random(0x01E7 + s));
|
||||
var traits = AnimalPhenotype.FromTraits(
|
||||
Phenotype.Compute(genome, _animals.GeneRegistry)
|
||||
);
|
||||
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(traits, sp.Def);
|
||||
var role =
|
||||
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
|
||||
: eatsPlants ? "herbivore"
|
||||
: "none";
|
||||
console.WriteLine(
|
||||
$"{sp.Def.DefName}: {role} — herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / "
|
||||
+ $"omni {traits.Omnivory:0.##} -> plants={eatsPlants} meat={eatsMeat}, spawn {sp.Def.SpawnPer1000Cells:0.##}/1000"
|
||||
);
|
||||
if (eatsMeat && sp.Def.AttackDamage > 0f)
|
||||
{
|
||||
console.WriteLine(
|
||||
$" hunt: dmg {sp.Def.AttackDamage:0}/d, bleed {sp.Def.AttackBleed:0.#}/d, "
|
||||
+ $"bloodloss {sp.Def.AttackBloodLoss:0.#}/d, range {sp.Def.AttackRangeCells:0.#} cells"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаемость отбора (фаза A2): средние ключевых признаков живой популяции и размах поколений —
|
||||
// видно дрейф генов под отбором. Без аргумента — все виды; с аргументом — один вид.
|
||||
private void RunPopStats(DevConsole console, string[] args)
|
||||
{
|
||||
if (_animals.Count == 0)
|
||||
{
|
||||
console.WriteLine("no animal species");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = new int[_animals.Count];
|
||||
var body = new double[_animals.Count];
|
||||
var brain = new double[_animals.Count];
|
||||
var life = new double[_animals.Count];
|
||||
var move = new double[_animals.Count];
|
||||
var tox = new double[_animals.Count];
|
||||
var maxGen = new int[_animals.Count];
|
||||
|
||||
Store
|
||||
.Query<AnimalOrganism>()
|
||||
.ForEachEntity(
|
||||
(ref AnimalOrganism o, Entity _) =>
|
||||
{
|
||||
var s = o.Species;
|
||||
count[s]++;
|
||||
body[s] += o.Traits.BodySize;
|
||||
brain[s] += o.Traits.BrainSize;
|
||||
life[s] += o.Traits.Lifespan;
|
||||
move[s] += o.Traits.MoveSpeed;
|
||||
tox[s] += o.Traits.ToxinTolerance;
|
||||
if (o.Generation > maxGen[s])
|
||||
{
|
||||
maxGen[s] = o.Generation;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var filter = args.Length > 0 ? args[0] : null;
|
||||
var any = false;
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var name = _animals[s].Def.DefName;
|
||||
if (
|
||||
count[s] == 0
|
||||
|| (
|
||||
filter is not null
|
||||
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
var c = count[s];
|
||||
console.WriteLine(
|
||||
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, "
|
||||
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, "
|
||||
+ $"toxinTol {tox[s] / c:0.##}"
|
||||
);
|
||||
}
|
||||
|
||||
if (!any)
|
||||
{
|
||||
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
|
||||
}
|
||||
|
||||
// Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра.
|
||||
if (filter is null)
|
||||
{
|
||||
var pn = 0;
|
||||
double pTox = 0,
|
||||
pThorn = 0,
|
||||
pPalat = 0;
|
||||
Store
|
||||
.Query<PlantOrganism>()
|
||||
.ForEachEntity(
|
||||
(ref PlantOrganism o, Entity _) =>
|
||||
{
|
||||
pn++;
|
||||
pTox += o.Traits.Toxicity;
|
||||
pThorn += o.Traits.Thorns;
|
||||
pPalat += o.Traits.Palatability;
|
||||
}
|
||||
);
|
||||
if (pn > 0)
|
||||
{
|
||||
console.WriteLine(
|
||||
$"plants: n={pn}, toxicity {pTox / pn:0.###}, thorns {pThorn / pn:0.###}, "
|
||||
+ $"palatability {pPalat / pn:0.###}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
|
||||
private void RunBodyDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine("usage: body <species> e.g. body Deer");
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(def.Body) || !content.Defs.TryGet<BodyDef>(def.Body, out var body))
|
||||
{
|
||||
console.WriteLine($"{def.DefName}: no body");
|
||||
return;
|
||||
}
|
||||
|
||||
console.WriteLine($"{def.DefName} body '{body.DefName}': {body.Parts.Length} parts");
|
||||
foreach (var part in body.Parts)
|
||||
{
|
||||
var caps =
|
||||
part.Capacities.Count > 0
|
||||
? " ["
|
||||
+ string.Join(
|
||||
", ",
|
||||
part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")
|
||||
)
|
||||
+ "]"
|
||||
: "";
|
||||
console.WriteLine(
|
||||
$" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}"
|
||||
);
|
||||
}
|
||||
|
||||
var state = HealthState.Create(body, 1f);
|
||||
console.WriteLine(
|
||||
"capacities (healthy): "
|
||||
+ string.Join(
|
||||
", ",
|
||||
state.Capacities.OrderBy(k => k.Key).Select(kv => $"{kv.Key} {kv.Value:0.##}")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Тест системы здоровья (фаза A6): навешивает болезнь всем живым животным.
|
||||
private void RunInfect(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var name = args.Length > 0 ? args[0] : "Fever";
|
||||
if (!content.Defs.TryGet<HediffDef>(name, out var def))
|
||||
{
|
||||
console.WriteLine($"no hediff '{name}'");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
Store
|
||||
.Query<Health>()
|
||||
.ForEachEntity(
|
||||
(ref Health h, Entity _) =>
|
||||
{
|
||||
h.State?.Add(def);
|
||||
count++;
|
||||
}
|
||||
);
|
||||
console.WriteLine($"infected {count} animals with {def.DefName}");
|
||||
}
|
||||
|
||||
// Наблюдаемость интеллекта (фаза A7): мозг особи (ген) × сознание = эффективный интеллект, который
|
||||
// гейтит набор нужд/действий. Показывает активные/отключённые нужды при заданном уровне сознания
|
||||
// (по умолчанию 1 — здоров; меньше — имитация повреждения мозга/болезни/боли).
|
||||
private void RunIntel(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine(
|
||||
"usage: intel <species> [consciousness 0..1] [seed] e.g. intel Deer 0.6"
|
||||
);
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var consciousness =
|
||||
args.Length > 1
|
||||
? Math.Clamp(
|
||||
float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture),
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
: 1f;
|
||||
var seed = args.Length > 2 ? int.Parse(args[2]) : Random.Shared.Next();
|
||||
var index = _animals.IndexOf(def);
|
||||
var genome = _animals.GenerateGenome(index, new Random(seed));
|
||||
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
|
||||
var intelligence = traits.BrainSize * consciousness;
|
||||
|
||||
console.WriteLine(
|
||||
$"{def.DefName} (seed {seed}): brain {traits.BrainSize:0.##} x consciousness {consciousness:0.##} "
|
||||
+ $"= intelligence {intelligence:0.###}"
|
||||
);
|
||||
var active = new List<string>();
|
||||
var gated = new List<string>();
|
||||
for (var k = 0; k < _needs.Count; k++)
|
||||
{
|
||||
var need = _needs[k];
|
||||
(need.MinBrain > intelligence ? gated : active).Add(
|
||||
$"{need.DefName}({need.MinBrain:0.##})"
|
||||
);
|
||||
}
|
||||
|
||||
console.WriteLine($" active: {(active.Count > 0 ? string.Join(", ", active) : "—")}");
|
||||
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "—")}");
|
||||
}
|
||||
|
||||
// Наблюдаемость настроения (фаза A8): среднее/мин/макс настроения живой популяции по виду и счётчики
|
||||
// довольных (>0.7) / угнетённых (<0.35). Считает только особей с активным настроением (тир 4+).
|
||||
private void RunMood(DevConsole console, string[] args)
|
||||
{
|
||||
if (_animals.Count == 0)
|
||||
{
|
||||
console.WriteLine("no animal species");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = new int[_animals.Count];
|
||||
var sum = new double[_animals.Count];
|
||||
var min = new float[_animals.Count];
|
||||
var max = new float[_animals.Count];
|
||||
var content_ = new int[_animals.Count];
|
||||
var stressed = new int[_animals.Count];
|
||||
Array.Fill(min, 1f);
|
||||
|
||||
Store
|
||||
.Query<AnimalOrganism, Mood>()
|
||||
.ForEachEntity(
|
||||
(ref AnimalOrganism o, ref Mood m, Entity _) =>
|
||||
{
|
||||
if (!m.Active)
|
||||
{
|
||||
return; // тир ниже зверя — настроения нет
|
||||
}
|
||||
|
||||
var s = o.Species;
|
||||
count[s]++;
|
||||
sum[s] += m.Value;
|
||||
min[s] = MathF.Min(min[s], m.Value);
|
||||
max[s] = MathF.Max(max[s], m.Value);
|
||||
if (m.Value > 0.7f)
|
||||
{
|
||||
content_[s]++;
|
||||
}
|
||||
else if (m.Value < 0.35f)
|
||||
{
|
||||
stressed[s]++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var filter = args.Length > 0 ? args[0] : null;
|
||||
var any = false;
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var name = _animals[s].Def.DefName;
|
||||
if (
|
||||
count[s] == 0
|
||||
|| (
|
||||
filter is not null
|
||||
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
console.WriteLine(
|
||||
$"{name}: n={count[s]}, mood {sum[s] / count[s]:0.##} (min {min[s]:0.##}, max {max[s]:0.##}), "
|
||||
+ $"content {content_[s]}, stressed {stressed[s]}"
|
||||
);
|
||||
}
|
||||
|
||||
if (!any)
|
||||
{
|
||||
console.WriteLine(filter is null ? "no animals with mood" : $"no '{filter}' with mood");
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаемость навыков: средний уровень каждого навыка по живым животным (растут от практики).
|
||||
private void RunSkills(DevConsole console)
|
||||
{
|
||||
if (_skills.Count == 0)
|
||||
{
|
||||
console.WriteLine("no skill defs loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
var sum = new double[_skills.Count];
|
||||
var n = 0;
|
||||
Store
|
||||
.Query<Skills>()
|
||||
.ForEachEntity(
|
||||
(ref Skills s, Entity _) =>
|
||||
{
|
||||
if (s.Levels is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
n++;
|
||||
for (var i = 0; i < _skills.Count && i < s.Levels.Length; i++)
|
||||
{
|
||||
sum[i] += s.Levels[i];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.WriteLine($"skills over {n} animals (mean level):");
|
||||
for (var i = 0; i < _skills.Count; i++)
|
||||
{
|
||||
console.WriteLine(
|
||||
$" {_skills[i].DefName}: {(n > 0 ? sum[i] / n : 0):0.###} "
|
||||
+ $"(learn {_skills[i].LearnRate:0.##}/d, decay {_skills[i].DecayPerDay:0.###}/d)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProductLabel(GameContent content, string productDefName) =>
|
||||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||
? content.Languages.Get(product.Label)
|
||||
: productDefName;
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.App;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using LittleSim.UI;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Assets;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Formulas;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Host;
|
||||
using MrGameEng.Input;
|
||||
using MrGameEng.Inspector;
|
||||
using MrGameEng.Lighting;
|
||||
using MrGameEng.Tilemaps;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D;
|
||||
using Myra.Graphics2D.Brushes;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
// Часть WorldScene: сохранение/загрузка мира и стартовый спавн животных.
|
||||
public sealed partial class WorldScene
|
||||
{
|
||||
private string SaveWorld()
|
||||
{
|
||||
var save = new WorldSave
|
||||
{
|
||||
Name = _config.Name,
|
||||
Width = _config.Width,
|
||||
Height = _config.Height,
|
||||
Seed = _config.Seed,
|
||||
SmoothPasses = _config.SmoothPasses,
|
||||
SavedUtc = DateTime.UtcNow,
|
||||
ElapsedSeconds = Context.Clock.TotalTime,
|
||||
};
|
||||
|
||||
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
|
||||
Store
|
||||
.Query<PlantGrowth, PlantOrganism, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(
|
||||
ref PlantGrowth grow,
|
||||
ref PlantOrganism org,
|
||||
ref Transform2D transform,
|
||||
Entity _
|
||||
) =>
|
||||
{
|
||||
save.Plants.Add(
|
||||
new PlantSave
|
||||
{
|
||||
Species = _plants[grow.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
AgeDays = grow.AgeDays,
|
||||
Stage = grow.Stage,
|
||||
CellFertility = grow.CellFertility,
|
||||
Genome = org.Genome.ToDictionary(),
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SaveAnimals(save);
|
||||
|
||||
// Население — живой счётчик животных на момент сохранения (а не статичный пресет конфига).
|
||||
save.Population = save.Animals.Count;
|
||||
|
||||
new SaveStore().Write(save);
|
||||
Log.Info(
|
||||
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
|
||||
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
|
||||
);
|
||||
return _config.Name;
|
||||
}
|
||||
|
||||
// Снимок популяции животных и трупов: всё нужное, чтобы эволюция/состояние пережили сейв/лоад
|
||||
// (а не пересыпались из сида). Health/Mood/Pregnant берём через entity — запрос ограничен 5 типами.
|
||||
private void SaveAnimals(WorldSave save)
|
||||
{
|
||||
Store
|
||||
.Query<AnimalOrganism, AnimalGrowth, AnimalNeeds, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(
|
||||
ref AnimalOrganism org,
|
||||
ref AnimalGrowth grow,
|
||||
ref AnimalNeeds needs,
|
||||
ref Transform2D transform,
|
||||
Entity entity
|
||||
) =>
|
||||
{
|
||||
var record = new AnimalSave
|
||||
{
|
||||
Species = _animals[org.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
Generation = org.Generation,
|
||||
Stage = grow.Stage,
|
||||
AgeDays = grow.AgeDays,
|
||||
Genome = org.Genome.ToDictionary(),
|
||||
Needs = needs.Values,
|
||||
Starve = needs.Starve,
|
||||
};
|
||||
|
||||
if (entity.GetComponent<Health>().State is { } health)
|
||||
{
|
||||
record.BloodLevel = health.BloodLevel;
|
||||
record.PartHp = health.Parts.Select(p => p.Hp).ToArray();
|
||||
foreach (var hediff in health.Hediffs)
|
||||
{
|
||||
record.Hediffs.Add(
|
||||
new HediffSave
|
||||
{
|
||||
DefName = hediff.Def.DefName,
|
||||
Severity = hediff.Severity,
|
||||
Immunity = hediff.Immunity,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var mood = entity.GetComponent<Mood>();
|
||||
record.Mood = mood.Value;
|
||||
record.MoodActive = mood.Active;
|
||||
if (mood.Thoughts is not null)
|
||||
{
|
||||
foreach (var thought in mood.Thoughts)
|
||||
{
|
||||
record.Thoughts.Add(
|
||||
new ThoughtSave
|
||||
{
|
||||
DefName = thought.Def.DefName,
|
||||
RemainingDays = thought.RemainingDays,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent<Pregnant>(out var preg))
|
||||
{
|
||||
record.Pregnant = true;
|
||||
record.FatherGenome = preg.FatherGenome.ToDictionary();
|
||||
record.DueInDays = preg.DueInDays;
|
||||
record.Litter = preg.Litter;
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent<Skills>(out var sk))
|
||||
{
|
||||
record.SkillLevels = sk.Levels ?? [];
|
||||
record.SkillPassions = sk.Passions is null
|
||||
? []
|
||||
: Array.ConvertAll(sk.Passions, p => (int)p);
|
||||
}
|
||||
|
||||
save.Animals.Add(record);
|
||||
}
|
||||
);
|
||||
|
||||
Store
|
||||
.Query<Corpse, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(ref Corpse corpse, ref Transform2D transform, Entity _) =>
|
||||
save.Corpses.Add(
|
||||
new CorpseSave
|
||||
{
|
||||
Species = _animals[corpse.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
RotDays = corpse.RotDays,
|
||||
BodySize = corpse.BodySize,
|
||||
Meat = corpse.Meat,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Store
|
||||
.Query<Egg, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(ref Egg egg, ref Transform2D transform, Entity _) =>
|
||||
save.Eggs.Add(
|
||||
new EggSave
|
||||
{
|
||||
Species = _animals[egg.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
Generation = egg.Generation,
|
||||
IncubateDays = egg.IncubateDays,
|
||||
Genome = egg.Genome?.ToDictionary() ?? new(),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void RestorePlants(GameContent content)
|
||||
{
|
||||
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
|
||||
foreach (var plant in _save!.Plants)
|
||||
{
|
||||
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
|
||||
{
|
||||
continue; // вид пропал (мод убрали) — пропускаем растение
|
||||
}
|
||||
|
||||
var index = _plants.IndexOf(def);
|
||||
var genome = plant.Genome is { Count: > 0 }
|
||||
? new Genome(plant.Genome)
|
||||
: _plants[index].Template.Generate(fallback);
|
||||
PlantFactory.Create(
|
||||
Store,
|
||||
_plants,
|
||||
index,
|
||||
new Vector2(plant.X, plant.Y),
|
||||
plant.AgeDays,
|
||||
genome,
|
||||
plant.CellFertility,
|
||||
CellSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Восстанавливает популяцию животных и трупы из сейва: фабрика собирает особь из генома (геном
|
||||
// несёт и пол), затем динамическое состояние (нужды/стадия/здоровье/настроение/беременность)
|
||||
// накладывается поверх. Старый/битый сейв без генома — особь регенерируется из шаблона вида.
|
||||
private void RestoreAnimals(GameContent content)
|
||||
{
|
||||
var fallback = new Random(_config.Seed);
|
||||
foreach (var saved in _save!.Animals)
|
||||
{
|
||||
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||
{
|
||||
continue; // вид пропал (мод убрали) — пропускаем особь
|
||||
}
|
||||
|
||||
var index = _animals.IndexOf(def);
|
||||
var genome = saved.Genome is { Count: > 0 }
|
||||
? new Genome(saved.Genome)
|
||||
: _animals.GenerateGenome(index, fallback);
|
||||
var entity = AnimalFactory.Create(
|
||||
Store,
|
||||
_animals,
|
||||
_needs,
|
||||
_skills,
|
||||
index,
|
||||
new Vector2(saved.X, saved.Y),
|
||||
saved.AgeDays,
|
||||
genome,
|
||||
saved.Generation,
|
||||
CellSize
|
||||
);
|
||||
RestoreAnimalState(entity, saved, content);
|
||||
}
|
||||
|
||||
foreach (var saved in _save.Corpses)
|
||||
{
|
||||
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var corpse = AnimalFactory.CreateCorpse(
|
||||
Store,
|
||||
_animals,
|
||||
_animals.IndexOf(def),
|
||||
new Vector2(saved.X, saved.Y),
|
||||
saved.BodySize,
|
||||
saved.Meat,
|
||||
CellSize
|
||||
);
|
||||
ref var data = ref corpse.GetComponent<Corpse>();
|
||||
data.RotDays = saved.RotDays;
|
||||
data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике
|
||||
}
|
||||
|
||||
foreach (var saved in _save.Eggs)
|
||||
{
|
||||
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||
{
|
||||
continue; // вид пропал (мод убрали) — пропускаем яйцо
|
||||
}
|
||||
|
||||
var index = _animals.IndexOf(def);
|
||||
var genome = saved.Genome is { Count: > 0 }
|
||||
? new Genome(saved.Genome)
|
||||
: _animals.GenerateGenome(index, fallback);
|
||||
AnimalFactory.CreateEgg(
|
||||
Store,
|
||||
_animals,
|
||||
index,
|
||||
new Vector2(saved.X, saved.Y),
|
||||
genome,
|
||||
saved.Generation,
|
||||
saved.IncubateDays,
|
||||
CellSize
|
||||
);
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
|
||||
);
|
||||
}
|
||||
|
||||
// Накладывает на свежесозданную особь сохранённое динамическое состояние. Ref-компоненты правим до
|
||||
// AddComponent(Pregnant) (структурное изменение инвалидирует ссылки); Health — managed, безопасен.
|
||||
private void RestoreAnimalState(Entity entity, AnimalSave saved, GameContent content)
|
||||
{
|
||||
ref var needs = ref entity.GetComponent<AnimalNeeds>();
|
||||
if (saved.Needs is { Length: > 0 } && saved.Needs.Length == _needs.Count)
|
||||
{
|
||||
needs.Values = saved.Needs; // состав совпал — иначе остаются свежие из фабрики
|
||||
}
|
||||
|
||||
needs.Starve = saved.Starve;
|
||||
entity.GetComponent<AnimalGrowth>().Stage = saved.Stage;
|
||||
|
||||
ref var mood = ref entity.GetComponent<Mood>();
|
||||
mood.Value = saved.Mood;
|
||||
mood.Active = saved.MoodActive;
|
||||
mood.Thoughts.Clear();
|
||||
foreach (var thought in saved.Thoughts)
|
||||
{
|
||||
if (content.Defs.TryGet<ThoughtDef>(thought.DefName, out var tdef))
|
||||
{
|
||||
mood.Thoughts.Add(
|
||||
new ThoughtInstance { Def = tdef, RemainingDays = thought.RemainingDays }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.GetComponent<Health>().State is { } health)
|
||||
{
|
||||
health.BloodLevel = saved.BloodLevel;
|
||||
// HP частей применяем только при точном совпадении анатомии (как для нужд/навыков): иначе
|
||||
// мод сменил состав/порядок частей тела, и значения легли бы не на те части — оставляем свежее.
|
||||
if (saved.PartHp.Length == health.Parts.Length)
|
||||
{
|
||||
for (var i = 0; i < health.Parts.Length; i++)
|
||||
{
|
||||
health.Parts[i].Hp = saved.PartHp[i];
|
||||
}
|
||||
}
|
||||
else if (saved.PartHp.Length > 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Скелет вида '{saved.Species}' изменился ({saved.PartHp.Length}→{health.Parts.Length} частей) — HP частей не восстановлены."
|
||||
);
|
||||
}
|
||||
|
||||
foreach (var hediff in saved.Hediffs)
|
||||
{
|
||||
if (content.Defs.TryGet<HediffDef>(hediff.DefName, out var hdef))
|
||||
{
|
||||
health.AddHediff(hdef, hediff.Severity, hediff.Immunity);
|
||||
}
|
||||
}
|
||||
|
||||
health.RecomputeCapacities();
|
||||
}
|
||||
|
||||
if (saved.Pregnant && saved.FatherGenome is { Count: > 0 })
|
||||
{
|
||||
entity.AddComponent(
|
||||
new Pregnant
|
||||
{
|
||||
FatherGenome = new Genome(saved.FatherGenome),
|
||||
DueInDays = saved.DueInDays,
|
||||
Litter = saved.Litter,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Навыки: накладываем сохранённые уровни/страсть, если состав совпал (иначе — свежие из фабрики).
|
||||
if (entity.TryGetComponent<Skills>(out var skills) && skills.Levels is not null)
|
||||
{
|
||||
if (saved.SkillLevels.Length == skills.Levels.Length)
|
||||
{
|
||||
saved.SkillLevels.CopyTo(skills.Levels, 0);
|
||||
}
|
||||
|
||||
if (skills.Passions is not null && saved.SkillPassions.Length == skills.Passions.Length)
|
||||
{
|
||||
for (var i = 0; i < skills.Passions.Length; i++)
|
||||
{
|
||||
skills.Passions[i] = (byte)Math.Clamp(saved.SkillPassions[i], 0, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0
|
||||
// рассыпается по клеткам суши с плотностью из дефа; геном и возраст особи — из шаблона вида.
|
||||
private void SpawnAnimals()
|
||||
{
|
||||
var landCells = 0;
|
||||
for (var i = 0; i < _cellLand.Length; i++)
|
||||
{
|
||||
if (_cellLand[i])
|
||||
{
|
||||
landCells++;
|
||||
}
|
||||
}
|
||||
|
||||
if (_animals.Count == 0 || landCells == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Свой офсет сида (0x5EED занят системой решений животных — нельзя связывать их RNG-потоки).
|
||||
var random = new Random(_config.Seed + 0x5A1D);
|
||||
var spawned = 0;
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var def = _animals[s].Def;
|
||||
if (def.SpawnPer1000Cells <= 0f)
|
||||
{
|
||||
continue; // вид не спавнится сам (напр. появляется только в сейве/событием)
|
||||
}
|
||||
|
||||
var count = Math.Max(1, (int)MathF.Round(landCells / 1000f * def.SpawnPer1000Cells));
|
||||
var maxAge =
|
||||
(def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f)
|
||||
* 0.85f;
|
||||
var placed = 0;
|
||||
var attempts = 0;
|
||||
while (placed < count && attempts < count * 50)
|
||||
{
|
||||
attempts++;
|
||||
var cell = random.Next(_cellLand.Length);
|
||||
if (!_cellLand[cell])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cx = cell % _config.Width;
|
||||
var cy = cell / _config.Width;
|
||||
var px = (cx + 0.2f + random.NextSingle() * 0.6f) * CellSize;
|
||||
var py = (cy + 0.2f + random.NextSingle() * 0.6f) * CellSize;
|
||||
AnimalFactory.Create(
|
||||
Store,
|
||||
_animals,
|
||||
_needs,
|
||||
_skills,
|
||||
s,
|
||||
new Vector2(px, py),
|
||||
ageDays: random.NextSingle() * maxAge,
|
||||
_animals.GenerateGenome(s, random),
|
||||
generation: 0,
|
||||
CellSize
|
||||
);
|
||||
placed++;
|
||||
}
|
||||
|
||||
spawned += placed;
|
||||
}
|
||||
|
||||
Log.Info($"Spawned {spawned} animals ({_animals.Count} species)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.App;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using LittleSim.UI;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Assets;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Formulas;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Host;
|
||||
using MrGameEng.Input;
|
||||
using MrGameEng.Inspector;
|
||||
using MrGameEng.Lighting;
|
||||
using MrGameEng.Tilemaps;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D;
|
||||
using Myra.Graphics2D.Brushes;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
// Часть WorldScene: генерация рельефа, поле берега и окклюдеры освещения.
|
||||
public sealed partial class WorldScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
|
||||
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
|
||||
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
|
||||
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
|
||||
/// </summary>
|
||||
private void BuildTerrain(
|
||||
GameContent content,
|
||||
ModAtlases atlases,
|
||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||
AssetManager assets,
|
||||
Random? scatterRandom
|
||||
)
|
||||
{
|
||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||
|
||||
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
|
||||
var tiles = new TileSet();
|
||||
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
||||
foreach (var terrain in content.Terrains.All)
|
||||
{
|
||||
tileByDef[terrain] = terrain.Surface is { } surface
|
||||
? tiles.Add(atlases.GetRegion(device, surface))
|
||||
: tiles.Add(white, terrain.Tint);
|
||||
}
|
||||
|
||||
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
|
||||
var heights = WorldGenerator.Generate(
|
||||
_config.Width,
|
||||
_config.Height,
|
||||
_config.Seed,
|
||||
_config.SmoothPasses
|
||||
);
|
||||
var grid = new TileGrid(_config.Width, _config.Height);
|
||||
var cells = _config.Width * _config.Height;
|
||||
_cellFertility = new float[cells];
|
||||
_cellLand = new bool[cells];
|
||||
_cellOccluderBase = new bool[cells];
|
||||
_cellOccluder = new bool[cells];
|
||||
for (var x = 0; x < _config.Width; x++)
|
||||
{
|
||||
for (var y = 0; y < _config.Height; y++)
|
||||
{
|
||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
||||
grid[x, y] = tileByDef[terrain];
|
||||
var cell = y * _config.Width + x;
|
||||
_cellFertility[cell] = terrain.Fertility;
|
||||
_cellLand[cell] = terrain.IsLand;
|
||||
_cellOccluderBase[cell] = terrain.BlocksLight;
|
||||
if (scatterRandom is not null)
|
||||
{
|
||||
ScatterSpawner.Spawn(
|
||||
this,
|
||||
content,
|
||||
_plants,
|
||||
terrain,
|
||||
new Point(x, y),
|
||||
CellSize,
|
||||
scatterRandom
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||
}
|
||||
|
||||
// Поле «ближайший берег» по клеткам мира: для каждой клетки — мировая позиция ближайшей береговой
|
||||
// клетки (суша, граничащая с водой), найденная многоисточниковым BFS за O(клеток). Звери читают его
|
||||
// за O(1) при поиске водопоя (см. AnimalDecisionSystem.TryFindShore), не сканируя береговую линию.
|
||||
// Пусто, если воды/берега в мире нет — тогда пить негде (зверь не пойдёт).
|
||||
private Vector2[] ComputeShoreField()
|
||||
{
|
||||
var w = _config.Width;
|
||||
var h = _config.Height;
|
||||
var count = w * h;
|
||||
|
||||
// BFS: nearest[c] — индекс ближайшей береговой клетки. Источники — все береговые клетки (шаг 0).
|
||||
var nearest = new int[count];
|
||||
Array.Fill(nearest, -1);
|
||||
var queue = new Queue<int>();
|
||||
for (var cell = 0; cell < count; cell++)
|
||||
{
|
||||
if (!_cellLand[cell])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var x = cell % w;
|
||||
var y = cell / w;
|
||||
var nearWater =
|
||||
(x > 0 && !_cellLand[cell - 1])
|
||||
|| (x < w - 1 && !_cellLand[cell + 1])
|
||||
|| (y > 0 && !_cellLand[cell - w])
|
||||
|| (y < h - 1 && !_cellLand[cell + w]);
|
||||
if (nearWater)
|
||||
{
|
||||
nearest[cell] = cell;
|
||||
queue.Enqueue(cell);
|
||||
}
|
||||
}
|
||||
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
return []; // нет берега
|
||||
}
|
||||
|
||||
// Волна по 4-связности: каждой клетке достаётся источник ближайшего фронта (берег по шагам сетки).
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var cell = queue.Dequeue();
|
||||
var src = nearest[cell];
|
||||
var x = cell % w;
|
||||
var y = cell / w;
|
||||
if (x > 0 && nearest[cell - 1] < 0)
|
||||
{
|
||||
nearest[cell - 1] = src;
|
||||
queue.Enqueue(cell - 1);
|
||||
}
|
||||
|
||||
if (x < w - 1 && nearest[cell + 1] < 0)
|
||||
{
|
||||
nearest[cell + 1] = src;
|
||||
queue.Enqueue(cell + 1);
|
||||
}
|
||||
|
||||
if (y > 0 && nearest[cell - w] < 0)
|
||||
{
|
||||
nearest[cell - w] = src;
|
||||
queue.Enqueue(cell - w);
|
||||
}
|
||||
|
||||
if (y < h - 1 && nearest[cell + w] < 0)
|
||||
{
|
||||
nearest[cell + w] = src;
|
||||
queue.Enqueue(cell + w);
|
||||
}
|
||||
}
|
||||
|
||||
var field = new Vector2[count];
|
||||
for (var cell = 0; cell < count; cell++)
|
||||
{
|
||||
var src = nearest[cell];
|
||||
if (src >= 0)
|
||||
{
|
||||
field[cell] = new Vector2((src % w + 0.5f) * CellSize, (src / w + 0.5f) * CellSize);
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
// Затеняют ли зрелые деревья землю. Сейчас выключено: одиночное дерево = 1 клетка-окклюдер, а спрайт
|
||||
// кроны крупнее и смещён, плюс направленная тень тянется «хвостом» — на открытой почве это читается как
|
||||
// странные оторванные пятна. Вернуть, когда у кроны будет настоящий многоклеточный след затенения.
|
||||
private static readonly bool TreesCastShade = false;
|
||||
|
||||
// Сетка окклюдеров для лайтмапа: светонепроницаемый террейн (сейчас такого нет) + опц. зрелые деревья.
|
||||
private bool[] BuildOccluders()
|
||||
{
|
||||
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
|
||||
if (!TreesCastShade)
|
||||
{
|
||||
return _cellOccluder; // ровное освещение — без странных теней от отдельных деревьев
|
||||
}
|
||||
|
||||
Store
|
||||
.Query<PlantGrowth, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
|
||||
{
|
||||
var species = _plants[grow.Species];
|
||||
if (
|
||||
species.Def.TrunkRadiusCells <= 0f
|
||||
|| grow.Stage < species.Stages.Length - 1
|
||||
)
|
||||
{
|
||||
return; // затеняют только зрелые деревья (со стволом)
|
||||
}
|
||||
|
||||
var cx = Math.Clamp(
|
||||
(int)(transform.Position.X / CellSize),
|
||||
0,
|
||||
_config.Width - 1
|
||||
);
|
||||
var cy = Math.Clamp(
|
||||
(int)(transform.Position.Y / CellSize),
|
||||
0,
|
||||
_config.Height - 1
|
||||
);
|
||||
_cellOccluder[cy * _config.Width + cx] = true;
|
||||
}
|
||||
);
|
||||
return _cellOccluder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.App;
|
||||
using LittleSim.Content;
|
||||
using LittleSim.Sim;
|
||||
using LittleSim.UI;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Assets;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Formulas;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.Host;
|
||||
using MrGameEng.Input;
|
||||
using MrGameEng.Inspector;
|
||||
using MrGameEng.Lighting;
|
||||
using MrGameEng.Tilemaps;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D;
|
||||
using Myra.Graphics2D.Brushes;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.Scenes;
|
||||
|
||||
// Часть WorldScene: HUD-хелперы, полоса скорости, горячие клавиши, переходы.
|
||||
public sealed partial class WorldScene
|
||||
{
|
||||
// Переход к «ответственному» за событие (двойной клик в журнале): выбирает сущность и центрирует на
|
||||
// ней камеру, если она ещё существует (актёр мог умереть). Камера зажимается к границам при отрисовке.
|
||||
private void FocusEntity(Entity camera, int id)
|
||||
{
|
||||
if (
|
||||
id < 0
|
||||
|| !Store.TryGetEntityById(id, out var target)
|
||||
|| target.IsNull
|
||||
|| !target.HasComponent<Transform2D>()
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selection.Select(id);
|
||||
ref var cam = ref camera.GetComponent<Camera>();
|
||||
cam.Position = target.GetComponent<Transform2D>().Position;
|
||||
}
|
||||
|
||||
// Кнопка-переключатель оверлея HUD: подсвечивается как активная, пока оверлей открыт.
|
||||
private static TextButton HudToggle(string text, Action onClick, Func<bool> isOpen)
|
||||
{
|
||||
var button = Ui.ChoiceButton(text);
|
||||
button.Click += (_, _) =>
|
||||
{
|
||||
onClick();
|
||||
Ui.SetChoiceActive(button, isOpen());
|
||||
};
|
||||
Ui.SetChoiceActive(button, isOpen());
|
||||
return button;
|
||||
}
|
||||
|
||||
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
||||
{
|
||||
var bar = Ui.Row(6);
|
||||
bar.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
bar.VerticalAlignment = VerticalAlignment.Bottom;
|
||||
bar.Margin = new Thickness(0, 0, 0, 12);
|
||||
|
||||
var hover = new SolidBrush(new Color(40, 54, 72)); // подсветка кнопок скорости при наведении
|
||||
var pause = new TextButton
|
||||
{
|
||||
Text = content.Languages.Get("speed.pause"),
|
||||
Padding = new Thickness(8, 4),
|
||||
OverBackground = hover,
|
||||
};
|
||||
pause.Click += (_, _) => _speed.Pause();
|
||||
bar.Widgets.Add(pause);
|
||||
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
||||
|
||||
for (var i = 0; i < _speed.Steps.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var button = new TextButton
|
||||
{
|
||||
Text = $"x{_speed.Steps[i]:0}",
|
||||
Padding = new Thickness(8, 4),
|
||||
OverBackground = hover,
|
||||
};
|
||||
button.Click += (_, _) => _speed.SetStep(index);
|
||||
bar.Widgets.Add(button);
|
||||
_speedRefreshers.Add(() =>
|
||||
button.TextColor =
|
||||
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
|
||||
);
|
||||
}
|
||||
|
||||
void Refresh()
|
||||
{
|
||||
foreach (var refresh in _speedRefreshers)
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
_speed.Changed += Refresh;
|
||||
RegisterUnload(() => _speed.Changed -= Refresh);
|
||||
Refresh();
|
||||
return bar;
|
||||
}
|
||||
|
||||
private void Hotkeys(InputManager input)
|
||||
{
|
||||
if (input.IsKeyPressed(Keys.Escape))
|
||||
{
|
||||
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
|
||||
if (_selection.HasSelection)
|
||||
{
|
||||
_selection.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_pause.Toggle();
|
||||
}
|
||||
}
|
||||
|
||||
if (_pause.IsOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.IsKeyPressed(Keys.Space))
|
||||
{
|
||||
_speed.TogglePause();
|
||||
}
|
||||
|
||||
if (input.IsKeyPressed(Keys.D1))
|
||||
{
|
||||
_speed.SetStep(0);
|
||||
}
|
||||
|
||||
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
|
||||
{
|
||||
_speed.SetStep(1);
|
||||
}
|
||||
|
||||
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
|
||||
{
|
||||
_speed.SetStep(2);
|
||||
}
|
||||
}
|
||||
|
||||
private void Switch(Scene scene)
|
||||
{
|
||||
if (!Context.Scenes.IsTransitioning)
|
||||
{
|
||||
_speed.Resume();
|
||||
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
-1297
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>Идентификаторы способностей (capacities) организма — на них ссылаются вклады частей тела.</summary>
|
||||
public static class AnimalCapacities
|
||||
{
|
||||
public const string Consciousness = "Consciousness";
|
||||
public const string Moving = "Moving";
|
||||
public const string Breathing = "Breathing";
|
||||
public const string BloodPumping = "BloodPumping";
|
||||
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";
|
||||
|
||||
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
|
||||
public static readonly string[] DisplayOrder =
|
||||
[
|
||||
Consciousness,
|
||||
Moving,
|
||||
Sight,
|
||||
Hearing,
|
||||
Smell,
|
||||
Touch,
|
||||
Talking,
|
||||
Eating,
|
||||
Breathing,
|
||||
BloodPumping,
|
||||
BloodFiltration,
|
||||
Digestion,
|
||||
];
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public static class AnimalFactory
|
||||
var sp = animals[species];
|
||||
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry));
|
||||
var isMale = IsMaleGenome(genome);
|
||||
var (eatsPlants, eatsMeat) = Diet(traits, sp.Def); // диета фиксируется при рождении (см. EatsPlants/EatsMeat)
|
||||
var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan, sp.Stages);
|
||||
var region = RegionFor(sp, stage, isMale);
|
||||
|
||||
@@ -54,6 +55,8 @@ public static class AnimalFactory
|
||||
Traits = traits,
|
||||
Species = species,
|
||||
IsMale = isMale,
|
||||
EatsPlants = eatsPlants,
|
||||
EatsMeat = eatsMeat,
|
||||
Generation = generation,
|
||||
},
|
||||
new AnimalGrowth { Stage = stage, AgeDays = ageDays },
|
||||
@@ -92,12 +95,20 @@ public static class AnimalFactory
|
||||
Capacity = (int)MathF.Round(MathF.Max(0f, traits.BrainSize) * MemoryPerBrain),
|
||||
}
|
||||
);
|
||||
|
||||
// Личная хроника (UI «История»): кольцевой список последних событий особи.
|
||||
entity.AddComponent(new CreatureLog { Entries = [], Capacity = CreatureLogCapacity });
|
||||
// Вспышка урона (презентация): заранее на нуле, чтобы удар лишь выставлял время (без структурных правок).
|
||||
entity.AddComponent(new DamageFlash());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это).</summary>
|
||||
public const float MemoryPerBrain = 16f;
|
||||
|
||||
/// <summary>Глубина личной хроники особи (последние N событий о ней).</summary>
|
||||
public const int CreatureLogCapacity = 24;
|
||||
|
||||
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
|
||||
private static int SkillSeed(Genome genome)
|
||||
{
|
||||
|
||||
@@ -1,720 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Признаки (фенотип) животного, вычисленные из генома формулами генов (см. <c>Defs/Genes/</c>):
|
||||
/// готовые значения, которые читают системы, не касаясь генов напрямую. Зеркало
|
||||
/// <see cref="PlantPhenotype"/>. Имена полей соответствуют именам признаков в эффектах генов.
|
||||
/// </summary>
|
||||
public struct AnimalPhenotype
|
||||
{
|
||||
/// <summary>Размер тела в клетках (масштаб спрайта; позже — HP/расход энергии).</summary>
|
||||
public float BodySize;
|
||||
|
||||
/// <summary>Скорость обмена веществ — множитель расхода нужд.</summary>
|
||||
public float Metabolism;
|
||||
|
||||
/// <summary>Скорость передвижения (множитель).</summary>
|
||||
public float MoveSpeed;
|
||||
|
||||
/// <summary>Объём крови (множитель) — масштаб скорости кровопотери.</summary>
|
||||
public float BloodVolume;
|
||||
|
||||
/// <summary>Дальность зрения (множитель радиуса поиска).</summary>
|
||||
public float Vision;
|
||||
|
||||
/// <summary>Острота слуха (множитель радиуса).</summary>
|
||||
public float Hearing;
|
||||
|
||||
/// <summary>Острота обоняния (множитель радиуса).</summary>
|
||||
public float Smell;
|
||||
|
||||
/// <summary>Острота осязания (множитель радиуса; чувство ближнего контакта).</summary>
|
||||
public float Touch;
|
||||
|
||||
/// <summary>Камуфляж [0..1] — снижает обнаружение этой особи зрением (заметность = 1−camouflage).</summary>
|
||||
public float Camouflage;
|
||||
|
||||
/// <summary>Запах [0..1] — повышает обнаружение этой особи обонянием.</summary>
|
||||
public float Scent;
|
||||
|
||||
/// <summary>Шумность [0..1] — повышает обнаружение этой особи слухом при движении.</summary>
|
||||
public float Noise;
|
||||
|
||||
/// <summary>Размер мозга (0..1) — основа интеллекта.</summary>
|
||||
public float BrainSize;
|
||||
|
||||
/// <summary>Теплоизоляция мехом (0..1).</summary>
|
||||
public float Insulation;
|
||||
|
||||
/// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта.</summary>
|
||||
public float FurHue;
|
||||
|
||||
/// <summary>Возраст созревания (игровых дней) — порог стадии Adult.</summary>
|
||||
public float MaturityAge;
|
||||
|
||||
/// <summary>Травоядность [0..1] — способность/тяга питаться растениями.</summary>
|
||||
public float Herbivory;
|
||||
|
||||
/// <summary>Хищничество [0..1] — тяга охотиться и питаться мясом (живая добыча/падаль).</summary>
|
||||
public float Carnivory;
|
||||
|
||||
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
|
||||
public float Omnivory;
|
||||
|
||||
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
|
||||
public float ToxinTolerance;
|
||||
|
||||
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
|
||||
public float Sociability;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||
public float Lifespan;
|
||||
|
||||
/// <summary>Сезон гона (0 весна … 3 зима).</summary>
|
||||
public int BreedingSeason;
|
||||
|
||||
/// <summary>Срок вынашивания (игровых дней).</summary>
|
||||
public float GestationDays;
|
||||
|
||||
/// <summary>Размер помёта.</summary>
|
||||
public float LitterSize;
|
||||
|
||||
/// <summary>Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.</summary>
|
||||
public float EggLaying;
|
||||
|
||||
/// <summary>Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.</summary>
|
||||
public readonly bool Oviparous => EggLaying >= 0.5f;
|
||||
|
||||
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> 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"),
|
||||
Metabolism = T("metabolism"),
|
||||
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"),
|
||||
MaturityAge = T("maturityAge"),
|
||||
Herbivory = T("herbivory"),
|
||||
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"),
|
||||
LitterSize = T("litterSize"),
|
||||
EggLaying = T("eggLaying"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Компонент-организм животного: управляемый <see cref="Genome"/> (для размножения, сейва и
|
||||
/// пересчёта), выраженный фенотип <see cref="Traits"/>, индекс вида в <see cref="Content.AnimalSet"/>
|
||||
/// и номер поколения (для <c>popstats</c> и видимого дрейфа). Геном — ссылочный объект, поэтому
|
||||
/// компонент несёт ссылку, а не копию (как у растений). Зеркало <see cref="PlantOrganism"/>.
|
||||
/// </summary>
|
||||
public struct AnimalOrganism : IComponent
|
||||
{
|
||||
/// <summary>Геном особи (аллели по генам); источник истины для признаков и размножения.</summary>
|
||||
public Genome Genome;
|
||||
|
||||
/// <summary>Выраженные признаки — то, что читают системы.</summary>
|
||||
public AnimalPhenotype Traits;
|
||||
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/>.</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Пол: самец (XY) или самка (XX) — выведен из локуса GeneSex при создании.</summary>
|
||||
public bool IsMale;
|
||||
|
||||
/// <summary>Номер поколения: max(родителей) + 1; у основателей 0.</summary>
|
||||
public int Generation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Состояние роста животного (фаза A3): текущая стадия и накопленный возраст в игровых днях. Плоские
|
||||
/// данные — переход между стадиями (Baby→Juvenile→Adult→Senior), смену спрайта и размера выполняет
|
||||
/// <see cref="AnimalGrowthSystem"/>, читая пороги из генов (возраст созревания, продолжительность жизни).
|
||||
/// Зеркало <see cref="PlantGrowth"/>.
|
||||
/// </summary>
|
||||
public struct AnimalGrowth : IComponent
|
||||
{
|
||||
/// <summary>Текущая стадия: 0 Baby, 1 Juvenile, 2 Adult, 3 Senior.</summary>
|
||||
public int Stage;
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays;
|
||||
}
|
||||
|
||||
/// <summary>Инстанс части тела особи: ссылка на деф части и текущий/максимальный HP (масштаб по размеру).</summary>
|
||||
public struct PartInstance
|
||||
{
|
||||
/// <summary>Деф части тела (анатомия вида).</summary>
|
||||
public Content.BodyPartDef Def;
|
||||
|
||||
/// <summary>Текущий HP.</summary>
|
||||
public float Hp;
|
||||
|
||||
/// <summary>Максимальный HP (база × размер тела).</summary>
|
||||
public float MaxHp;
|
||||
|
||||
/// <summary>Доля сохранности [0,1].</summary>
|
||||
public readonly float Fraction => MaxHp > 0f ? Math.Clamp(Hp / MaxHp, 0f, 1f) : 0f;
|
||||
}
|
||||
|
||||
/// <summary>Инстанс хедифа на особи: деф + текущая тяжесть и набранный иммунитет (гонка, фаза A6).</summary>
|
||||
public struct Hediff
|
||||
{
|
||||
/// <summary>Деф хедифа (гон/болезнь/…).</summary>
|
||||
public HediffDef Def;
|
||||
|
||||
/// <summary>Текущая тяжесть.</summary>
|
||||
public float Severity;
|
||||
|
||||
/// <summary>Набранный иммунитет [0,1]; при 1 — выздоровление.</summary>
|
||||
public float Immunity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Состояние здоровья особи (фазы A4–A6): активные хедифы (инстансы) + части тела, кровь и вычисленные
|
||||
/// способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает ссылку в
|
||||
/// компоненте). Болезни (A6) — первый источник урона: снижают способности и могут убить. Растёт по
|
||||
/// фазам — контейнер один.
|
||||
/// </summary>
|
||||
public sealed class HealthState
|
||||
{
|
||||
private readonly List<Hediff> _hediffs = [];
|
||||
|
||||
/// <summary>Активные хедифы (гон, болезни).</summary>
|
||||
public IReadOnlyList<Hediff> Hediffs => _hediffs;
|
||||
|
||||
/// <summary>Части тела особи (из анатомии вида); пусто — у вида нет тела.</summary>
|
||||
public PartInstance[] Parts { get; private set; } = [];
|
||||
|
||||
/// <summary>Уровень крови [0,1]; ниже порога — потеря сознания и смерть (когда появятся раны).</summary>
|
||||
public float BloodLevel { get; set; } = 1f;
|
||||
|
||||
/// <summary>Боль [0,1] — снижает сознание (когда появятся раны).</summary>
|
||||
public float Pain { get; set; }
|
||||
|
||||
/// <summary>Погибла ли особь по здоровью (vital-часть уничтожена / кровь на нуле).</summary>
|
||||
public bool Dead { get; set; }
|
||||
|
||||
/// <summary>Вычисленные способности (capacity id → [0,1]); у здоровой особи все = 1.</summary>
|
||||
public Dictionary<string, float> Capacities { get; private set; } = new();
|
||||
|
||||
/// <summary>Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий.</summary>
|
||||
public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
|
||||
/// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель <paramref name="capacityScale"/>
|
||||
/// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
|
||||
/// частей/крови/хедифов; <paramref name="capacityScale"/>=1 — без возрастного спада.
|
||||
/// </summary>
|
||||
public void RecomputeCapacities(float capacityScale = 1f)
|
||||
{
|
||||
var pain = 0f;
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
pain += h.Def.Pain * h.Severity;
|
||||
}
|
||||
|
||||
Pain = Math.Clamp(pain, 0f, 1f);
|
||||
|
||||
// Множитель старения применяется внутри Compute (без второго прохода/аллокации списка ключей).
|
||||
var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain, capacityScale);
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
if (h.Def.CapMods.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var t = Math.Clamp(h.Severity, 0f, 1f);
|
||||
foreach (var (capacity, factor) in h.Def.CapMods)
|
||||
{
|
||||
caps[capacity] = caps.GetValueOrDefault(capacity, 1f) * (1f + (factor - 1f) * t);
|
||||
}
|
||||
}
|
||||
|
||||
Capacities = caps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
|
||||
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
|
||||
/// <paramref name="immunityScale"/> — множитель набора иммунитета (способность «фильтрация крови»:
|
||||
/// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
|
||||
/// </summary>
|
||||
public bool AdvanceHediffs(float days, float immunityScale = 1f)
|
||||
{
|
||||
var lethal = false;
|
||||
for (var i = _hediffs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var h = _hediffs[i];
|
||||
if (h.Def.SeverityPerDay <= 0f && h.Def.ImmunityPerDay <= 0f)
|
||||
{
|
||||
continue; // статичный хедиф (гон) не прогрессирует
|
||||
}
|
||||
|
||||
h.Severity += h.Def.SeverityPerDay * days;
|
||||
h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
|
||||
if (h.Immunity >= 1f)
|
||||
{
|
||||
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление
|
||||
continue;
|
||||
}
|
||||
|
||||
if (h.Def.LethalSeverity > 0f && h.Severity >= h.Def.LethalSeverity)
|
||||
{
|
||||
lethal = true;
|
||||
}
|
||||
|
||||
_hediffs[i] = h;
|
||||
}
|
||||
|
||||
return lethal;
|
||||
}
|
||||
|
||||
/// <summary>Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности.</summary>
|
||||
public static HealthState Create(Content.BodyDef? body, float bodySize)
|
||||
{
|
||||
var state = new HealthState();
|
||||
if (body is { Parts.Length: > 0 })
|
||||
{
|
||||
var scale = MathF.Max(0.2f, bodySize);
|
||||
state.Parts = new PartInstance[body.Parts.Length];
|
||||
for (var i = 0; i < body.Parts.Length; i++)
|
||||
{
|
||||
var hp = body.Parts[i].MaxHp * scale;
|
||||
state.Parts[i] = new PartInstance
|
||||
{
|
||||
Def = body.Parts[i],
|
||||
Hp = hp,
|
||||
MaxHp = hp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
state.RecomputeCapacities();
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>Несёт ли особь хедиф с данным именем дефа.</summary>
|
||||
public bool Has(string defName)
|
||||
{
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
if (h.Def.DefName == defName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Навешивает хедиф (если ещё не навешан) с начальной тяжестью из дефа.</summary>
|
||||
public void Add(HediffDef def)
|
||||
{
|
||||
if (!Has(def.DefName))
|
||||
{
|
||||
_hediffs.Add(new Hediff { Def = def, Severity = def.InitialSeverity });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Восстанавливает хедиф с заданной тяжестью и иммунитетом (для загрузки сейва).</summary>
|
||||
public void AddHediff(HediffDef def, float severity, float immunity)
|
||||
{
|
||||
if (!Has(def.DefName))
|
||||
{
|
||||
_hediffs.Add(
|
||||
new Hediff
|
||||
{
|
||||
Def = def,
|
||||
Severity = severity,
|
||||
Immunity = immunity,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Снимает хедиф по имени дефа; возвращает, был ли он.</summary>
|
||||
public bool Remove(string defName)
|
||||
{
|
||||
for (var i = 0; i < _hediffs.Count; i++)
|
||||
{
|
||||
if (_hediffs[i].Def.DefName == defName)
|
||||
{
|
||||
_hediffs.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Травма (укус хищника): повреждает часть тела (выбор по весу попадания <c>coverage</c>), вызывает
|
||||
/// мгновенную кровопотерю и навешивает/усиливает кровотечение, затем пересчитывает способности.
|
||||
/// Первый травматический источник урона — оживляет каркас здоровья A5.
|
||||
/// </summary>
|
||||
public void ApplyInjury(
|
||||
float partDamage,
|
||||
float bloodLoss,
|
||||
HediffDef? bleeding,
|
||||
float bleedSeverity,
|
||||
Random rng
|
||||
)
|
||||
{
|
||||
if (Parts.Length > 0 && partDamage > 0f)
|
||||
{
|
||||
var idx = PickPart(rng);
|
||||
var part = Parts[idx];
|
||||
part.Hp = MathF.Max(0f, part.Hp - partDamage);
|
||||
Parts[idx] = part;
|
||||
}
|
||||
|
||||
BloodLevel = Math.Clamp(BloodLevel - bloodLoss, 0f, 1f);
|
||||
if (bleeding is not null && bleedSeverity > 0f)
|
||||
{
|
||||
Intensify(bleeding, bleedSeverity);
|
||||
}
|
||||
|
||||
RecomputeCapacities();
|
||||
}
|
||||
|
||||
// Выбор задетой части по весу попадания (coverage); детерминируется переданным rng.
|
||||
private int PickPart(Random rng)
|
||||
{
|
||||
var total = 0f;
|
||||
foreach (var p in Parts)
|
||||
{
|
||||
total += p.Def?.Coverage ?? 0f;
|
||||
}
|
||||
|
||||
if (total <= 0f)
|
||||
{
|
||||
return rng.Next(Parts.Length);
|
||||
}
|
||||
|
||||
var roll = (float)rng.NextDouble() * total;
|
||||
for (var i = 0; i < Parts.Length; i++)
|
||||
{
|
||||
if (Parts[i].Def is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
roll -= Parts[i].Def.Coverage;
|
||||
if (roll <= 0f)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return Parts.Length - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Навешивает хедиф или усиливает существующий: тяжесть растёт (до 1), а заживление (иммунитет)
|
||||
/// сбрасывается — свежая рана начинает затягиваться заново. Для повторных укусов/ранений.
|
||||
/// </summary>
|
||||
public void Intensify(HediffDef def, float addSeverity)
|
||||
{
|
||||
for (var i = 0; i < _hediffs.Count; i++)
|
||||
{
|
||||
if (_hediffs[i].Def.DefName == def.DefName)
|
||||
{
|
||||
var h = _hediffs[i];
|
||||
h.Severity = Math.Clamp(h.Severity + addSeverity, 0f, 1f);
|
||||
h.Immunity = 0f; // свежая рана — заживление с нуля
|
||||
_hediffs[i] = h;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_hediffs.Add(new Hediff { Def = def, Severity = Math.Clamp(addSeverity, 0f, 1f) });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Здоровье особи: managed-состояние со списком хедифов (см. <see cref="HealthState"/>).</summary>
|
||||
public struct Health : IComponent
|
||||
{
|
||||
/// <summary>Состояние здоровья (хедифы; позже части тела/кровь).</summary>
|
||||
public HealthState State;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Труп животного (фаза A5): отдельная сущность после смерти. Разлагается во времени по стадиям
|
||||
/// (Fresh→Rotting→Skeleton→исчезает), скорость зависит от температуры. Несёт запас мяса (∝ размер тела)
|
||||
/// для будущей разделки/падальщиков. Управляется <see cref="Sim.CorpseSystem"/>.
|
||||
/// </summary>
|
||||
public struct Corpse : IComponent
|
||||
{
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта, разделки, сейва).</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||
public float RotDays;
|
||||
|
||||
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
|
||||
public int Stage;
|
||||
|
||||
/// <summary>Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт).</summary>
|
||||
public float Meat;
|
||||
|
||||
/// <summary>Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса.</summary>
|
||||
public float BodySize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
|
||||
/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
|
||||
/// вылупляется в детёныша (<see cref="Sim.EggSystem"/>). Геном — managed-ссылка (как у трупа/беременности).
|
||||
/// </summary>
|
||||
public struct Egg : IComponent
|
||||
{
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта при вылуплении и сейва).</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Геном будущего детёныша (уже скрещён из родителей на момент кладки).</summary>
|
||||
public Genome Genome;
|
||||
|
||||
/// <summary>Поколение будущего детёныша.</summary>
|
||||
public int Generation;
|
||||
|
||||
/// <summary>Остаток инкубации в игровых днях; ≤0 — вылупление.</summary>
|
||||
public float IncubateDays;
|
||||
}
|
||||
|
||||
/// <summary>Вид запомненного: угроза (избегать), корм/вода/партнёр (идти) — расширяемо.</summary>
|
||||
public enum MemoryKind : byte
|
||||
{
|
||||
Threat,
|
||||
Food,
|
||||
Water,
|
||||
Mate,
|
||||
}
|
||||
|
||||
/// <summary>Запись памяти: что, где (воспринятая позиция — грубая, если по запаху), насколько уверенно
|
||||
/// (падает со временем — забывание), сколько дней назад.</summary>
|
||||
public struct MemoryEntry
|
||||
{
|
||||
public MemoryKind Kind;
|
||||
public Vector2 Pos;
|
||||
public float Confidence;
|
||||
public float AgeDays;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Память особи (объём ∝ мозгу): ограниченный набор записей о замеченном (сейчас — угрозы). Чувства
|
||||
/// пишут сюда воспринятое; со временем уверенность падает и запись забывается; ИИ использует память,
|
||||
/// когда цель не воспринимается сейчас (избегает зоны недавнего хищника). Список — managed-ссылка.
|
||||
/// </summary>
|
||||
public struct Memory : IComponent
|
||||
{
|
||||
/// <summary>Активные записи (упорядочены по добавлению; слабейшая вытесняется при переполнении).</summary>
|
||||
public List<MemoryEntry> Entries;
|
||||
|
||||
/// <summary>Сколько записей вмещает (∝ размеру мозга; 0 — память отсутствует).</summary>
|
||||
public int Capacity;
|
||||
|
||||
/// <summary>Запоминает/освежает наблюдение: близкое того же вида — обновляет, иначе добавляет (с
|
||||
/// вытеснением слабейшего при переполнении). Вне ёмкости (Capacity 0) — ничего не помнит.</summary>
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
|
||||
public static class AnimalSkills
|
||||
{
|
||||
/// <summary>Добыча корма: эффективность выпаса/поедания растений.</summary>
|
||||
public const string Foraging = "Foraging";
|
||||
|
||||
/// <summary>Охота: урон по добыче.</summary>
|
||||
public const string Hunting = "Hunting";
|
||||
|
||||
/// <summary>Уклонение: скорость бегства от хищника.</summary>
|
||||
public const string Evasion = "Evasion";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Навыки особи (как нужды/способности — managed-массивы по индексам <see cref="Content.SkillSet"/>):
|
||||
/// уровни [0..1] (растут от практики, угасают без неё) и страсть к каждому навыку (скорость обучения).
|
||||
/// Есть у животных; те же навыки получат будущие люди. Добавление навыка не меняет структуру.
|
||||
/// </summary>
|
||||
public struct Skills : IComponent
|
||||
{
|
||||
/// <summary>Уровни навыков [0..1] по индексам <see cref="Content.SkillSet"/>.</summary>
|
||||
public float[] Levels;
|
||||
|
||||
/// <summary>Страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) — множитель обучения.</summary>
|
||||
public byte[] Passions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
|
||||
/// сверх грузоподъёмности (<see cref="Sim.Mass.CarryCapacity"/>) замедляет движение. Компонент появляется
|
||||
/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь.
|
||||
/// </summary>
|
||||
public struct Carrier : IComponent
|
||||
{
|
||||
/// <summary>Суммарная масса переносимого груза (кг).</summary>
|
||||
public float CarriedKg;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public struct ThoughtInstance
|
||||
{
|
||||
/// <summary>Деф мысли (сдвиг настроения и длительность).</summary>
|
||||
public Content.ThoughtDef Def;
|
||||
|
||||
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||
public float RemainingDays;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Настроение особи (фаза A8): значение [0,1] и активные событийные мысли. Считается
|
||||
/// <see cref="Sim.AnimalMoodSystem"/> из непрерывного фона (нужды/боль) и суммы мыслей. Есть только у
|
||||
/// тира 4+ (<see cref="Active"/> по размеру мозга) — у примитивных настроения нет. Низкое настроение
|
||||
/// влияет на поведение (стресс не даёт спариваться) и видно в инспекторе. Список мыслей — managed-ссылка
|
||||
/// (как <c>AnimalNeeds.Values</c>): события добавляют мысли через <see cref="Add"/>.
|
||||
/// </summary>
|
||||
public struct Mood : IComponent
|
||||
{
|
||||
/// <summary>Текущее настроение [0,1]; у неактивного (тир < 4) держится нейтральным.</summary>
|
||||
public float Value;
|
||||
|
||||
/// <summary>Есть ли у особи настроение (гейт тиром мозга) — иначе фон/мысли не считаются.</summary>
|
||||
public bool Active;
|
||||
|
||||
/// <summary>Активные событийные мысли (затухают со временем).</summary>
|
||||
public List<ThoughtInstance> Thoughts;
|
||||
|
||||
/// <summary>
|
||||
/// Вешает событийную мысль (или обновляет её длительность, если уже висит — без накопления
|
||||
/// дубликатов). Вызывается из систем-источников событий (роды, спаривание, утоление жажды).
|
||||
/// </summary>
|
||||
public readonly void Add(Content.ThoughtDef? def)
|
||||
{
|
||||
if (Thoughts is null || def is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Thoughts.Count; i++)
|
||||
{
|
||||
if (Thoughts[i].Def == def)
|
||||
{
|
||||
var refreshed = Thoughts[i];
|
||||
refreshed.RemainingDays = def.DurationDays;
|
||||
Thoughts[i] = refreshed;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Thoughts.Add(new ThoughtInstance { Def = def, RemainingDays = def.DurationDays });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
|
||||
/// срока вынашивания и размер помёта. По истечении срока <see cref="Sim.AnimalPregnancySystem"/>
|
||||
/// рожает детёнышей через <see cref="Genome.Breed"/> и снимает компонент.
|
||||
/// </summary>
|
||||
public struct Pregnant : IComponent
|
||||
{
|
||||
/// <summary>Геном отца на момент зачатия.</summary>
|
||||
public Genome FatherGenome;
|
||||
|
||||
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||
public float DueInDays;
|
||||
|
||||
/// <summary>Сколько детёнышей родится.</summary>
|
||||
public int Litter;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,720 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Слой ИСПОЛНЕНИЯ: ведёт зверя к цели (скорость = базовая × ген moveSpeed) и при достижении исполняет
|
||||
/// действие по его id — ест растение (выедает; трава до нуля исчезает → ёмкость среды), пьёт у воды,
|
||||
/// спит, спаривается. Восполняемую нужду находит по действию через <see cref="NeedSet"/>. Структурные
|
||||
/// изменения (гибель растений, беременность) применяются после прохода.
|
||||
/// </summary>
|
||||
public sealed class AnimalActionSystem : BaseSystem
|
||||
{
|
||||
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
|
||||
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 const float IdleRestFactor = 0.3f; // доля скорости восстановления сил при холостом отдыхе
|
||||
private const float DigForageFactor = 0.25f; // доля сытости с копания относительно нормальной кормёжки
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly PlantSet _plants;
|
||||
private readonly AnimalSet _animalSet;
|
||||
private readonly NeedSet _needs;
|
||||
private readonly SkillSet _skills;
|
||||
private readonly ThoughtSet _thoughts;
|
||||
private readonly WorldEvents _events;
|
||||
private readonly GameClock _clock;
|
||||
private readonly float _secondsPerDay;
|
||||
private readonly int _cellSize;
|
||||
private readonly RectF _bounds;
|
||||
private readonly HediffDef? _bleeding;
|
||||
private readonly HediffDef? _poisoned;
|
||||
private readonly Random _rng;
|
||||
|
||||
// Кэш индексов навыков-эффектов (−1, если навык не определён модом — тогда эффекта/XP нет).
|
||||
private readonly int _forageSkill;
|
||||
private readonly int _huntSkill;
|
||||
private readonly int _evadeSkill;
|
||||
|
||||
private readonly ArchetypeQuery<
|
||||
AnimalBrain,
|
||||
AnimalNeeds,
|
||||
AnimalOrganism,
|
||||
Transform2D,
|
||||
Health
|
||||
> _query;
|
||||
private readonly List<Entity> _eaten = [];
|
||||
private readonly HashSet<Entity> _eatenSet = []; // O(1)-проверка членства (порядок даёт _eaten)
|
||||
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
|
||||
private readonly HashSet<Entity> _consumedSet = []; // O(1)-проверка членства (порядок даёт _consumed)
|
||||
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
|
||||
private readonly HashSet<Entity> _killsSet = []; // O(1)-проверка членства (порядок даёт _kills)
|
||||
private readonly List<(Entity Self, Entity Partner)> _matings = [];
|
||||
private readonly HashSet<(int, int)> _matedPairs = []; // обработанные пары за проход (дедуп (a,b)/(b,a))
|
||||
private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория)
|
||||
|
||||
public AnimalActionSystem(
|
||||
EntityStore store,
|
||||
PlantSet plants,
|
||||
AnimalSet animals,
|
||||
NeedSet needs,
|
||||
SkillSet skills,
|
||||
ThoughtSet thoughts,
|
||||
WorldEvents events,
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
int cellSize,
|
||||
RectF bounds,
|
||||
HediffDef? bleeding,
|
||||
HediffDef? poisoned,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
_plants = plants;
|
||||
_animalSet = animals;
|
||||
_needs = needs;
|
||||
_skills = skills;
|
||||
_forageSkill = skills.IndexOf(AnimalSkills.Foraging);
|
||||
_huntSkill = skills.IndexOf(AnimalSkills.Hunting);
|
||||
_evadeSkill = skills.IndexOf(AnimalSkills.Evasion);
|
||||
_thoughts = thoughts;
|
||||
_events = events;
|
||||
_clock = clock;
|
||||
_secondsPerDay = secondsPerDay;
|
||||
_cellSize = cellSize;
|
||||
_bounds = bounds;
|
||||
_bleeding = bleeding;
|
||||
_poisoned = poisoned;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
var seconds = _clock.DeltaTime;
|
||||
if (seconds <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var days = seconds / _secondsPerDay;
|
||||
_eaten.Clear();
|
||||
_eatenSet.Clear();
|
||||
_consumed.Clear();
|
||||
_consumedSet.Clear();
|
||||
_kills.Clear();
|
||||
_killsSet.Clear();
|
||||
_matings.Clear();
|
||||
_ingested.Clear();
|
||||
|
||||
foreach (
|
||||
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
|
||||
)
|
||||
{
|
||||
var b = brains.Span;
|
||||
var n = needsChunk.Span;
|
||||
var o = organisms.Span;
|
||||
var t = transforms.Span;
|
||||
var hh = healths.Span;
|
||||
for (var i = 0; i < b.Length; i++)
|
||||
{
|
||||
ref var brain = ref b[i];
|
||||
var values = n[i].Values;
|
||||
ref var pos = ref t[i].Position;
|
||||
var def = _animalSet[o[i].Species].Def;
|
||||
// Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее.
|
||||
var moving = hh[i].State.Capacity(AnimalCapacities.Moving);
|
||||
var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * moving;
|
||||
// Питание (челюсть) масштабирует укус, пищеварение (желудок) — усвоение: их произведение
|
||||
// даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
|
||||
var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
|
||||
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
|
||||
var self = entities.EntityAt(i);
|
||||
self.TryGetComponent<Skills>(out var skills); // один лукап на особь (эффекты + XP)
|
||||
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
|
||||
if (self.TryGetComponent<Carrier>(out var carrier) && carrier.CarriedKg > 0f)
|
||||
{
|
||||
var sp = _animalSet[o[i].Species];
|
||||
var stageScale = entities.EntityAt(i).TryGetComponent<AnimalGrowth>(out var gr)
|
||||
? sp.Stages[Math.Clamp(gr.Stage, 0, sp.Stages.Length - 1)].Scale
|
||||
: 1f;
|
||||
var capacity = Mass.CarryCapacity(
|
||||
Mass.OfAnimal(sp.Def, o[i].Traits, stageScale),
|
||||
moving
|
||||
);
|
||||
speed *= Mass.LoadSpeedFactor(carrier.CarriedKg, capacity);
|
||||
}
|
||||
|
||||
switch (brain.Action)
|
||||
{
|
||||
case AnimalActions.Sleep:
|
||||
Refill(values, AnimalActions.Sleep, days);
|
||||
break;
|
||||
|
||||
case AnimalActions.Eat:
|
||||
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
||||
{
|
||||
// Навык добычи корма: опытный форажир извлекает больше сытости; растёт от практики.
|
||||
var forageF = SkillFactor(_forageSkill, skills);
|
||||
Refill(values, AnimalActions.Eat, days, foodEff * forageF);
|
||||
Graze(
|
||||
brain.TargetPlant,
|
||||
days,
|
||||
def.ForageBiteDays * MathF.Max(0.2f, eating)
|
||||
);
|
||||
// Коэволюция: защита растения бьёт по поедателю (яд/шипы).
|
||||
ApplyPlantDefense(
|
||||
brain.TargetPlant,
|
||||
hh[i].State,
|
||||
o[i].Traits.ToxinTolerance,
|
||||
days
|
||||
);
|
||||
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
|
||||
TryIngestSeed(brain.TargetPlant, self);
|
||||
GrantXp(_forageSkill, skills, days);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AnimalActions.Drink:
|
||||
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
||||
{
|
||||
// Утоление сильной жажды — событийная мысль настроения (A8); уровень берём
|
||||
// ДО восполнения, чтобы радость была от облегчения, а не от каждого глотка.
|
||||
var thirst = _needs.NeedForAction(AnimalActions.Drink);
|
||||
var parched =
|
||||
thirst >= 0
|
||||
&& thirst < values.Length
|
||||
&& values[thirst] < ThirstReliefBelow;
|
||||
Refill(values, AnimalActions.Drink, days);
|
||||
if (parched)
|
||||
{
|
||||
entities
|
||||
.EntityAt(i)
|
||||
.GetComponent<Mood>()
|
||||
.Add(_thoughts.Get(AnimalThoughts.QuenchedThirst));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AnimalActions.Mate:
|
||||
if (
|
||||
brain.TargetMate >= 0
|
||||
&& _store.TryGetEntityById(brain.TargetMate, out var partner)
|
||||
&& !partner.IsNull
|
||||
&& partner.HasComponent<Transform2D>()
|
||||
)
|
||||
{
|
||||
var partnerPos = partner.GetComponent<Transform2D>().Position;
|
||||
if (MoveTo(ref pos, partnerPos, speed * seconds))
|
||||
{
|
||||
_matings.Add((entities.EntityAt(i), partner));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AnimalActions.Hunt:
|
||||
{
|
||||
// Сила удара: размер тела особи (со стадией) относительно эталона вида — крупная/
|
||||
// взрослая особь бьёт сильнее, детёныш слабее. Дальше домножается на навык охоты.
|
||||
var spH = _animalSet[o[i].Species];
|
||||
var stageScaleH = self.TryGetComponent<AnimalGrowth>(out var grH)
|
||||
? spH.Stages[Math.Clamp(grH.Stage, 0, spH.Stages.Length - 1)].Scale
|
||||
: 1f;
|
||||
var strength = MathF.Max(
|
||||
0.2f,
|
||||
o[i].Traits.BodySize * stageScaleH / spH.RefBodySize
|
||||
);
|
||||
// Навык охоты: опытный хищник бьёт сильнее; растёт от практики.
|
||||
Hunt(
|
||||
ref brain,
|
||||
ref pos,
|
||||
values,
|
||||
def,
|
||||
speed * seconds,
|
||||
seconds,
|
||||
days,
|
||||
foodEff,
|
||||
SkillFactor(_huntSkill, skills),
|
||||
strength,
|
||||
self,
|
||||
o[i].Species
|
||||
);
|
||||
GrantXp(_huntSkill, skills, days);
|
||||
break;
|
||||
}
|
||||
|
||||
case AnimalActions.Flee:
|
||||
// Навык уклонения: опытная жертва убегает быстрее; растёт от практики бегства.
|
||||
MoveTo(
|
||||
ref pos,
|
||||
brain.Target,
|
||||
speed * FleeSpeedMult * SkillFactor(_evadeSkill, skills) * seconds
|
||||
);
|
||||
GrantXp(_evadeSkill, skills, days);
|
||||
break;
|
||||
|
||||
case AnimalActions.Rest:
|
||||
// Отдых на месте: понемногу восстанавливает силы (меньше, чем полноценный сон).
|
||||
Refill(values, AnimalActions.Sleep, days, IdleRestFactor);
|
||||
break;
|
||||
|
||||
case AnimalActions.Dig:
|
||||
// Копание в земле: травоядные/всеядные выкапывают корешки/личинок — немного сытости.
|
||||
if (o[i].EatsPlants)
|
||||
{
|
||||
Refill(values, AnimalActions.Eat, days, DigForageFactor * foodEff);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case AnimalActions.Wallow:
|
||||
// Валяние в грязи: стоит на месте у воды (комфорт/терморегуляция) — без эффекта на нужды.
|
||||
break;
|
||||
|
||||
default: // Wander — блуждание к цели
|
||||
MoveTo(ref pos, brain.Target, speed * seconds);
|
||||
break;
|
||||
}
|
||||
|
||||
pos.X = Math.Clamp(pos.X, _bounds.Left, _bounds.Right);
|
||||
pos.Y = Math.Clamp(pos.Y, _bounds.Top, _bounds.Bottom);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var plant in _eaten)
|
||||
{
|
||||
plant.DeleteEntity();
|
||||
}
|
||||
|
||||
foreach (var corpse in _consumed)
|
||||
{
|
||||
if (!corpse.IsNull)
|
||||
{
|
||||
corpse.DeleteEntity(); // труп доеден до нуля
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prey in _kills)
|
||||
{
|
||||
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
|
||||
}
|
||||
|
||||
foreach (var (eater, seed) in _ingested)
|
||||
{
|
||||
if (!eater.IsNull && !eater.HasComponent<GutSeed>())
|
||||
{
|
||||
eater.AddComponent(seed); // структурное добавление — после прохода
|
||||
}
|
||||
}
|
||||
|
||||
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).
|
||||
// Минимальная видимая длительность красной вспышки урона у жертвы (реального времени).
|
||||
private const float DamageFlashSeconds = 0.35f;
|
||||
|
||||
private void Hunt(
|
||||
ref AnimalBrain brain,
|
||||
ref Vector2 pos,
|
||||
float[] values,
|
||||
AnimalDef def,
|
||||
float step,
|
||||
float seconds,
|
||||
float days,
|
||||
float foodEff,
|
||||
float huntFactor,
|
||||
float strength,
|
||||
Entity hunter,
|
||||
int hunterSpecies
|
||||
)
|
||||
{
|
||||
if (
|
||||
brain.TargetPrey < 0
|
||||
|| !_store.TryGetEntityById(brain.TargetPrey, out var target)
|
||||
|| target.IsNull
|
||||
|| !target.HasComponent<Transform2D>()
|
||||
)
|
||||
{
|
||||
return; // цель исчезла — действие пересмотрят на следующем решении
|
||||
}
|
||||
|
||||
var targetPos = target.GetComponent<Transform2D>().Position;
|
||||
var range = def.AttackRangeCells * _cellSize;
|
||||
var inRange = Vector2.DistanceSquared(pos, targetPos) <= range * range;
|
||||
|
||||
if (brain.TargetIsCorpse)
|
||||
{
|
||||
if (!target.HasComponent<Corpse>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inRange)
|
||||
{
|
||||
MoveTo(ref pos, targetPos, step);
|
||||
return;
|
||||
}
|
||||
|
||||
ref var corpse = ref target.GetComponent<Corpse>();
|
||||
Refill(values, AnimalActions.Eat, days, foodEff);
|
||||
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
|
||||
if (corpse.Meat <= 0f && _consumedSet.Add(target))
|
||||
{
|
||||
_consumed.Add(target);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Живая добыча.
|
||||
if (!target.HasComponent<Health>() || !target.HasComponent<AnimalOrganism>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Дискретные удары (как в RimWorld): кулдаун идёт и при сближении — первый контакт бьёт сразу.
|
||||
brain.AttackCooldown -= seconds;
|
||||
if (!inRange)
|
||||
{
|
||||
MoveTo(ref pos, targetPos, step);
|
||||
return;
|
||||
}
|
||||
|
||||
if (brain.AttackCooldown > 0f)
|
||||
{
|
||||
return; // в контакте, но удар ещё на перезарядке
|
||||
}
|
||||
|
||||
brain.AttackCooldown = MathF.Max(0.05f, def.AttackIntervalSec);
|
||||
|
||||
var preyHealth = target.GetComponent<Health>().State;
|
||||
if (preyHealth is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Сила удара = база дефа × сила особи (размер×стадия) × навык охоты; кровопотеря/кровотечение — ×силу.
|
||||
var damage = def.AttackDamage * strength * huntFactor;
|
||||
preyHealth.ApplyInjury(
|
||||
damage,
|
||||
def.AttackBloodLoss * strength,
|
||||
_bleeding,
|
||||
def.AttackBleed * strength,
|
||||
_rng
|
||||
);
|
||||
|
||||
// Красная вспышка жертвы (презентация) + удар в боевую хронику обоих участников.
|
||||
if (target.HasComponent<DamageFlash>())
|
||||
{
|
||||
target.GetComponent<DamageFlash>().Seconds = DamageFlashSeconds;
|
||||
}
|
||||
|
||||
_events.Struck(
|
||||
hunter,
|
||||
hunterSpecies,
|
||||
target,
|
||||
target.GetComponent<AnimalOrganism>().Species,
|
||||
damage
|
||||
);
|
||||
|
||||
if (
|
||||
preyHealth.BloodLevel <= 0f
|
||||
|| preyHealth.Capacity(AnimalCapacities.Consciousness) <= 0.01f
|
||||
)
|
||||
{
|
||||
if (_killsSet.Add(target))
|
||||
{
|
||||
_kills.Add(target); // добита — смерть после прохода (структурное изменение)
|
||||
_events.Killed(
|
||||
hunter,
|
||||
hunterSpecies,
|
||||
target.GetComponent<AnimalOrganism>().Species
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay × эффективность
|
||||
// (питание/пищеварение для еды — слабая челюсть/больной желудок дают меньше сытости с того же корма).
|
||||
private void Refill(float[] values, string action, float days, float efficiency = 1f)
|
||||
{
|
||||
var index = _needs.NeedForAction(action);
|
||||
if (index < 0 || values is null || index >= values.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
values[index] = Math.Clamp(
|
||||
values[index] + _needs[index].FeedPerDay * days * efficiency,
|
||||
0f,
|
||||
1f
|
||||
);
|
||||
}
|
||||
|
||||
// Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта).
|
||||
private static float SkillFactor(int idx, in Skills s)
|
||||
{
|
||||
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
return 0.7f + 0.6f * s.Levels[idx];
|
||||
}
|
||||
|
||||
// Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача).
|
||||
private void GrantXp(int idx, in Skills s, float days)
|
||||
{
|
||||
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var passion = s.Passions is not null && idx < s.Passions.Length ? s.Passions[idx] : (byte)0;
|
||||
var gain =
|
||||
_skills[idx].LearnRate
|
||||
* SkillSet.PassionMultiplier(passion)
|
||||
* days
|
||||
* (1f - s.Levels[idx]);
|
||||
s.Levels[idx] = Math.Clamp(s.Levels[idx] + gain, 0f, 1f);
|
||||
}
|
||||
|
||||
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
|
||||
private static bool MoveTo(ref Vector2 pos, Vector2 target, float step)
|
||||
{
|
||||
var delta = target - pos;
|
||||
var dist = delta.Length();
|
||||
if (dist <= step || dist < 0.001f)
|
||||
{
|
||||
pos = dist < 0.001f ? pos : target;
|
||||
return true;
|
||||
}
|
||||
|
||||
pos += delta / dist * step;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Выедание: убавляет рост растения; траву (без ствола), выеденную до нуля, помечает на гибель.
|
||||
private void Graze(int plantId, float days, float bitePerDay)
|
||||
{
|
||||
if (
|
||||
plantId < 0
|
||||
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||
|| plant.IsNull
|
||||
|| !plant.HasComponent<PlantGrowth>()
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ref var grow = ref plant.GetComponent<PlantGrowth>();
|
||||
grow.AgeDays = MathF.Max(0f, grow.AgeDays - bitePerDay * days);
|
||||
var isGrass = _plants[grow.Species].Def.TrunkRadiusCells <= 0f;
|
||||
if (isGrass && grow.AgeDays <= GrazeKillAge && _eatenSet.Add(plant))
|
||||
{
|
||||
_eaten.Add(plant);
|
||||
}
|
||||
}
|
||||
|
||||
// Доза яда за день укуса при токсичности 1 и нулевой устойчивости (масштабируется тяжести Poisoned).
|
||||
private const float PoisonDosePerDay = 1.6f;
|
||||
|
||||
// Порог шипов, выше которого укус о растение травмирует, и масштабы урона/кровопотери от шипов.
|
||||
private const float ThornThreshold = 0.2f;
|
||||
private const float ThornPartDamage = 4f;
|
||||
private const float ThornBloodLoss = 0.02f;
|
||||
|
||||
// Коэволюция: при поедании растение «отвечает» — токсичность отравляет (тем сильнее, чем ниже
|
||||
// устойчивость зверя к яду), шипы наносят лёгкую травму. Доза яда копится в хедифе Poisoned; если
|
||||
// зверь ест яд быстрее, чем выводит, тяжесть дойдёт до летальной. Детерминированно (общий _rng).
|
||||
private void ApplyPlantDefense(
|
||||
int plantId,
|
||||
HealthState? health,
|
||||
float toxinTolerance,
|
||||
float days
|
||||
)
|
||||
{
|
||||
if (
|
||||
health is null
|
||||
|| plantId < 0
|
||||
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||
|| plant.IsNull
|
||||
|| !plant.HasComponent<PlantOrganism>()
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ref readonly var traits = ref plant.GetComponent<PlantOrganism>().Traits;
|
||||
|
||||
var poisoned = false;
|
||||
if (_poisoned is not null && traits.Toxicity > 0f)
|
||||
{
|
||||
var dose = traits.Toxicity * (1f - Math.Clamp(toxinTolerance, 0f, 1f));
|
||||
if (dose > 0f)
|
||||
{
|
||||
health.Intensify(_poisoned, dose * PoisonDosePerDay * days);
|
||||
poisoned = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (traits.Thorns > ThornThreshold)
|
||||
{
|
||||
// ApplyInjury сам пересчитывает способности (учтёт и свежий яд).
|
||||
health.ApplyInjury(
|
||||
traits.Thorns * ThornPartDamage * days,
|
||||
traits.Thorns * ThornBloodLoss * days,
|
||||
_bleeding,
|
||||
traits.Thorns * 0.15f * days,
|
||||
_rng
|
||||
);
|
||||
}
|
||||
else if (poisoned)
|
||||
{
|
||||
health.RecomputeCapacities(); // отравление меняет capacity-моды — применить сразу
|
||||
}
|
||||
}
|
||||
|
||||
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление —
|
||||
// после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
|
||||
private void ApplyMatings()
|
||||
{
|
||||
var mateNeed = _needs.NeedForAction(AnimalActions.Mate);
|
||||
_matedPairs.Clear();
|
||||
foreach (var (a, b) in _matings)
|
||||
{
|
||||
if (
|
||||
a.IsNull
|
||||
|| b.IsNull
|
||||
|| !a.HasComponent<AnimalOrganism>()
|
||||
|| !b.HasComponent<AnimalOrganism>()
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Оба партнёра независимо ставят себя в очередь, так что пара приходит дважды — (a,b) и (b,a).
|
||||
// Дедупим по неупорядоченному ключу: иначе ResetMating/мысль «спаривание» применяются повторно.
|
||||
var pairKey = a.Id < b.Id ? (a.Id, b.Id) : (b.Id, a.Id);
|
||||
if (!_matedPairs.Add(pairKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var aMale = a.GetComponent<AnimalOrganism>().IsMale;
|
||||
var bMale = b.GetComponent<AnimalOrganism>().IsMale;
|
||||
if (aMale == bMale)
|
||||
{
|
||||
continue; // один пол — не пара (страховка)
|
||||
}
|
||||
|
||||
var mother = aMale ? b : a;
|
||||
var father = aMale ? a : b;
|
||||
if (!mother.HasComponent<Pregnant>())
|
||||
{
|
||||
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
|
||||
// Плодовитость падает со старостью (тот же фактор сенесценса): дряхлая самка приносит
|
||||
// меньший помёт, а после порога пострепродуктивного возраста уже не зачинает.
|
||||
var momAge = mother.HasComponent<AnimalGrowth>()
|
||||
? mother.GetComponent<AnimalGrowth>().AgeDays
|
||||
: 0f;
|
||||
var ageFrac = mom.Traits.Lifespan > 0f ? momAge / mom.Traits.Lifespan : 0f;
|
||||
if (ageFrac <= PostReproductiveFrac)
|
||||
{
|
||||
var fert = AnimalHealthSystem.SenescenceFactor(momAge, mom.Traits.Lifespan);
|
||||
mother.AddComponent(
|
||||
new Pregnant
|
||||
{
|
||||
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
|
||||
DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
|
||||
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize * fert)),
|
||||
}
|
||||
);
|
||||
_events.Pregnant(mother, mom.Species);
|
||||
}
|
||||
}
|
||||
|
||||
ResetMating(a, mateNeed);
|
||||
ResetMating(b, mateNeed);
|
||||
// Спаривание — положительная событийная мысль обоим партнёрам (A8).
|
||||
var mated = _thoughts.Get(AnimalThoughts.Mated);
|
||||
a.GetComponent<Mood>().Add(mated);
|
||||
b.GetComponent<Mood>().Add(mated);
|
||||
_events.Mated(
|
||||
a,
|
||||
a.GetComponent<AnimalOrganism>().Species,
|
||||
b,
|
||||
b.GetComponent<AnimalOrganism>().Species
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetMating(Entity entity, int mateNeed)
|
||||
{
|
||||
if (mateNeed < 0 || !entity.HasComponent<AnimalNeeds>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var values = entity.GetComponent<AnimalNeeds>().Values;
|
||||
if (values is not null && mateNeed < values.Length)
|
||||
{
|
||||
values[mateNeed] = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Id поведений-исполнителей (реестр действий ИИ). Дефы нужд (<see cref="NeedDef.Action"/>) ссылаются на
|
||||
/// них; <see cref="AnimalDecisionSystem"/> выбирает действие, <see cref="AnimalActionSystem"/> исполняет.
|
||||
/// Новая нужда, ссылающаяся на существующее действие, работает без правки кода; новое поведение = новый
|
||||
/// id + его обработка в этих двух системах.
|
||||
/// </summary>
|
||||
public static class AnimalActions
|
||||
{
|
||||
public const string Wander = "Wander";
|
||||
public const string Eat = "Eat";
|
||||
public const string Drink = "Drink";
|
||||
public const string Sleep = "Sleep";
|
||||
public const string Mate = "Mate";
|
||||
|
||||
/// <summary>Охота: преследовать живую добычу и кусать (урон) либо есть труп — утоляет голод хищника.</summary>
|
||||
public const string Hunt = "Hunt";
|
||||
|
||||
/// <summary>Бегство: уходить от ближайшего хищника (реакция жертвы, важнее прочих нужд).</summary>
|
||||
public const string Flee = "Flee";
|
||||
|
||||
// — Холостые (idle) активности: чем зверь занят, когда нет срочных нужд. Выбираются внутри ветки
|
||||
// Wander с фиксированной длительностью (см. AnimalDecisionSystem) — придают поведению разнообразие.
|
||||
|
||||
/// <summary>Отдых: стоит/лежит на месте, понемногу восстанавливая силы.</summary>
|
||||
public const string Rest = "Rest";
|
||||
|
||||
/// <summary>Копание в земле в поисках корня/личинок (травоядные/всеядные) — даёт чуть сытости.</summary>
|
||||
public const string Dig = "Dig";
|
||||
|
||||
/// <summary>Валяние в грязи у воды (комфорт/терморегуляция) — стоит на месте у кромки.</summary>
|
||||
public const string Wallow = "Wallow";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
|
||||
/// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
|
||||
/// <see cref="Sprite"/> — граница sim/presentation цела.
|
||||
/// </summary>
|
||||
public sealed class AnimalAppearanceSystem(NeedSet needs)
|
||||
: QuerySystem<AnimalNeeds, AnimalOrganism, Sprite>
|
||||
{
|
||||
private const float MinBrightness = 0.5f;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
foreach (var (needsChunk, organisms, sprites, _) in Query.Chunks)
|
||||
{
|
||||
var n = needsChunk.Span;
|
||||
var o = organisms.Span;
|
||||
var s = sprites.Span;
|
||||
for (var i = 0; i < n.Length; i++)
|
||||
{
|
||||
var values = n[i].Values;
|
||||
var worst = 1f;
|
||||
if (values is not null)
|
||||
{
|
||||
for (var k = 0; k < needs.Count && k < values.Length; k++)
|
||||
{
|
||||
if (needs.IsDeplete(k))
|
||||
{
|
||||
worst = MathF.Min(worst, values[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var brightness = MinBrightness + (1f - MinBrightness) * Math.Clamp(worst, 0f, 1f);
|
||||
s[i].Color = AnimalFactory.FurTint(o[i].Traits) * brightness;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// «Раскачка» при ходьбе (презентация): идущий зверь покачивается из стороны в сторону (наклон спрайта
|
||||
/// через <see cref="Transform2D.Rotation"/>), стоящий — плавно выпрямляется. Делает движение живее, не
|
||||
/// затрагивая симуляцию (она вращение не использует). Фаза покачивания идёт по игровому времени (стоп на
|
||||
/// паузе, быстрее на ускорении) и сдвинута по id особи — звери качаются вразнобой. Граница sim/presentation
|
||||
/// цела: система пишет только поворот трансформа.
|
||||
/// </summary>
|
||||
public sealed class AnimalBobSystem(GameClock clock) : QuerySystem<AnimalBrain, Transform2D>
|
||||
{
|
||||
private const float SwaySpeed = 9f; // скорость набега фазы (рад/сек игрового времени)
|
||||
private const float SwayAmplitude = 0.08f; // амплитуда наклона (рад ≈ 4.6°)
|
||||
private const float MinMoveSq = 0.5f * 0.5f; // ближе к цели — считаем стоящим
|
||||
private const float UprightDecay = 0.85f; // как быстро выпрямляется стоя (за кадр)
|
||||
|
||||
private float _phase;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var dt = clock.DeltaTime;
|
||||
if (dt <= 0f)
|
||||
{
|
||||
return; // пауза — не качаем и не выпрямляем
|
||||
}
|
||||
|
||||
_phase += dt * SwaySpeed;
|
||||
foreach (var (brains, transforms, entities) in Query.Chunks)
|
||||
{
|
||||
var b = brains.Span;
|
||||
var t = transforms.Span;
|
||||
for (var i = 0; i < b.Length; i++)
|
||||
{
|
||||
ref var rot = ref t[i].Rotation;
|
||||
if ((b[i].Target - t[i].Position).LengthSquared() > MinMoveSq)
|
||||
{
|
||||
var offset = entities.EntityAt(i).Id * 0.7f; // десинхрон между особями
|
||||
rot = MathF.Sin(_phase + offset) * SwayAmplitude;
|
||||
}
|
||||
else
|
||||
{
|
||||
rot *= UprightDecay; // у цели/стоя — плавно к вертикали
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// «Мозг» зверя: выбранное действие (id поведения), его цель и таймер до пересмотра. Решение принимает
|
||||
/// <see cref="AnimalDecisionSystem"/> (движковый <see cref="UtilityAi{TContext}"/>), исполняет
|
||||
/// <see cref="AnimalActionSystem"/>.
|
||||
/// </summary>
|
||||
public struct AnimalBrain : IComponent
|
||||
{
|
||||
/// <summary>Id текущего действия (см. <see cref="AnimalActions"/>).</summary>
|
||||
public string Action;
|
||||
|
||||
/// <summary>Куда идти (позиция растения/воды/партнёра/точки блуждания).</summary>
|
||||
public Vector2 Target;
|
||||
|
||||
/// <summary>Id растения-цели для <see cref="AnimalActions.Eat"/>; -1 — нет.</summary>
|
||||
public int TargetPlant;
|
||||
|
||||
/// <summary>Id партнёра для <see cref="AnimalActions.Mate"/>; -1 — нет.</summary>
|
||||
public int TargetMate;
|
||||
|
||||
/// <summary>Id цели для <see cref="AnimalActions.Hunt"/> — живой добычи или трупа; -1 — нет.</summary>
|
||||
public int TargetPrey;
|
||||
|
||||
/// <summary>Цель — труп (падаль), а не живая добыча: при достижении едим, а не атакуем.</summary>
|
||||
public bool TargetIsCorpse;
|
||||
|
||||
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
||||
public float DecideIn;
|
||||
|
||||
/// <summary>Игровые секунды до следующего удара по добыче (кулдаун дискретных атак, как в RimWorld).</summary>
|
||||
public float AttackCooldown;
|
||||
|
||||
/// <summary>Текущее направление блуждания (коррелированное — плавно дрейфует, не дёргается каждый раз).</summary>
|
||||
public Vector2 Heading;
|
||||
|
||||
/// <summary>Игровое время (TotalTime), до которого длится текущая холостая активность (отдых/копание/…).</summary>
|
||||
public float ActivityUntil;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Снимок нужд зверя для соображений utility-выбора (доступ по индексу нужды) с гейтом интеллектом
|
||||
/// (фаза A7): нужда, чей <see cref="NeedDef.MinBrain"/> выше эффективного интеллекта особи
|
||||
/// (brainSize × Consciousness), в выбор не входит.
|
||||
/// </summary>
|
||||
public readonly struct AnimalContext(
|
||||
float[] values,
|
||||
NeedSet needs,
|
||||
float intelligence,
|
||||
int overrideIndex = -1,
|
||||
float overrideValue = 0f
|
||||
)
|
||||
{
|
||||
private readonly float[] _values = values;
|
||||
private readonly NeedSet _needs = needs;
|
||||
private readonly float _intelligence = intelligence;
|
||||
|
||||
// Переопределение значения одной нужды (стиль питания: «эффективный голод» с гистерезисом —
|
||||
// см. AnimalDecisionSystem). Override имеет приоритет над сырым значением и над гейтом мозга.
|
||||
private readonly int _overrideIndex = overrideIndex;
|
||||
private readonly float _overrideValue = overrideValue;
|
||||
|
||||
/// <summary>
|
||||
/// Значение нужды по индексу. Если для этой нужды задан override (стиль питания) — он и возвращается.
|
||||
/// Если её порог мозга выше интеллекта особи — нужда недоступна и возвращается её «не-срочный» край
|
||||
/// (deplete → 1 «сыта», drive → 0 «без влечения»), чтобы соображение дало ~0 и действие не выбралось.
|
||||
/// Индекс вне диапазона → 0.
|
||||
/// </summary>
|
||||
public float Get(int index)
|
||||
{
|
||||
if (index == _overrideIndex)
|
||||
{
|
||||
return _overrideValue;
|
||||
}
|
||||
|
||||
if (index >= _values.Length)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (_needs[index].MinBrain > _intelligence)
|
||||
{
|
||||
return _needs.IsDrive(index) ? 0f : 1f;
|
||||
}
|
||||
|
||||
return _values[index];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Разворот спрайта по направлению движения (как в RimWorld): зверь смотрит туда, куда идёт. Восток —
|
||||
/// базовая текстура, запад — её зеркало (флип по X), север/юг — отдельные текстуры (если есть у вида).
|
||||
/// Направление берётся из цели «мозга» (<see cref="AnimalBrain.Target"/>) относительно позиции; стоящий
|
||||
/// зверь сохраняет прежний разворот. При смене региона пересчитывает центр-origin и масштаб (текстуры
|
||||
/// направлений могут отличаться шириной). Презентация (читает sim, пишет только Sprite/Transform-scale).
|
||||
/// </summary>
|
||||
public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize)
|
||||
: QuerySystem<AnimalBrain, AnimalOrganism, AnimalGrowth, Transform2D, Sprite>
|
||||
{
|
||||
private const float MinMove = 0.5f; // короче — считаем стоящим, разворот не трогаем
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
foreach (var (brains, organisms, growths, transforms, sprites, _) in Query.Chunks)
|
||||
{
|
||||
var b = brains.Span;
|
||||
var o = organisms.Span;
|
||||
var g = growths.Span;
|
||||
var t = transforms.Span;
|
||||
var s = sprites.Span;
|
||||
for (var i = 0; i < b.Length; i++)
|
||||
{
|
||||
var dir = b[i].Target - t[i].Position;
|
||||
if (dir.LengthSquared() < MinMove * MinMove)
|
||||
{
|
||||
continue; // стоит/у цели — сохраняем текущий разворот
|
||||
}
|
||||
|
||||
var sp = animals[o[i].Species];
|
||||
var dirs = sp.LookDirs(g[i].Stage, o[i].IsMale);
|
||||
Texture2DRegion region;
|
||||
SpriteFlip flip;
|
||||
if (MathF.Abs(dir.X) >= MathF.Abs(dir.Y))
|
||||
{
|
||||
region = dirs.East;
|
||||
flip = dir.X < 0f ? SpriteFlip.X : SpriteFlip.None; // запад = зеркало востока
|
||||
}
|
||||
else
|
||||
{
|
||||
region = dir.Y < 0f ? dirs.North : dirs.South; // экранный Y вниз: вверх = север
|
||||
flip = SpriteFlip.None;
|
||||
}
|
||||
|
||||
ref var sprite = ref s[i];
|
||||
if (sprite.Region == region && sprite.Flip == flip)
|
||||
{
|
||||
continue; // разворот не изменился
|
||||
}
|
||||
|
||||
sprite.Region = region;
|
||||
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||
sprite.Flip = flip;
|
||||
t[i].Scale = new Vector2(
|
||||
AnimalFactory.Scale(sp, o[i].Traits, g[i].Stage, region, cellSize)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Friflo.Engine.ECS;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние роста животного (фаза A3): текущая стадия и накопленный возраст в игровых днях. Плоские
|
||||
/// данные — переход между стадиями (Baby→Juvenile→Adult→Senior), смену спрайта и размера выполняет
|
||||
/// <see cref="AnimalGrowthSystem"/>, читая пороги из генов (возраст созревания, продолжительность жизни).
|
||||
/// Зеркало <see cref="PlantGrowth"/>.
|
||||
/// </summary>
|
||||
public struct AnimalGrowth : IComponent
|
||||
{
|
||||
/// <summary>Текущая стадия: 0 Baby, 1 Juvenile, 2 Adult, 3 Senior.</summary>
|
||||
public int Stage;
|
||||
|
||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||
public float AgeDays;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
|
||||
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.
|
||||
/// </summary>
|
||||
public sealed class AnimalGrowthSystem(
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
AnimalSet animals,
|
||||
int cellSize,
|
||||
WorldEvents events
|
||||
) : QuerySystem<AnimalGrowth, AnimalOrganism, Transform2D, Sprite>
|
||||
{
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var days = clock.DeltaTime / secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (growths, organisms, transforms, sprites, entities) in Query.Chunks)
|
||||
{
|
||||
var g = growths.Span;
|
||||
var o = organisms.Span;
|
||||
var t = transforms.Span;
|
||||
var s = sprites.Span;
|
||||
for (var i = 0; i < g.Length; i++)
|
||||
{
|
||||
ref var grow = ref g[i];
|
||||
grow.AgeDays += days;
|
||||
ref readonly var org = ref o[i];
|
||||
var sp = animals[org.Species];
|
||||
var stage = AnimalFactory.StageAt(
|
||||
grow.AgeDays,
|
||||
org.Traits.MaturityAge,
|
||||
org.Traits.Lifespan,
|
||||
sp.Stages
|
||||
);
|
||||
if (stage == grow.Stage)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
grow.Stage = stage;
|
||||
var region = AnimalFactory.RegionFor(sp, stage, org.IsMale);
|
||||
ref var sprite = ref s[i];
|
||||
sprite.Region = region;
|
||||
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||
t[i].Scale = new Vector2(
|
||||
AnimalFactory.Scale(sp, org.Traits, stage, region, cellSize)
|
||||
);
|
||||
events.Grew(entities.EntityAt(i), org.Species, sp.Stages[stage].Name); // в хронику особи
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Здоровье и болезни (фаза A6): медленный тик (~игровой час). Фоновое заражение, прогресс болезней
|
||||
/// (гонка тяжесть↔иммунитет — выздоровление или смерть), пересчёт способностей с учётом хедифов. Смерть
|
||||
/// по здоровью (летальная болезнь / кровь на нуле / потеря сознания) оставляет труп. Первый реальный
|
||||
/// источник урона — оживляет каркас A5 (больные звери теряют Moving → медленнее).
|
||||
/// </summary>
|
||||
public sealed class AnimalHealthSystem : BaseSystem
|
||||
{
|
||||
private const float TickDays = 1f / 24f; // ~игровой час
|
||||
private const float BloodRecoveryPerDay = 0.25f; // восстановление крови в покое (нет кровотечений)
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly AnimalSet _animals;
|
||||
private readonly WorldEvents _events;
|
||||
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<Health, AnimalOrganism, AnimalGrowth> _query;
|
||||
private readonly List<Entity> _deaths = [];
|
||||
private float _accum;
|
||||
|
||||
// Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает
|
||||
// максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее.
|
||||
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,
|
||||
WorldEvents events,
|
||||
int cellSize,
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
Climate climate,
|
||||
HediffDef[] ambient,
|
||||
HediffDef? hypothermia,
|
||||
HediffDef? heatstroke,
|
||||
HediffDef? infection,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
_animals = animals;
|
||||
_events = events;
|
||||
_cellSize = cellSize;
|
||||
_clock = clock;
|
||||
_secondsPerDay = secondsPerDay;
|
||||
_climate = climate;
|
||||
_ambient = ambient;
|
||||
_hypothermia = hypothermia;
|
||||
_heatstroke = heatstroke;
|
||||
_infection = infection;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
|
||||
}
|
||||
|
||||
/// <summary>Множитель способностей от старения (1 — молод/расцвет, <1 — дряхлеет к концу жизни).</summary>
|
||||
public static float SenescenceFactor(float ageDays, float lifespan)
|
||||
{
|
||||
if (lifespan <= 0f)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
var frac = (ageDays / lifespan - SenescenceStart) / MathF.Max(0.01f, 1f - SenescenceStart);
|
||||
return 1f - SenescenceMax * Math.Clamp(frac, 0f, 1f);
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
_accum += _clock.DeltaTime / _secondsPerDay;
|
||||
if (_accum < TickDays)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var days = _accum;
|
||||
_accum = 0f;
|
||||
_deaths.Clear();
|
||||
|
||||
foreach (var (healths, organisms, growths, entities) in _query.Chunks)
|
||||
{
|
||||
var h = healths.Span;
|
||||
var o = organisms.Span;
|
||||
var g = growths.Span;
|
||||
for (var i = 0; i < h.Length; i++)
|
||||
{
|
||||
var state = h[i].State;
|
||||
if (state is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var disease in _ambient)
|
||||
{
|
||||
if (
|
||||
!state.Has(disease.DefName)
|
||||
&& _rng.NextSingle() < disease.AmbientPerDay * days
|
||||
)
|
||||
{
|
||||
state.Add(disease);
|
||||
}
|
||||
}
|
||||
|
||||
// Терморегуляция: вне комфорта (зависит от 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).
|
||||
var lethal = state.AdvanceHediffs(
|
||||
days,
|
||||
state.Capacity(AnimalCapacities.BloodFiltration)
|
||||
);
|
||||
// Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
|
||||
var bleed = 0f;
|
||||
foreach (var hd in state.Hediffs)
|
||||
{
|
||||
bleed += hd.Def.BloodLossPerDay * hd.Severity;
|
||||
}
|
||||
|
||||
if (bleed > 0f)
|
||||
{
|
||||
state.BloodLevel = Math.Clamp(state.BloodLevel - bleed * days, 0f, 1f);
|
||||
}
|
||||
else if (state.BloodLevel < 1f)
|
||||
{
|
||||
state.BloodLevel = Math.Clamp(
|
||||
state.BloodLevel + BloodRecoveryPerDay * days,
|
||||
0f,
|
||||
1f
|
||||
);
|
||||
}
|
||||
|
||||
// Старение: к концу жизни способности дряхлеют (медленнее/слабее, уязвимее к ранам/болезни).
|
||||
var senescence = SenescenceFactor(g[i].AgeDays, o[i].Traits.Lifespan);
|
||||
state.RecomputeCapacities(senescence);
|
||||
if (
|
||||
lethal
|
||||
|| state.BloodLevel <= 0f
|
||||
|| state.Capacity(AnimalCapacities.Consciousness) <= 0.01f
|
||||
)
|
||||
{
|
||||
_deaths.Add(entities.EntityAt(i));
|
||||
_events.Died(o[i].Species, "event.death.disease");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dead in _deaths)
|
||||
{
|
||||
AnimalFactory.Die(_store, _animals, dead, _cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие
|
||||
/// наблюдения записывает/освежает <see cref="AnimalDecisionSystem"/>; эта система только «выветривает»
|
||||
/// память, давая ей конечный срок.
|
||||
/// </summary>
|
||||
public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem<Memory>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Настроение (фаза A8): для особей тира 4+ (мозг ≥ порога) считает настроение из непрерывного фона
|
||||
/// (удовлетворённость убывающих нужд минус боль из здоровья) и суммы затухающих событийных мыслей.
|
||||
/// Мысли угасают со временем. У примитивных (тир < 4) настроения нет (Active=false, мысли сбрасываются).
|
||||
/// Гейт интеллектом — тем же порогом тира 4, что и высшие нужды (сон/секс). Низкое настроение влияет на
|
||||
/// поведение (стресс не даёт спариваться — см. <see cref="AnimalDecisionSystem"/>) и видно в консоли/инспекторе.
|
||||
/// </summary>
|
||||
public sealed class AnimalMoodSystem(GameClock clock, float secondsPerDay, NeedSet needs)
|
||||
: QuerySystem<Mood, AnimalNeeds, AnimalOrganism, Health>
|
||||
{
|
||||
/// <summary>Порог размера мозга (тир 4), с которого у особи появляется настроение.</summary>
|
||||
public const float MoodMinBrain = 0.40f;
|
||||
private const float NeutralBase = 0.35f; // настроение при полном истощении нужд (без боли/мыслей)
|
||||
private const float NeedsWeight = 0.55f; // вклад средней удовлетворённости убывающих нужд
|
||||
private const float PainWeight = 0.4f; // боль из здоровья тянет настроение вниз
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var days = clock.DeltaTime / secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (moods, needsChunk, organisms, healths, _) in Query.Chunks)
|
||||
{
|
||||
var m = moods.Span;
|
||||
var n = needsChunk.Span;
|
||||
var o = organisms.Span;
|
||||
var h = healths.Span;
|
||||
for (var i = 0; i < m.Length; i++)
|
||||
{
|
||||
ref var mood = ref m[i];
|
||||
if (o[i].Traits.BrainSize < MoodMinBrain)
|
||||
{
|
||||
mood.Active = false; // тир ниже зверя — настроения нет
|
||||
mood.Value = 0.5f;
|
||||
mood.Thoughts?.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
mood.Active = true;
|
||||
// 1) Затухание событийных мыслей + сумма их сдвигов.
|
||||
var thoughtSum = 0f;
|
||||
var list = mood.Thoughts;
|
||||
if (list is not null)
|
||||
{
|
||||
for (var k = list.Count - 1; k >= 0; k--)
|
||||
{
|
||||
var t = list[k];
|
||||
t.RemainingDays -= days;
|
||||
if (t.RemainingDays <= 0f)
|
||||
{
|
||||
list.RemoveAt(k);
|
||||
continue;
|
||||
}
|
||||
|
||||
list[k] = t;
|
||||
thoughtSum += t.Def.MoodOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Непрерывный фон: средняя удовлетворённость убывающих нужд минус боль из здоровья.
|
||||
var wellbeing = AvgDeplete(n[i].Values);
|
||||
var pain = h[i].State?.Pain ?? 0f;
|
||||
mood.Value = Math.Clamp(
|
||||
NeutralBase + NeedsWeight * wellbeing - PainWeight * pain + thoughtSum,
|
||||
0f,
|
||||
1f
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Средняя удовлетворённость убывающих нужд (голод/жажда/усталость) [0,1]; нет нужд → 1 (нейтрально).
|
||||
private float AvgDeplete(float[] values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
var sum = 0f;
|
||||
var count = 0;
|
||||
for (var k = 0; k < needs.Count && k < values.Length; k++)
|
||||
{
|
||||
if (needs.IsDeplete(k))
|
||||
{
|
||||
sum += values[k];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : 1f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Смертность (фаза A3/A5): гибель от старости (возраст превысил продолжительность жизни) или истощения
|
||||
/// (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды. Фаза A5: вместо деспавна
|
||||
/// оставляет <see cref="Corpse"/> (через <see cref="AnimalFactory.CreateCorpse"/>), затем удаляет особь.
|
||||
/// </summary>
|
||||
public sealed class AnimalMortalitySystem : BaseSystem
|
||||
{
|
||||
private const float LethalStarveDays = 3f;
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly AnimalSet _animals;
|
||||
private readonly WorldEvents _events;
|
||||
private readonly int _cellSize;
|
||||
private readonly ArchetypeQuery<AnimalGrowth, AnimalOrganism, AnimalNeeds> _query;
|
||||
private readonly List<Entity> _deaths = [];
|
||||
|
||||
public AnimalMortalitySystem(
|
||||
EntityStore store,
|
||||
AnimalSet animals,
|
||||
WorldEvents events,
|
||||
int cellSize
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
_animals = animals;
|
||||
_events = events;
|
||||
_cellSize = cellSize;
|
||||
_query = store.Query<AnimalGrowth, AnimalOrganism, AnimalNeeds>();
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
_deaths.Clear();
|
||||
foreach (var (growths, organisms, needsChunk, entities) in _query.Chunks)
|
||||
{
|
||||
var g = growths.Span;
|
||||
var o = organisms.Span;
|
||||
var n = needsChunk.Span;
|
||||
for (var i = 0; i < g.Length; i++)
|
||||
{
|
||||
var ageDeath = g[i].AgeDays > o[i].Traits.Lifespan;
|
||||
var starveDeath = n[i].Starve >= LethalStarveDays;
|
||||
if (ageDeath || starveDeath)
|
||||
{
|
||||
_deaths.Add(entities.EntityAt(i));
|
||||
_events.Died(o[i].Species, ageDeath ? "event.death.age" : "event.death.starve");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dead in _deaths)
|
||||
{
|
||||
AnimalFactory.Die(_store, _animals, dead, _cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
|
||||
/// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
|
||||
/// <see cref="AnimalNeedsSystem"/> по <see cref="NeedDef"/>, восполняются исполнением действия.
|
||||
/// </summary>
|
||||
public struct AnimalNeeds : IComponent
|
||||
{
|
||||
/// <summary>Значения нужд [0,1] по индексам <see cref="NeedSet"/>.</summary>
|
||||
public float[] Values;
|
||||
|
||||
/// <summary>Накоплено игровых дней истощения (летальная нужда на нуле); порог — смерть (A3).</summary>
|
||||
public float Starve;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Падение/рост нужд со временем (игровые дни через <see cref="GameClock"/>): deplete-нужды убывают
|
||||
/// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные
|
||||
/// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>.
|
||||
/// </summary>
|
||||
public sealed class AnimalNeedsSystem(
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
NeedSet needs,
|
||||
AnimalSet animals
|
||||
) : QuerySystem<AnimalNeeds, AnimalOrganism, Health, AnimalGrowth>
|
||||
{
|
||||
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
|
||||
private const float ToleranceMetabolicCost = 0.5f;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var days = clock.DeltaTime / secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (needsChunk, organisms, healths, growths, _) in Query.Chunks)
|
||||
{
|
||||
var n = needsChunk.Span;
|
||||
var o = organisms.Span;
|
||||
var h = healths.Span;
|
||||
var g = growths.Span;
|
||||
for (var i = 0; i < n.Length; i++)
|
||||
{
|
||||
var values = n[i].Values;
|
||||
if (values is null || values.Length < needs.Count)
|
||||
{
|
||||
continue; // защита от старого/пустого состояния
|
||||
}
|
||||
|
||||
// Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела:
|
||||
// расход ∝ масса^0.75 = (линейный размер/эталон)^2.25 (масса ∝ размер³), нормирован на
|
||||
// эталон вида → молодняк экономнее, крупные прожорливее. Эталон предпосчитан (без словаря).
|
||||
var sp = animals[o[i].Species];
|
||||
var stageScale = sp.Stages[Math.Clamp(g[i].Stage, 0, sp.Stages.Length - 1)].Scale;
|
||||
var sizeRatio = MathF.Max(
|
||||
0.01f,
|
||||
o[i].Traits.BodySize * stageScale / sp.RefBodySize
|
||||
);
|
||||
var metabolism =
|
||||
MathF.Max(0.1f, o[i].Traits.Metabolism)
|
||||
* (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f))
|
||||
* Mass.KleiberFactorFromSizeRatio(sizeRatio);
|
||||
var lethalEmpty = false;
|
||||
for (var k = 0; k < needs.Count; k++)
|
||||
{
|
||||
var def = needs[k];
|
||||
if (needs.IsDrive(k))
|
||||
{
|
||||
var rising = def.RisesUnder is null || h[i].State.Has(def.RisesUnder);
|
||||
var delta = (rising ? def.RisePerDay : -def.DecayPerDay) * days;
|
||||
values[k] = Math.Clamp(values[k] + delta, 0f, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
values[k] = Math.Clamp(
|
||||
values[k] - def.DecayPerDay * metabolism * days,
|
||||
0f,
|
||||
1f
|
||||
);
|
||||
if (def.Lethal && values[k] <= 0f)
|
||||
{
|
||||
lethalEmpty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ref var starve = ref n[i].Starve;
|
||||
starve = lethalEmpty ? starve + days : MathF.Max(0f, starve - days * 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Компонент-организм животного: управляемый <see cref="Genome"/> (для размножения, сейва и
|
||||
/// пересчёта), выраженный фенотип <see cref="Traits"/>, индекс вида в <see cref="Content.AnimalSet"/>
|
||||
/// и номер поколения (для <c>popstats</c> и видимого дрейфа). Геном — ссылочный объект, поэтому
|
||||
/// компонент несёт ссылку, а не копию (как у растений). Зеркало <see cref="PlantOrganism"/>.
|
||||
/// </summary>
|
||||
public struct AnimalOrganism : IComponent
|
||||
{
|
||||
/// <summary>Геном особи (аллели по генам); источник истины для признаков и размножения.</summary>
|
||||
public Genome Genome;
|
||||
|
||||
/// <summary>Выраженные признаки — то, что читают системы.</summary>
|
||||
public AnimalPhenotype Traits;
|
||||
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/>.</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Пол: самец (XY) или самка (XX) — выведен из локуса GeneSex при создании.</summary>
|
||||
public bool IsMale;
|
||||
|
||||
/// <summary>Ест ли растения (диета по генам/дефу, неизменна после рождения — кэш вместо пересчёта на тик).</summary>
|
||||
public bool EatsPlants;
|
||||
|
||||
/// <summary>Ест ли мясо (диета по генам/дефу, неизменна после рождения — кэш вместо пересчёта на тик).</summary>
|
||||
public bool EatsMeat;
|
||||
|
||||
/// <summary>Номер поколения: max(родителей) + 1; у основателей 0.</summary>
|
||||
public int Generation;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic;
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Признаки (фенотип) животного, вычисленные из генома формулами генов (см. <c>Defs/Genes/</c>):
|
||||
/// готовые значения, которые читают системы, не касаясь генов напрямую. Зеркало
|
||||
/// <see cref="PlantPhenotype"/>. Имена полей соответствуют именам признаков в эффектах генов.
|
||||
/// </summary>
|
||||
public struct AnimalPhenotype
|
||||
{
|
||||
/// <summary>Размер тела в клетках (масштаб спрайта; позже — HP/расход энергии).</summary>
|
||||
public float BodySize;
|
||||
|
||||
/// <summary>Скорость обмена веществ — множитель расхода нужд.</summary>
|
||||
public float Metabolism;
|
||||
|
||||
/// <summary>Скорость передвижения (множитель).</summary>
|
||||
public float MoveSpeed;
|
||||
|
||||
/// <summary>Объём крови (множитель) — масштаб скорости кровопотери.</summary>
|
||||
public float BloodVolume;
|
||||
|
||||
/// <summary>Дальность зрения (множитель радиуса поиска).</summary>
|
||||
public float Vision;
|
||||
|
||||
/// <summary>Острота слуха (множитель радиуса).</summary>
|
||||
public float Hearing;
|
||||
|
||||
/// <summary>Острота обоняния (множитель радиуса).</summary>
|
||||
public float Smell;
|
||||
|
||||
/// <summary>Острота осязания (множитель радиуса; чувство ближнего контакта).</summary>
|
||||
public float Touch;
|
||||
|
||||
/// <summary>Камуфляж [0..1] — снижает обнаружение этой особи зрением (заметность = 1−camouflage).</summary>
|
||||
public float Camouflage;
|
||||
|
||||
/// <summary>Запах [0..1] — повышает обнаружение этой особи обонянием.</summary>
|
||||
public float Scent;
|
||||
|
||||
/// <summary>Шумность [0..1] — повышает обнаружение этой особи слухом при движении.</summary>
|
||||
public float Noise;
|
||||
|
||||
/// <summary>Размер мозга (0..1) — основа интеллекта.</summary>
|
||||
public float BrainSize;
|
||||
|
||||
/// <summary>Теплоизоляция мехом (0..1).</summary>
|
||||
public float Insulation;
|
||||
|
||||
/// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта.</summary>
|
||||
public float FurHue;
|
||||
|
||||
/// <summary>Возраст созревания (игровых дней) — порог стадии Adult.</summary>
|
||||
public float MaturityAge;
|
||||
|
||||
/// <summary>Травоядность [0..1] — способность/тяга питаться растениями.</summary>
|
||||
public float Herbivory;
|
||||
|
||||
/// <summary>Хищничество [0..1] — тяга охотиться и питаться мясом (живая добыча/падаль).</summary>
|
||||
public float Carnivory;
|
||||
|
||||
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
|
||||
public float Omnivory;
|
||||
|
||||
/// <summary>
|
||||
/// Стиль питания [0..1]: 0 — «пастьба» (graze), часто подтапливает сытость мелкими приёмами;
|
||||
/// 1 — «обжорство» (gorge), ищет еду лишь при сильном голоде и ест до полной сытости (редкие
|
||||
/// крупные приёмы, как у хищников). Задаёт пороги начала/конца кормёжки в <see cref="Sim.AnimalDecisionSystem"/>.
|
||||
/// </summary>
|
||||
public float FeedingStyle;
|
||||
|
||||
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
|
||||
public float ToxinTolerance;
|
||||
|
||||
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
|
||||
public float Sociability;
|
||||
|
||||
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||
public float Lifespan;
|
||||
|
||||
/// <summary>Сезон гона (0 весна … 3 зима).</summary>
|
||||
public int BreedingSeason;
|
||||
|
||||
/// <summary>Срок вынашивания (игровых дней).</summary>
|
||||
public float GestationDays;
|
||||
|
||||
/// <summary>Размер помёта.</summary>
|
||||
public float LitterSize;
|
||||
|
||||
/// <summary>Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.</summary>
|
||||
public float EggLaying;
|
||||
|
||||
/// <summary>Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.</summary>
|
||||
public readonly bool Oviparous => EggLaying >= 0.5f;
|
||||
|
||||
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> 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"),
|
||||
Metabolism = T("metabolism"),
|
||||
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"),
|
||||
MaturityAge = T("maturityAge"),
|
||||
Herbivory = T("herbivory"),
|
||||
Carnivory = T("carnivory"),
|
||||
Omnivory = T("omnivory"),
|
||||
FeedingStyle = traits.GetValueOrDefault("feedingStyle", 0.5f),
|
||||
ToxinTolerance = T("toxinTolerance"),
|
||||
Sociability = T("sociability"),
|
||||
Lifespan = T("lifespan"),
|
||||
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||
GestationDays = T("gestationDays"),
|
||||
LitterSize = T("litterSize"),
|
||||
EggLaying = T("eggLaying"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Беременность и роды (фаза A4): тикает срок вынашивания; по истечении рожает помёт — каждый детёныш =
|
||||
/// <see cref="Genome.Breed"/> генома матери и отца (пол наследуется без YY), стадия Baby, поколение
|
||||
/// матери + 1, рядом с матерью. Компонент <see cref="Pregnant"/> снимается. Потолок численности
|
||||
/// страхует от взрыва популяции.
|
||||
/// </summary>
|
||||
public sealed class AnimalPregnancySystem : BaseSystem
|
||||
{
|
||||
private readonly EntityStore _store;
|
||||
private readonly AnimalSet _animals;
|
||||
private readonly NeedSet _needs;
|
||||
private readonly SkillSet _skills;
|
||||
private readonly ThoughtSet _thoughts;
|
||||
private readonly WorldEvents _events;
|
||||
private readonly GameClock _clock;
|
||||
private readonly float _secondsPerDay;
|
||||
private readonly int _cellSize;
|
||||
private readonly int _maxAnimals;
|
||||
private readonly Random _rng;
|
||||
private readonly ArchetypeQuery<Pregnant, AnimalOrganism, Transform2D> _query;
|
||||
private readonly ArchetypeQuery<AnimalOrganism> _census;
|
||||
private readonly List<Birth> _births = [];
|
||||
|
||||
public AnimalPregnancySystem(
|
||||
EntityStore store,
|
||||
AnimalSet animals,
|
||||
NeedSet needs,
|
||||
SkillSet skills,
|
||||
ThoughtSet thoughts,
|
||||
WorldEvents events,
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
int cellSize,
|
||||
int maxAnimals,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
_animals = animals;
|
||||
_needs = needs;
|
||||
_skills = skills;
|
||||
_thoughts = thoughts;
|
||||
_events = events;
|
||||
_clock = clock;
|
||||
_secondsPerDay = secondsPerDay;
|
||||
_cellSize = cellSize;
|
||||
_maxAnimals = maxAnimals;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<Pregnant, AnimalOrganism, Transform2D>();
|
||||
_census = store.Query<AnimalOrganism>();
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
var days = _clock.DeltaTime / _secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_births.Clear();
|
||||
foreach (var (pregnancies, organisms, transforms, entities) in _query.Chunks)
|
||||
{
|
||||
var p = pregnancies.Span;
|
||||
var o = organisms.Span;
|
||||
var t = transforms.Span;
|
||||
for (var i = 0; i < p.Length; i++)
|
||||
{
|
||||
ref var preg = ref p[i];
|
||||
preg.DueInDays -= days;
|
||||
if (preg.DueInDays > 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_births.Add(
|
||||
new Birth
|
||||
{
|
||||
Mother = entities.EntityAt(i),
|
||||
Species = o[i].Species,
|
||||
MotherGenome = o[i].Genome,
|
||||
FatherGenome = preg.FatherGenome,
|
||||
Litter = preg.Litter,
|
||||
Generation = o[i].Generation + 1,
|
||||
Position = t[i].Position,
|
||||
Oviparous = o[i].Traits.Oviparous,
|
||||
IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var count = _census.Count;
|
||||
foreach (var birth in _births)
|
||||
{
|
||||
if (birth.Mother.HasComponent<Pregnant>())
|
||||
{
|
||||
birth.Mother.RemoveComponent<Pregnant>();
|
||||
}
|
||||
|
||||
// Роды — сильная положительная мысль матери (A8).
|
||||
if (birth.Mother.HasComponent<Mood>())
|
||||
{
|
||||
birth.Mother.GetComponent<Mood>().Add(_thoughts.Get(AnimalThoughts.GaveBirth));
|
||||
}
|
||||
|
||||
var bornLive = 0;
|
||||
for (var k = 0; k < birth.Litter && count < _maxAnimals; k++)
|
||||
{
|
||||
var child = Genome.Breed(
|
||||
birth.MotherGenome,
|
||||
birth.FatherGenome,
|
||||
_animals.GeneRegistry,
|
||||
_rng
|
||||
);
|
||||
var offset =
|
||||
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
|
||||
if (birth.Oviparous)
|
||||
{
|
||||
// Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже.
|
||||
AnimalFactory.CreateEgg(
|
||||
_store,
|
||||
_animals,
|
||||
birth.Species,
|
||||
birth.Position + offset,
|
||||
child,
|
||||
birth.Generation,
|
||||
birth.IncubateDays,
|
||||
_cellSize
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimalFactory.Create(
|
||||
_store,
|
||||
_animals,
|
||||
_needs,
|
||||
_skills,
|
||||
birth.Species,
|
||||
birth.Position + offset,
|
||||
ageDays: 0f,
|
||||
child,
|
||||
birth.Generation,
|
||||
_cellSize
|
||||
);
|
||||
bornLive++;
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
// Живые роды — заметное событие (яйцекладка отметится позже при вылуплении в EggSystem).
|
||||
if (bornLive > 0)
|
||||
{
|
||||
_events.Born(birth.Mother, birth.Species, bornLive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Birth
|
||||
{
|
||||
public Entity Mother;
|
||||
public int Species;
|
||||
public Genome MotherGenome;
|
||||
public Genome FatherGenome;
|
||||
public int Litter;
|
||||
public int Generation;
|
||||
public Vector2 Position;
|
||||
public bool Oviparous;
|
||||
public float IncubateDays;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Гон (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut, вне сезона снимает. Рост влечения
|
||||
/// под этим хедифом считает <see cref="AnimalNeedsSystem"/> (нужда Mating с <c>risesUnder=Rut</c>) —
|
||||
/// сезонность и влечение разнесены: эта система отвечает только за состояние.
|
||||
/// </summary>
|
||||
public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDef rut)
|
||||
: QuerySystem<AnimalGrowth, AnimalOrganism, Health>
|
||||
{
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var season = (int)climate.Season;
|
||||
foreach (var (growths, organisms, healths, _) in Query.Chunks)
|
||||
{
|
||||
var g = growths.Span;
|
||||
var o = organisms.Span;
|
||||
var h = healths.Span;
|
||||
for (var i = 0; i < g.Length; i++)
|
||||
{
|
||||
var inRut =
|
||||
AnimalFactory.IsAdult(animals[o[i].Species], g[i].Stage)
|
||||
&& season == o[i].Traits.BreedingSeason;
|
||||
if (inRut)
|
||||
{
|
||||
h[i].State.Add(rut);
|
||||
}
|
||||
else
|
||||
{
|
||||
h[i].State.Remove(rut.DefName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Угасание навыков без практики: каждый уровень медленно сползает к полу своего <see cref="SkillDef"/>
|
||||
/// (рост идёт от использования в <see cref="AnimalActionSystem"/>). Так навык, которым не пользуются,
|
||||
/// постепенно забывается — есть давление на специализацию.
|
||||
/// </summary>
|
||||
public sealed class AnimalSkillSystem(GameClock clock, float secondsPerDay, SkillSet skills)
|
||||
: QuerySystem<Skills>
|
||||
{
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var days = clock.DeltaTime / secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (skillChunk, _) in Query.Chunks)
|
||||
{
|
||||
var s = skillChunk.Span;
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
var levels = s[i].Levels;
|
||||
if (levels is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var k = 0; k < skills.Count && k < levels.Length; k++)
|
||||
{
|
||||
levels[k] = MathF.Max(
|
||||
skills[k].FloorLevel,
|
||||
levels[k] - skills[k].DecayPerDay * days
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
|
||||
public static class AnimalSkills
|
||||
{
|
||||
/// <summary>Добыча корма: эффективность выпаса/поедания растений.</summary>
|
||||
public const string Foraging = "Foraging";
|
||||
|
||||
/// <summary>Охота: урон по добыче.</summary>
|
||||
public const string Hunting = "Hunting";
|
||||
|
||||
/// <summary>Уклонение: скорость бегства от хищника.</summary>
|
||||
public const string Evasion = "Evasion";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Id событийных мыслей настроения (фаза A8). Системы-источники события вешают мысль с этим id (через
|
||||
/// <see cref="Content.ThoughtSet"/>); её сила/длительность/метка — данные (thoughts.json). Новая мысль на
|
||||
/// новое событие = код-триггер + деф; правка чисел существующей — только JSON.
|
||||
/// </summary>
|
||||
public static class AnimalThoughts
|
||||
{
|
||||
public const string GaveBirth = "GaveBirth";
|
||||
public const string Mated = "Mated";
|
||||
public const string QuenchedThirst = "QuenchedThirst";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Friflo.Engine.ECS;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
|
||||
/// сверх грузоподъёмности (<see cref="Sim.Mass.CarryCapacity"/>) замедляет движение. Компонент появляется
|
||||
/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь.
|
||||
/// </summary>
|
||||
public struct Carrier : IComponent
|
||||
{
|
||||
/// <summary>Суммарная масса переносимого груза (кг).</summary>
|
||||
public float CarriedKg;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Friflo.Engine.ECS;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Труп животного (фаза A5): отдельная сущность после смерти. Разлагается во времени по стадиям
|
||||
/// (Fresh→Rotting→Skeleton→исчезает), скорость зависит от температуры. Несёт запас мяса (∝ размер тела)
|
||||
/// для будущей разделки/падальщиков. Управляется <see cref="Sim.CorpseSystem"/>.
|
||||
/// </summary>
|
||||
public struct Corpse : IComponent
|
||||
{
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта, разделки, сейва).</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||
public float RotDays;
|
||||
|
||||
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
|
||||
public int Stage;
|
||||
|
||||
/// <summary>Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт).</summary>
|
||||
public float Meat;
|
||||
|
||||
/// <summary>Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса.</summary>
|
||||
public float BodySize;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Разложение трупа (фаза A5): копит дни гниения (ускоряется в тепле, замедляется на морозе),
|
||||
/// переключает стадии Fresh→Rotting→Skeleton по порогам и тинтит спрайт; по достижении предела труп
|
||||
/// исчезает. Скелет рисуется бледным тинтом падали (отдельного спрайта пока нет — см. docs).
|
||||
/// </summary>
|
||||
public sealed class CorpseSystem : BaseSystem
|
||||
{
|
||||
private const float RottingDay = 2f;
|
||||
private const float SkeletonDay = 6f;
|
||||
private const float GoneDay = 14f;
|
||||
|
||||
private static readonly Color FreshTint = new(176, 148, 128);
|
||||
private static readonly Color RottingTint = new(120, 118, 98);
|
||||
private static readonly Color SkeletonTint = new(228, 226, 214);
|
||||
|
||||
private readonly GameClock _clock;
|
||||
private readonly float _secondsPerDay;
|
||||
private readonly Climate _climate;
|
||||
private readonly ArchetypeQuery<Corpse, Sprite> _query;
|
||||
private readonly List<Entity> _gone = [];
|
||||
|
||||
public CorpseSystem(EntityStore store, GameClock clock, float secondsPerDay, Climate climate)
|
||||
{
|
||||
_clock = clock;
|
||||
_secondsPerDay = secondsPerDay;
|
||||
_climate = climate;
|
||||
_query = store.Query<Corpse, Sprite>();
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
var days = _clock.DeltaTime / _secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Тепло ускоряет гниение, около нуля и ниже — резко замедляет (заморозка).
|
||||
var factor = Math.Clamp((_climate.Temperature + 5f) / 20f, 0.1f, 2.5f);
|
||||
var advance = days * factor;
|
||||
_gone.Clear();
|
||||
|
||||
foreach (var (corpses, sprites, entities) in _query.Chunks)
|
||||
{
|
||||
var c = corpses.Span;
|
||||
var s = sprites.Span;
|
||||
for (var i = 0; i < c.Length; i++)
|
||||
{
|
||||
ref var corpse = ref c[i];
|
||||
corpse.RotDays += advance;
|
||||
if (corpse.RotDays >= GoneDay)
|
||||
{
|
||||
_gone.Add(entities.EntityAt(i));
|
||||
continue;
|
||||
}
|
||||
|
||||
var stage =
|
||||
corpse.RotDays >= SkeletonDay ? 2
|
||||
: corpse.RotDays >= RottingDay ? 1
|
||||
: 0;
|
||||
if (stage != corpse.Stage)
|
||||
{
|
||||
corpse.Stage = stage;
|
||||
s[i].Color =
|
||||
stage == 2 ? SkeletonTint
|
||||
: stage == 1 ? RottingTint
|
||||
: FreshTint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entity in _gone)
|
||||
{
|
||||
entity.DeleteEntity();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Личная хроника особи (фича UI «История»): ограниченный кольцевой список событий о ней (спаривание,
|
||||
/// роды, охота, кормёжка, бегство, рост). Пишет <see cref="Sim.WorldEvents"/>, читает инспектор. Список —
|
||||
/// managed-ссылка (как мысли/память); старые записи вытесняются по достижении <see cref="Capacity"/>.
|
||||
/// </summary>
|
||||
public struct CreatureLog : IComponent
|
||||
{
|
||||
/// <summary>Записи в порядке добавления (новые в конце).</summary>
|
||||
public List<LogEntry> Entries;
|
||||
|
||||
/// <summary>Сколько записей хранится (старейшие вытесняются).</summary>
|
||||
public int Capacity;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Friflo.Engine.ECS;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Кратковременная «вспышка урона» (презентация, как в RimWorld): при получении удара выставляется
|
||||
/// время свечения; <see cref="Sim.DamageFlashSystem"/> подкрашивает спрайт красным и гасит свечение.
|
||||
/// Есть у всех животных (выставляется в 0 при создании), чтобы удар не делал структурного добавления.
|
||||
/// </summary>
|
||||
public struct DamageFlash : IComponent
|
||||
{
|
||||
/// <summary>Остаток реального времени свечения (сек); 0 — не подсвечен.</summary>
|
||||
public float Seconds;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Красная вспышка урона (презентация, как в RimWorld): особь, получившую удар, кратко подкрашивает
|
||||
/// красным и гасит свечение по РЕАЛЬНОМУ времени (видно и на высокой скорости). Запускать ПОСЛЕ
|
||||
/// <see cref="AnimalAppearanceSystem"/> (та задаёт базовый цвет от нужд) — вспышка подмешивается поверх.
|
||||
/// Время свечения выставляет <see cref="AnimalActionSystem"/> при ударе. Граница sim/presentation цела.
|
||||
/// </summary>
|
||||
public sealed class DamageFlashSystem(GameClock clock) : QuerySystem<Sprite, DamageFlash>
|
||||
{
|
||||
private static readonly Color FlashColor = new(255, 60, 60);
|
||||
private const float MaxIntensity = 0.85f; // насколько краснит на пике
|
||||
private const float FlashDuration = 0.35f; // полная длительность свечения (сек реального времени)
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var dt = clock.UnscaledDeltaTime;
|
||||
foreach (var (sprites, flashes, _) in Query.Chunks)
|
||||
{
|
||||
var s = sprites.Span;
|
||||
var f = flashes.Span;
|
||||
for (var i = 0; i < f.Length; i++)
|
||||
{
|
||||
ref var flash = ref f[i];
|
||||
if (flash.Seconds <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
flash.Seconds = MathF.Max(0f, flash.Seconds - dt);
|
||||
var t = Math.Clamp(flash.Seconds / FlashDuration, 0f, 1f) * MaxIntensity;
|
||||
s[i].Color = Color.Lerp(s[i].Color, FlashColor, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
|
||||
/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
|
||||
/// вылупляется в детёныша (<see cref="Sim.EggSystem"/>). Геном — managed-ссылка (как у трупа/беременности).
|
||||
/// </summary>
|
||||
public struct Egg : IComponent
|
||||
{
|
||||
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта при вылуплении и сейва).</summary>
|
||||
public int Species;
|
||||
|
||||
/// <summary>Геном будущего детёныша (уже скрещён из родителей на момент кладки).</summary>
|
||||
public Genome Genome;
|
||||
|
||||
/// <summary>Поколение будущего детёныша.</summary>
|
||||
public int Generation;
|
||||
|
||||
/// <summary>Остаток инкубации в игровых днях; ≤0 — вылупление.</summary>
|
||||
public float IncubateDays;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Genetics;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Инкубация яиц (тип рождения «яйцекладка»): тикает остаток инкубации каждого яйца; по истечении
|
||||
/// вылупляет детёныша (<see cref="AnimalFactory.Create"/> из хранимого генома, стадия Baby) рядом с
|
||||
/// яйцом и удаляет яйцо. Зеркало родов для яйцекладущих видов.
|
||||
/// </summary>
|
||||
public sealed class EggSystem : BaseSystem
|
||||
{
|
||||
private readonly EntityStore _store;
|
||||
private readonly AnimalSet _animals;
|
||||
private readonly NeedSet _needs;
|
||||
private readonly SkillSet _skills;
|
||||
private readonly WorldEvents _events;
|
||||
private readonly GameClock _clock;
|
||||
private readonly float _secondsPerDay;
|
||||
private readonly int _cellSize;
|
||||
private readonly Random _rng;
|
||||
private readonly ArchetypeQuery<Egg, Transform2D> _query;
|
||||
private readonly List<Entity> _hatched = [];
|
||||
|
||||
public EggSystem(
|
||||
EntityStore store,
|
||||
AnimalSet animals,
|
||||
NeedSet needs,
|
||||
SkillSet skills,
|
||||
WorldEvents events,
|
||||
GameClock clock,
|
||||
float secondsPerDay,
|
||||
int cellSize,
|
||||
int seed
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
_animals = animals;
|
||||
_needs = needs;
|
||||
_skills = skills;
|
||||
_events = events;
|
||||
_clock = clock;
|
||||
_secondsPerDay = secondsPerDay;
|
||||
_cellSize = cellSize;
|
||||
_rng = new Random(seed);
|
||||
_query = store.Query<Egg, Transform2D>();
|
||||
}
|
||||
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
var days = _clock.DeltaTime / _secondsPerDay;
|
||||
if (days <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hatched.Clear();
|
||||
foreach (var (eggs, transforms, entities) in _query.Chunks)
|
||||
{
|
||||
var e = eggs.Span;
|
||||
for (var i = 0; i < e.Length; i++)
|
||||
{
|
||||
ref var egg = ref e[i];
|
||||
egg.IncubateDays -= days;
|
||||
if (egg.IncubateDays > 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offset =
|
||||
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
|
||||
AnimalFactory.Create(
|
||||
_store,
|
||||
_animals,
|
||||
_needs,
|
||||
_skills,
|
||||
egg.Species,
|
||||
transforms.Span[i].Position + offset,
|
||||
ageDays: 0f,
|
||||
egg.Genome,
|
||||
egg.Generation,
|
||||
_cellSize
|
||||
);
|
||||
_events.Hatched(egg.Species, 1);
|
||||
_hatched.Add(entities.EntityAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var egg in _hatched)
|
||||
{
|
||||
egg.DeleteEntity();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using MrGameEng.Genetics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Friflo.Engine.ECS;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>Здоровье особи: managed-состояние со списком хедифов (см. <see cref="HealthState"/>).</summary>
|
||||
public struct Health : IComponent
|
||||
{
|
||||
/// <summary>Состояние здоровья (хедифы; позже части тела/кровь).</summary>
|
||||
public HealthState State;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
using System.Collections.Generic;
|
||||
using LittleSim.Content;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние здоровья особи (фазы A4–A6): активные хедифы (инстансы) + части тела, кровь и вычисленные
|
||||
/// способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает ссылку в
|
||||
/// компоненте). Болезни (A6) — первый источник урона: снижают способности и могут убить. Растёт по
|
||||
/// фазам — контейнер один.
|
||||
/// </summary>
|
||||
public sealed class HealthState
|
||||
{
|
||||
private readonly List<Hediff> _hediffs = [];
|
||||
|
||||
/// <summary>Активные хедифы (гон, болезни).</summary>
|
||||
public IReadOnlyList<Hediff> Hediffs => _hediffs;
|
||||
|
||||
/// <summary>Части тела особи (из анатомии вида); пусто — у вида нет тела.</summary>
|
||||
public PartInstance[] Parts { get; private set; } = [];
|
||||
|
||||
/// <summary>Уровень крови [0,1]; ниже порога — потеря сознания и смерть (когда появятся раны).</summary>
|
||||
public float BloodLevel { get; set; } = 1f;
|
||||
|
||||
/// <summary>Боль [0,1] — снижает сознание (когда появятся раны).</summary>
|
||||
public float Pain { get; set; }
|
||||
|
||||
/// <summary>Погибла ли особь по здоровью (vital-часть уничтожена / кровь на нуле).</summary>
|
||||
public bool Dead { get; set; }
|
||||
|
||||
/// <summary>Вычисленные способности (capacity id → [0,1]); у здоровой особи все = 1.</summary>
|
||||
public Dictionary<string, float> Capacities { get; private set; } = new();
|
||||
|
||||
/// <summary>Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий.</summary>
|
||||
public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
|
||||
/// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель <paramref name="capacityScale"/>
|
||||
/// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
|
||||
/// частей/крови/хедифов; <paramref name="capacityScale"/>=1 — без возрастного спада.
|
||||
/// </summary>
|
||||
public void RecomputeCapacities(float capacityScale = 1f)
|
||||
{
|
||||
var pain = 0f;
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
pain += h.Def.Pain * h.Severity;
|
||||
}
|
||||
|
||||
Pain = Math.Clamp(pain, 0f, 1f);
|
||||
|
||||
// Множитель старения применяется внутри Compute. Пишем в собственный словарь Capacities
|
||||
// (переиспользуется, очищается внутри Compute) — без аллокации карты способностей на пересчёт.
|
||||
var caps = Capacities;
|
||||
CapacityCalc.Compute(Parts, BloodLevel, Pain, capacityScale, caps);
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
if (h.Def.CapMods.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var t = Math.Clamp(h.Severity, 0f, 1f);
|
||||
foreach (var (capacity, factor) in h.Def.CapMods)
|
||||
{
|
||||
caps[capacity] = caps.GetValueOrDefault(capacity, 1f) * (1f + (factor - 1f) * t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
|
||||
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
|
||||
/// <paramref name="immunityScale"/> — множитель набора иммунитета (способность «фильтрация крови»:
|
||||
/// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
|
||||
/// </summary>
|
||||
public bool AdvanceHediffs(float days, float immunityScale = 1f)
|
||||
{
|
||||
var lethal = false;
|
||||
for (var i = _hediffs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var h = _hediffs[i];
|
||||
if (h.Def.SeverityPerDay <= 0f && h.Def.ImmunityPerDay <= 0f)
|
||||
{
|
||||
continue; // статичный хедиф (гон) не прогрессирует
|
||||
}
|
||||
|
||||
h.Severity += h.Def.SeverityPerDay * days;
|
||||
h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
|
||||
if (h.Immunity >= 1f)
|
||||
{
|
||||
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление
|
||||
continue;
|
||||
}
|
||||
|
||||
if (h.Def.LethalSeverity > 0f && h.Severity >= h.Def.LethalSeverity)
|
||||
{
|
||||
lethal = true;
|
||||
}
|
||||
|
||||
_hediffs[i] = h;
|
||||
}
|
||||
|
||||
return lethal;
|
||||
}
|
||||
|
||||
/// <summary>Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности.</summary>
|
||||
public static HealthState Create(Content.BodyDef? body, float bodySize)
|
||||
{
|
||||
var state = new HealthState();
|
||||
if (body is { Parts.Length: > 0 })
|
||||
{
|
||||
var scale = MathF.Max(0.2f, bodySize);
|
||||
state.Parts = new PartInstance[body.Parts.Length];
|
||||
for (var i = 0; i < body.Parts.Length; i++)
|
||||
{
|
||||
var hp = body.Parts[i].MaxHp * scale;
|
||||
state.Parts[i] = new PartInstance
|
||||
{
|
||||
Def = body.Parts[i],
|
||||
Hp = hp,
|
||||
MaxHp = hp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
state.RecomputeCapacities();
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>Несёт ли особь хедиф с данным именем дефа.</summary>
|
||||
public bool Has(string defName)
|
||||
{
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
if (h.Def.DefName == defName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Навешивает хедиф (если ещё не навешан) с начальной тяжестью из дефа.</summary>
|
||||
public void Add(HediffDef def)
|
||||
{
|
||||
if (!Has(def.DefName))
|
||||
{
|
||||
_hediffs.Add(new Hediff { Def = def, Severity = def.InitialSeverity });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Восстанавливает хедиф с заданной тяжестью и иммунитетом (для загрузки сейва).</summary>
|
||||
public void AddHediff(HediffDef def, float severity, float immunity)
|
||||
{
|
||||
if (!Has(def.DefName))
|
||||
{
|
||||
_hediffs.Add(
|
||||
new Hediff
|
||||
{
|
||||
Def = def,
|
||||
Severity = severity,
|
||||
Immunity = immunity,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Снимает хедиф по имени дефа; возвращает, был ли он.</summary>
|
||||
public bool Remove(string defName)
|
||||
{
|
||||
for (var i = 0; i < _hediffs.Count; i++)
|
||||
{
|
||||
if (_hediffs[i].Def.DefName == defName)
|
||||
{
|
||||
_hediffs.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Травма (укус хищника): повреждает часть тела (выбор по весу попадания <c>coverage</c>), вызывает
|
||||
/// мгновенную кровопотерю и навешивает/усиливает кровотечение, затем пересчитывает способности.
|
||||
/// Первый травматический источник урона — оживляет каркас здоровья A5.
|
||||
/// </summary>
|
||||
public void ApplyInjury(
|
||||
float partDamage,
|
||||
float bloodLoss,
|
||||
HediffDef? bleeding,
|
||||
float bleedSeverity,
|
||||
Random rng
|
||||
)
|
||||
{
|
||||
if (Parts.Length > 0 && partDamage > 0f)
|
||||
{
|
||||
var idx = PickPart(rng);
|
||||
var part = Parts[idx];
|
||||
part.Hp = MathF.Max(0f, part.Hp - partDamage);
|
||||
Parts[idx] = part;
|
||||
}
|
||||
|
||||
BloodLevel = Math.Clamp(BloodLevel - bloodLoss, 0f, 1f);
|
||||
if (bleeding is not null && bleedSeverity > 0f)
|
||||
{
|
||||
Intensify(bleeding, bleedSeverity);
|
||||
}
|
||||
|
||||
RecomputeCapacities();
|
||||
}
|
||||
|
||||
// Выбор задетой части по весу попадания (coverage); детерминируется переданным rng.
|
||||
private int PickPart(Random rng)
|
||||
{
|
||||
var total = 0f;
|
||||
foreach (var p in Parts)
|
||||
{
|
||||
total += p.Def?.Coverage ?? 0f;
|
||||
}
|
||||
|
||||
if (total <= 0f)
|
||||
{
|
||||
return rng.Next(Parts.Length);
|
||||
}
|
||||
|
||||
var roll = (float)rng.NextDouble() * total;
|
||||
for (var i = 0; i < Parts.Length; i++)
|
||||
{
|
||||
if (Parts[i].Def is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
roll -= Parts[i].Def.Coverage;
|
||||
if (roll <= 0f)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return Parts.Length - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Навешивает хедиф или усиливает существующий: тяжесть растёт (до 1), а заживление (иммунитет)
|
||||
/// сбрасывается — свежая рана начинает затягиваться заново. Для повторных укусов/ранений.
|
||||
/// </summary>
|
||||
public void Intensify(HediffDef def, float addSeverity)
|
||||
{
|
||||
for (var i = 0; i < _hediffs.Count; i++)
|
||||
{
|
||||
if (_hediffs[i].Def.DefName == def.DefName)
|
||||
{
|
||||
var h = _hediffs[i];
|
||||
h.Severity = Math.Clamp(h.Severity + addSeverity, 0f, 1f);
|
||||
h.Immunity = 0f; // свежая рана — заживление с нуля
|
||||
_hediffs[i] = h;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_hediffs.Add(new Hediff { Def = def, Severity = Math.Clamp(addSeverity, 0f, 1f) });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using LittleSim.Content;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>Инстанс хедифа на особи: деф + текущая тяжесть и набранный иммунитет (гонка, фаза A6).</summary>
|
||||
public struct Hediff
|
||||
{
|
||||
/// <summary>Деф хедифа (гон/болезнь/…).</summary>
|
||||
public HediffDef Def;
|
||||
|
||||
/// <summary>Текущая тяжесть.</summary>
|
||||
public float Severity;
|
||||
|
||||
/// <summary>Набранный иммунитет [0,1]; при 1 — выздоровление.</summary>
|
||||
public float Immunity;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using Friflo.Engine.ECS;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Память особи (объём ∝ мозгу): ограниченный набор записей о замеченном (сейчас — угрозы). Чувства
|
||||
/// пишут сюда воспринятое; со временем уверенность падает и запись забывается; ИИ использует память,
|
||||
/// когда цель не воспринимается сейчас (избегает зоны недавнего хищника). Список — managed-ссылка.
|
||||
/// </summary>
|
||||
public struct Memory : IComponent
|
||||
{
|
||||
/// <summary>Активные записи (упорядочены по добавлению; слабейшая вытесняется при переполнении).</summary>
|
||||
public List<MemoryEntry> Entries;
|
||||
|
||||
/// <summary>Сколько записей вмещает (∝ размеру мозга; 0 — память отсутствует).</summary>
|
||||
public int Capacity;
|
||||
|
||||
/// <summary>Запоминает/освежает наблюдение: близкое того же вида — обновляет, иначе добавляет (с
|
||||
/// вытеснением слабейшего при переполнении). Вне ёмкости (Capacity 0) — ничего не помнит.</summary>
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>Запись памяти: что, где (воспринятая позиция — грубая, если по запаху), насколько уверенно
|
||||
/// (падает со временем — забывание), сколько дней назад.</summary>
|
||||
public struct MemoryEntry
|
||||
{
|
||||
public MemoryKind Kind;
|
||||
public Vector2 Pos;
|
||||
public float Confidence;
|
||||
public float AgeDays;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>Вид запомненного: угроза (избегать), корм/вода/партнёр (идти) — расширяемо.</summary>
|
||||
public enum MemoryKind : byte
|
||||
{
|
||||
Threat,
|
||||
Food,
|
||||
Water,
|
||||
Mate,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user