Refactor game settings and remove unused files
- Removed the GameSettingsStore class and its associated methods for loading and saving game settings, streamlining the GameSettings class. - Deleted obsolete files related to world saving, game definitions, network smoothing, and content helpers, which were no longer in use, reducing code complexity and improving maintainability. - Updated the WorldScene class to reflect changes in the GameSettings structure, ensuring compatibility with the new settings management approach. This update enhances the overall codebase by eliminating redundancy and focusing on essential components.
This commit is contained in:
@@ -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="Pages\Index.razor.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="LittleSimWebGame.cs" />
|
<Compile Include="LittleSimWebGame.cs" />
|
||||||
|
<Compile Include="ConnectionStatus.cs" />
|
||||||
<Compile Include="NetContract.cs" />
|
<Compile Include="NetContract.cs" />
|
||||||
<Compile Include="WorldViewScene.cs" />
|
<Compile Include="WorldViewScene.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -9,22 +9,6 @@ using MrGameEng.Net;
|
|||||||
|
|
||||||
namespace LittleSim.Web;
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
|
|
||||||
public enum ConnectionStatus
|
|
||||||
{
|
|
||||||
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
|
|
||||||
Idle,
|
|
||||||
|
|
||||||
/// <summary>Идёт первая попытка подключения.</summary>
|
|
||||||
Connecting,
|
|
||||||
|
|
||||||
/// <summary>Соединение установлено, снапшоты приходят.</summary>
|
|
||||||
Connected,
|
|
||||||
|
|
||||||
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
|
|
||||||
Reconnecting,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
||||||
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Audio;
|
|
||||||
using MrGameEng.Mods;
|
|
||||||
|
|
||||||
namespace LittleSim.App;
|
namespace LittleSim.App;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -35,62 +30,3 @@ public sealed class GameSettings
|
|||||||
/// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
|
/// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
|
||||||
public bool DeveloperMode { get; set; } = true;
|
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,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,320 +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>Сериализуемый хедиф особи (фаза 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 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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,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,483 +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>Игровые секунды между ударами (кулдаун дискретной атаки, как в 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -5,25 +5,6 @@ using MrGameEng.Graphics;
|
|||||||
|
|
||||||
namespace LittleSim.Net;
|
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>
|
/// <summary>
|
||||||
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
||||||
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
||||||
@@ -10,69 +10,6 @@ using MrGameEng.Lighting;
|
|||||||
|
|
||||||
namespace LittleSim.Scenes;
|
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary>
|
/// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary>
|
||||||
internal static class ContentCommands
|
internal static class ContentCommands
|
||||||
{
|
{
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,759 +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]: 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"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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. Пишем в собственный словарь 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) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// Кратковременная «вспышка урона» (презентация, как в RimWorld): при получении удара выставляется
|
|
||||||
/// время свечения; <see cref="Sim.DamageFlashSystem"/> подкрашивает спрайт красным и гасит свечение.
|
|
||||||
/// Есть у всех животных (выставляется в 0 при создании), чтобы удар не делал структурного добавления.
|
|
||||||
/// </summary>
|
|
||||||
public struct DamageFlash : IComponent
|
|
||||||
{
|
|
||||||
/// <summary>Остаток реального времени свечения (сек); 0 — не подсвечен.</summary>
|
|
||||||
public float Seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using LittleSim.Content;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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="GutSeed"/>); по
|
||||||
|
/// истечении высаживает росток того вида растения там, где сейчас зверь (если клетка — суша),
|
||||||
|
/// геномом-клоном съеденного растения, и снимает компонент. Делает травоядных разносчиками флоры —
|
||||||
|
/// растения расселяются вдоль кормовых маршрутов.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SeedDispersalSystem : BaseSystem
|
||||||
|
{
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly int _width;
|
||||||
|
private readonly int _height;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly float[] _cellFertility;
|
||||||
|
private readonly bool[] _cellLand;
|
||||||
|
private readonly ArchetypeQuery<GutSeed, Transform2D> _query;
|
||||||
|
private readonly List<Entity> _dropped = [];
|
||||||
|
|
||||||
|
public SeedDispersalSystem(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int cellSize,
|
||||||
|
float[] cellFertility,
|
||||||
|
bool[] cellLand
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_plants = plants;
|
||||||
|
_clock = clock;
|
||||||
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_width = width;
|
||||||
|
_height = height;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_cellFertility = cellFertility;
|
||||||
|
_cellLand = cellLand;
|
||||||
|
_query = store.Query<GutSeed, Transform2D>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
var days = _clock.DeltaTime / _secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropped.Clear();
|
||||||
|
foreach (var (seeds, transforms, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var s = seeds.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < s.Length; i++)
|
||||||
|
{
|
||||||
|
ref var seed = ref s[i];
|
||||||
|
seed.DropInDays -= days;
|
||||||
|
if (seed.DropInDays > 0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dropped.Add(entities.EntityAt(i));
|
||||||
|
var pos = t[i].Position;
|
||||||
|
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
||||||
|
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
||||||
|
var cell = cy * _width + cx;
|
||||||
|
if (!_cellLand[cell])
|
||||||
|
{
|
||||||
|
continue; // упало в воду — семя пропадает
|
||||||
|
}
|
||||||
|
|
||||||
|
PlantFactory.Create(
|
||||||
|
_store,
|
||||||
|
_plants,
|
||||||
|
seed.PlantSpecies,
|
||||||
|
pos,
|
||||||
|
ageDays: 0f,
|
||||||
|
seed.Genome,
|
||||||
|
_cellFertility[cell],
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var eater in _dropped)
|
||||||
|
{
|
||||||
|
if (!eater.IsNull && eater.HasComponent<GutSeed>())
|
||||||
|
{
|
||||||
|
eater.RemoveComponent<GutSeed>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
|
||||||
|
public struct ThoughtInstance
|
||||||
|
{
|
||||||
|
/// <summary>Деф мысли (сдвиг настроения и длительность).</summary>
|
||||||
|
public Content.ThoughtDef Def;
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||||
|
public float RemainingDays;
|
||||||
|
}
|
||||||
@@ -2,40 +2,6 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace LittleSim.Sim;
|
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,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обобщённый расчёт способностей (capacities) из частей тела и крови (фаза A5). Сырой вклад способности
|
/// Обобщённый расчёт способностей (capacities) из частей тела и крови (фаза A5). Сырой вклад способности
|
||||||
/// = Σ(доля части × доля её HP). Затем известные способности домножаются по зависимостям (кровь →
|
/// = Σ(доля части × доля её HP). Затем известные способности домножаются по зависимостям (кровь →
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Категория события — определяет цвет строки в журнале/истории и группировку.</summary>
|
||||||
|
public enum EventCategory : byte
|
||||||
|
{
|
||||||
|
/// <summary>Размножение: спаривание, беременность, роды, вылупление.</summary>
|
||||||
|
Reproduction,
|
||||||
|
|
||||||
|
/// <summary>Охота и бой: начало охоты, убийство добычи.</summary>
|
||||||
|
Combat,
|
||||||
|
|
||||||
|
/// <summary>Смерть: голод/жажда, старость, болезнь, хищник.</summary>
|
||||||
|
Death,
|
||||||
|
|
||||||
|
/// <summary>Бытовое: кормёжка, водопой, бегство, смена стадии (только в истории особи).</summary>
|
||||||
|
Routine,
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его
|
||||||
|
/// сезон плодоношения (признак <see cref="PlantPhenotype.FruitSeason"/>) до потолка
|
||||||
|
/// <see cref="PlantPhenotype.FruitYield"/>, вне сезона опадает. Управляется <c>PlantFruitingSystem</c>.
|
||||||
|
/// </summary>
|
||||||
|
public struct Fruiting : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
||||||
|
public float RipeFruit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Камера бога: WASD/стрелки — панорамирование, колесо — зум к курсору (мировая точка под мышью
|
||||||
|
/// остаётся под мышью). Молчит, пока открыта консоль или активен <paramref name="blocked"/>
|
||||||
|
/// (например, открыто меню-пауза).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GodCameraSystem(
|
||||||
|
Entity cameraEntity,
|
||||||
|
InputManager input,
|
||||||
|
Renderer2D renderer,
|
||||||
|
MrGameEng.DevConsole.DevConsole? console,
|
||||||
|
Func<bool>? blocked = null
|
||||||
|
) : BaseSystem
|
||||||
|
{
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
if (console?.IsOpen == true || blocked?.Invoke() == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var camera = ref cameraEntity.GetComponent<Camera>();
|
||||||
|
|
||||||
|
var pan = new Vector2(
|
||||||
|
Axis(input, Keys.A, Keys.Left, Keys.D, Keys.Right),
|
||||||
|
Axis(input, Keys.W, Keys.Up, Keys.S, Keys.Down)
|
||||||
|
);
|
||||||
|
if (pan != Vector2.Zero)
|
||||||
|
{
|
||||||
|
pan.Normalize();
|
||||||
|
camera.Position += pan * (500f / camera.Zoom) * Tick.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.WheelDelta != 0)
|
||||||
|
{
|
||||||
|
var oldZoom = camera.Zoom;
|
||||||
|
var newZoom = Math.Clamp(oldZoom * (1f + input.WheelDelta * 0.001f), 0.4f, 8f);
|
||||||
|
if (newZoom != oldZoom)
|
||||||
|
{
|
||||||
|
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
|
||||||
|
// Якорь и базовая позиция берутся из одного кадра камеры: WorldCenter — это позиция
|
||||||
|
// ПОСЛЕ клампа к границам (то, что реально нарисовано), а не сырой camera.Position —
|
||||||
|
// иначе у краёв карты/при широком обзоре зум «уезжает» от курсора.
|
||||||
|
var anchor = renderer.ScreenToWorld(
|
||||||
|
new Vector2(input.MousePosition.X, input.MousePosition.Y)
|
||||||
|
);
|
||||||
|
var effective = renderer.Camera.WorldCenter;
|
||||||
|
camera.Zoom = newZoom;
|
||||||
|
camera.Position = anchor - (anchor - effective) * (oldZoom / newZoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float Axis(
|
||||||
|
InputManager input,
|
||||||
|
Keys negativeA,
|
||||||
|
Keys negativeB,
|
||||||
|
Keys positiveA,
|
||||||
|
Keys positiveB
|
||||||
|
) =>
|
||||||
|
(input.IsKeyDown(positiveA) || input.IsKeyDown(positiveB) ? 1f : 0f)
|
||||||
|
- (input.IsKeyDown(negativeA) || input.IsKeyDown(negativeB) ? 1f : 0f);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Строка даты/времени/сезона в HUD (верх-слева). Обновляется ~4 раза в секунду; смена языка
|
||||||
|
/// подхватывается сама. Сид и FPS убраны (сид виден в меню, FPS — в окне производительности),
|
||||||
|
/// подсказки управления вынесены отдельной строкой сцены.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HudSystem(
|
||||||
|
MrGameEng.Core.EngineContext context,
|
||||||
|
MrGameEng.Mods.LanguageManager languages,
|
||||||
|
Myra.Graphics2D.UI.Label label,
|
||||||
|
MrGameEng.Core.Calendar? calendar = null,
|
||||||
|
MrGameEng.Core.Climate? climate = null
|
||||||
|
) : BaseSystem
|
||||||
|
{
|
||||||
|
// Ключи локализации сезонов предсобраны (Spring..Winter), чтобы не строить строку
|
||||||
|
// "season."+ToString().ToLowerInvariant() на каждое обновление HUD.
|
||||||
|
private static readonly string[] SeasonKeys = BuildSeasonKeys();
|
||||||
|
|
||||||
|
private float _accumulated;
|
||||||
|
|
||||||
|
private static string[] BuildSeasonKeys()
|
||||||
|
{
|
||||||
|
var names = Enum.GetNames<MrGameEng.Core.Season>();
|
||||||
|
var keys = new string[names.Length];
|
||||||
|
for (var i = 0; i < names.Length; i++)
|
||||||
|
{
|
||||||
|
keys[i] = "season." + names[i].ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
_accumulated += context.Clock.UnscaledDeltaTime;
|
||||||
|
if (_accumulated < 0.25f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_accumulated = 0f;
|
||||||
|
var text = calendar is null
|
||||||
|
? ""
|
||||||
|
: languages.Format("hud.datetime", calendar.Day, calendar.Hour, calendar.Minute);
|
||||||
|
if (climate is not null)
|
||||||
|
{
|
||||||
|
var season = languages.Get(SeasonKeys[(int)climate.Season]);
|
||||||
|
text +=
|
||||||
|
" · "
|
||||||
|
+ languages.Format("hud.climate", season, (int)MathF.Round(climate.Temperature));
|
||||||
|
}
|
||||||
|
|
||||||
|
label.Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Одна запись журнала: готовая (локализованная) строка, категория, отметка времени календаря и id
|
||||||
|
/// «ответственной» сущности (для перехода камерой/выбора по двойному клику); -1 — не к кому переходить
|
||||||
|
/// (напр. смерть — особь исчезла).
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct LogEntry(
|
||||||
|
string Text,
|
||||||
|
EventCategory Category,
|
||||||
|
int Day,
|
||||||
|
int Hour,
|
||||||
|
int Minute,
|
||||||
|
int SubjectId
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Что житель делает прямо сейчас — результат utility-выбора.</summary>
|
||||||
|
public enum PawnAction
|
||||||
|
{
|
||||||
|
/// <summary>Бродит по миру, тратя энергию.</summary>
|
||||||
|
Wander,
|
||||||
|
|
||||||
|
/// <summary>Стоит и восстанавливает энергию.</summary>
|
||||||
|
Rest,
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Презентация состояния ИИ: уставший житель темнеет (альфа падает с энергией). Система читает
|
||||||
|
/// симуляцию и пишет только в <see cref="Sprite"/> — никаких draw-вызовов, граница sim/presentation цела.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PawnAppearanceSystem : QuerySystem<PawnNeeds, Sprite>
|
||||||
|
{
|
||||||
|
private const float MinBrightness = 0.45f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
foreach (var (needs, sprites, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var n = needs.Span;
|
||||||
|
var s = sprites.Span;
|
||||||
|
for (var i = 0; i < n.Length; i++)
|
||||||
|
{
|
||||||
|
var brightness =
|
||||||
|
MinBrightness + (1f - MinBrightness) * Math.Clamp(n[i].Energy, 0f, 1f);
|
||||||
|
s[i].Color = Color.White * brightness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,26 +6,6 @@ using MrGameEng.Graphics;
|
|||||||
|
|
||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>Что житель делает прямо сейчас — результат utility-выбора.</summary>
|
|
||||||
public enum PawnAction
|
|
||||||
{
|
|
||||||
/// <summary>Бродит по миру, тратя энергию.</summary>
|
|
||||||
Wander,
|
|
||||||
|
|
||||||
/// <summary>Стоит и восстанавливает энергию.</summary>
|
|
||||||
Rest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Потребности жителя. Пока одна — энергия (0 — без сил, 1 — полон сил); со временем
|
|
||||||
/// расходуется при блуждании и восстанавливается на отдыхе. Голод/жажда добавятся так же.
|
|
||||||
/// </summary>
|
|
||||||
public struct PawnNeeds : IComponent
|
|
||||||
{
|
|
||||||
/// <summary>Запас сил в диапазоне [0, 1].</summary>
|
|
||||||
public float Energy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// «Мозг» жителя: выбранное действие и таймер до следующего пересмотра решения.
|
/// «Мозг» жителя: выбранное действие и таймер до следующего пересмотра решения.
|
||||||
/// Решение принимает <see cref="PawnDecisionSystem"/> через движковый <see cref="UtilityAi{TContext}"/>.
|
/// Решение принимает <see cref="PawnDecisionSystem"/> через движковый <see cref="UtilityAi{TContext}"/>.
|
||||||
@@ -38,124 +18,3 @@ public struct PawnBrain : IComponent
|
|||||||
/// <summary>Секунды до следующего пересмотра решения — не пересчитываем выбор каждый кадр.</summary>
|
/// <summary>Секунды до следующего пересмотра решения — не пересчитываем выбор каждый кадр.</summary>
|
||||||
public float DecideIn;
|
public float DecideIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Снимок состояния жителя, который читают соображения utility-выбора.</summary>
|
|
||||||
public readonly record struct PawnContext(float Energy);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Utility-выбор действия жителя на движковом <see cref="UtilityAi{TContext}"/>. Reasoner строится
|
|
||||||
/// один раз; каждый житель пересматривает решение раз в <see cref="ReevaluateInterval"/> секунд, а
|
|
||||||
/// не каждый кадр (энергия меняется медленно — частить незачем, и решение не дёргается на границе).
|
|
||||||
/// Выбор детерминирован: при равенстве очков побеждает первое действие.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PawnDecisionSystem : QuerySystem<PawnNeeds, PawnBrain>
|
|
||||||
{
|
|
||||||
private const float ReevaluateInterval = 0.75f;
|
|
||||||
|
|
||||||
private readonly UtilityAi<PawnContext> _brain = new(
|
|
||||||
// Отдых тянет к себе только когда сил мало: (1 − energy)^3 быстро спадает к нулю выше ~0.3.
|
|
||||||
new UtilityAction<PawnContext>(
|
|
||||||
"rest",
|
|
||||||
new Consideration<PawnContext>(
|
|
||||||
"tired",
|
|
||||||
c => c.Energy,
|
|
||||||
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// Блуждание — поведение по умолчанию: тем привлекательнее, чем больше сил.
|
|
||||||
new UtilityAction<PawnContext>(
|
|
||||||
"wander",
|
|
||||||
new Consideration<PawnContext>("rested", c => c.Energy)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
var delta = Tick.deltaTime;
|
|
||||||
foreach (var (needs, brains, _) in Query.Chunks)
|
|
||||||
{
|
|
||||||
var n = needs.Span;
|
|
||||||
var b = brains.Span;
|
|
||||||
for (var i = 0; i < n.Length; i++)
|
|
||||||
{
|
|
||||||
ref var brain = ref b[i];
|
|
||||||
brain.DecideIn -= delta;
|
|
||||||
if (brain.DecideIn > 0f)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
brain.DecideIn = ReevaluateInterval;
|
|
||||||
var choice = _brain.Select(new PawnContext(n[i].Energy));
|
|
||||||
brain.Action = choice?.Name == "rest" ? PawnAction.Rest : PawnAction.Wander;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Очки последнего вычисленного выбора, по порядку действий — для дебаг-команды.</summary>
|
|
||||||
public (string Name, float Score)[] Inspect(float energy)
|
|
||||||
{
|
|
||||||
_brain.Select(new PawnContext(energy));
|
|
||||||
var scores = _brain.LastScores;
|
|
||||||
var result = new (string, float)[_brain.Actions.Count];
|
|
||||||
for (var i = 0; i < result.Length; i++)
|
|
||||||
{
|
|
||||||
result[i] = (_brain.Actions[i].Name, scores[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Презентация состояния ИИ: уставший житель темнеет (альфа падает с энергией). Система читает
|
|
||||||
/// симуляцию и пишет только в <see cref="Sprite"/> — никаких draw-вызовов, граница sim/presentation цела.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PawnAppearanceSystem : QuerySystem<PawnNeeds, Sprite>
|
|
||||||
{
|
|
||||||
private const float MinBrightness = 0.45f;
|
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
foreach (var (needs, sprites, _) in Query.Chunks)
|
|
||||||
{
|
|
||||||
var n = needs.Span;
|
|
||||||
var s = sprites.Span;
|
|
||||||
for (var i = 0; i < n.Length; i++)
|
|
||||||
{
|
|
||||||
var brightness =
|
|
||||||
MinBrightness + (1f - MinBrightness) * Math.Clamp(n[i].Energy, 0f, 1f);
|
|
||||||
s[i].Color = Color.White * brightness;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Расход и восстановление энергии по текущему действию: блуждающий устаёт, отдыхающий
|
|
||||||
/// восстанавливается. Скорости подобраны так, что отдых короткий, а блуждание долгое.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PawnNeedsSystem : QuerySystem<PawnNeeds, PawnBrain>
|
|
||||||
{
|
|
||||||
private const float DrainPerSecond = 0.04f;
|
|
||||||
private const float RegenPerSecond = 0.2f;
|
|
||||||
|
|
||||||
protected override void OnUpdate()
|
|
||||||
{
|
|
||||||
var delta = Tick.deltaTime;
|
|
||||||
foreach (var (needs, brains, _) in Query.Chunks)
|
|
||||||
{
|
|
||||||
var n = needs.Span;
|
|
||||||
var b = brains.Span;
|
|
||||||
for (var i = 0; i < n.Length; i++)
|
|
||||||
{
|
|
||||||
ref var energy = ref n[i].Energy;
|
|
||||||
energy +=
|
|
||||||
b[i].Action == PawnAction.Rest
|
|
||||||
? RegenPerSecond * delta
|
|
||||||
: -DrainPerSecond * delta;
|
|
||||||
energy = Math.Clamp(energy, 0f, 1f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Снимок состояния жителя, который читают соображения utility-выбора.</summary>
|
||||||
|
public readonly record struct PawnContext(float Energy);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Utility-выбор действия жителя на движковом <see cref="UtilityAi{TContext}"/>. Reasoner строится
|
||||||
|
/// один раз; каждый житель пересматривает решение раз в <see cref="ReevaluateInterval"/> секунд, а
|
||||||
|
/// не каждый кадр (энергия меняется медленно — частить незачем, и решение не дёргается на границе).
|
||||||
|
/// Выбор детерминирован: при равенстве очков побеждает первое действие.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PawnDecisionSystem : QuerySystem<PawnNeeds, PawnBrain>
|
||||||
|
{
|
||||||
|
private const float ReevaluateInterval = 0.75f;
|
||||||
|
|
||||||
|
private readonly UtilityAi<PawnContext> _brain = new(
|
||||||
|
// Отдых тянет к себе только когда сил мало: (1 − energy)^3 быстро спадает к нулю выше ~0.3.
|
||||||
|
new UtilityAction<PawnContext>(
|
||||||
|
"rest",
|
||||||
|
new Consideration<PawnContext>(
|
||||||
|
"tired",
|
||||||
|
c => c.Energy,
|
||||||
|
curve: ResponseCurve.Polynomial(exponent: 3f, slope: -1f, xShift: 1f)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
// Блуждание — поведение по умолчанию: тем привлекательнее, чем больше сил.
|
||||||
|
new UtilityAction<PawnContext>(
|
||||||
|
"wander",
|
||||||
|
new Consideration<PawnContext>("rested", c => c.Energy)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var delta = Tick.deltaTime;
|
||||||
|
foreach (var (needs, brains, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var n = needs.Span;
|
||||||
|
var b = brains.Span;
|
||||||
|
for (var i = 0; i < n.Length; i++)
|
||||||
|
{
|
||||||
|
ref var brain = ref b[i];
|
||||||
|
brain.DecideIn -= delta;
|
||||||
|
if (brain.DecideIn > 0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
brain.DecideIn = ReevaluateInterval;
|
||||||
|
var choice = _brain.Select(new PawnContext(n[i].Energy));
|
||||||
|
brain.Action = choice?.Name == "rest" ? PawnAction.Rest : PawnAction.Wander;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Очки последнего вычисленного выбора, по порядку действий — для дебаг-команды.</summary>
|
||||||
|
public (string Name, float Score)[] Inspect(float energy)
|
||||||
|
{
|
||||||
|
_brain.Select(new PawnContext(energy));
|
||||||
|
var scores = _brain.LastScores;
|
||||||
|
var result = new (string, float)[_brain.Actions.Count];
|
||||||
|
for (var i = 0; i < result.Length; i++)
|
||||||
|
{
|
||||||
|
result[i] = (_brain.Actions[i].Name, scores[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Имя дефа жителя — нужно сохранению, чтобы при загрузке пересобрать спрайт из атласа.</summary>
|
||||||
|
public struct PawnId : IComponent
|
||||||
|
{
|
||||||
|
public string DefName;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user