diff --git a/Mods/Core/Defs/Skills/Skills.json b/Mods/Core/Defs/Skills/Skills.json
new file mode 100644
index 0000000..1307daf
--- /dev/null
+++ b/Mods/Core/Defs/Skills/Skills.json
@@ -0,0 +1,14 @@
+{
+ "type": "Skill",
+ // Навыки как данные: уровень особи [0..1] растёт от практики (XP при соответствующем действии,
+ // ускоряется страстью) и медленно угасает к floorLevel. Эффект навыка — код по его id (добыча→выпас,
+ // охота→урон, уклонение→бегство). Сейчас навыки у животных; те же дефы пригодятся будущему человеку.
+ "defs": [
+ { "defName": "Foraging", "label": "skill.foraging", "category": "survival",
+ "learnRate": 0.6, "decayPerDay": 0.015, "floorLevel": 0.0 },
+ { "defName": "Hunting", "label": "skill.hunting", "category": "survival",
+ "learnRate": 0.5, "decayPerDay": 0.02, "floorLevel": 0.0 },
+ { "defName": "Evasion", "label": "skill.evasion", "category": "survival",
+ "learnRate": 0.7, "decayPerDay": 0.02, "floorLevel": 0.0 }
+ ]
+}
diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index dbaca85..1a7272b 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -11,6 +11,7 @@
"inspect.tab.genes": "Genes",
"inspect.tab.products": "Products",
"inspect.tab.needs": "Needs",
+ "inspect.tab.skills": "Skills",
"inspect.tab.health": "Health",
"inspect.tab.mood": "Mood",
"inspect.stage": "Stage: {0} ({1}/{2})",
@@ -42,6 +43,10 @@
"inspect.animal.sex": "Sex: {0} · generation {1}",
"inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth",
"inspect.needline": "{0}: {1:0}%",
+ "inspect.skillline": "{0}: {1:0}% {2}",
+ "skill.foraging": "foraging",
+ "skill.hunting": "hunting",
+ "skill.evasion": "evasion",
"inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%",
"inspect.health.caps": "— Capacities —",
"inspect.health.cap": "{0}: {1:0}%",
@@ -108,6 +113,16 @@
"settings.resolution": "Resolution",
"settings.volume": "Volume",
"settings.uiscale": "UI scale",
+ "settings.devmode": "Developer mode",
+ "dev.title": "Dev spawner",
+ "dev.hint": "Pick an entry · F9 to hide",
+ "dev.armed": "In hand: {0} · LMB to spawn, RMB to clear",
+ "dev.tools": "Tools",
+ "dev.damage": "Injure selected",
+ "dev.kill": "Kill selected",
+ "dev.infect": "Infect selected",
+ "dev.animals": "Animals",
+ "dev.plants": "Plants",
"settings.apply": "Apply",
"settings.back": "Back",
"pause.title": "Paused",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index ada3961..8550b50 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -11,6 +11,7 @@
"inspect.tab.genes": "Гены",
"inspect.tab.products": "Продукты",
"inspect.tab.needs": "Нужды",
+ "inspect.tab.skills": "Навыки",
"inspect.tab.health": "Здоровье",
"inspect.tab.mood": "Настроение",
"inspect.stage": "Стадия: {0} ({1}/{2})",
@@ -42,6 +43,10 @@
"inspect.animal.sex": "Пол: {0} · поколение {1}",
"inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов",
"inspect.needline": "{0}: {1:0}%",
+ "inspect.skillline": "{0}: {1:0}% {2}",
+ "skill.foraging": "добыча корма",
+ "skill.hunting": "охота",
+ "skill.evasion": "уклонение",
"inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%",
"inspect.health.caps": "— Способности —",
"inspect.health.cap": "{0}: {1:0}%",
@@ -108,6 +113,16 @@
"settings.resolution": "Разрешение",
"settings.volume": "Громкость",
"settings.uiscale": "Масштаб UI",
+ "settings.devmode": "Режим разработчика",
+ "dev.title": "Дев-спавнер",
+ "dev.hint": "Выбери пункт · F9 — скрыть",
+ "dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
+ "dev.tools": "Инструменты",
+ "dev.damage": "Ранить выбранного",
+ "dev.kill": "Убить выбранного",
+ "dev.infect": "Заразить выбранного",
+ "dev.animals": "Животные",
+ "dev.plants": "Растения",
"settings.apply": "Применить",
"settings.back": "Назад",
"pause.title": "Пауза",
diff --git a/src/LittleSim/App/GameSettings.cs b/src/LittleSim/App/GameSettings.cs
index 27a0e88..0326c4d 100644
--- a/src/LittleSim/App/GameSettings.cs
+++ b/src/LittleSim/App/GameSettings.cs
@@ -31,6 +31,9 @@ public sealed class GameSettings
/// Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.
public float UiScale { get; set; } = 1f;
+
+ /// Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.
+ public bool DeveloperMode { get; set; } = true;
}
/// Чтение/запись и применение их к движку.
diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs
index 752672c..3442b56 100644
--- a/src/LittleSim/App/WorldSave.cs
+++ b/src/LittleSim/App/WorldSave.cs
@@ -149,6 +149,12 @@ public sealed class AnimalSave
/// Размер помёта.
public int Litter { get; set; }
+
+ /// Уровни навыков [0..1] по индексам SkillSet.
+ public float[] SkillLevels { get; set; } = [];
+
+ /// Страсть к навыкам (0/1/2) по индексам SkillSet.
+ public int[] SkillPassions { get; set; } = [];
}
/// Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.
diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs
index 90803d7..77c6749 100644
--- a/src/LittleSim/Content/GameContent.cs
+++ b/src/LittleSim/Content/GameContent.cs
@@ -68,6 +68,8 @@ public sealed class GameContent
defs.RegisterType("Need");
defs.RegisterType("Thought");
defs.RegisterType("Body");
+ defs.RegisterType("Skill");
+ defs.RegisterType("DevTool");
defs.RegisterType("WorldPreset");
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 32a427c..9a764da 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -368,6 +368,45 @@ public sealed class ThoughtDef : Def
public float DurationDays { get; init; } = 1f;
}
+///
+/// Пункт дев-инструмента (Defs/DevTools/) — ЧТОБЫ МОДЫ ДОБАВЛЯЛИ кнопки в дев-спавнер без правки кода.
+/// — id обработчика из реестра (spawn-animal/spawn-plant/infect/damage/kill/…),
+/// — параметр (defName вида/хедифа). Виды животных/растений спавнер берёт из их
+/// дефов сам (модовый контент появляется автоматически); DevToolDef добавляет ПРОЧИЕ действия.
+///
+public sealed class DevToolDef : Def
+{
+ /// Категория для группировки в панели (например "tool").
+ public string Category { get; init; } = "tool";
+
+ /// Id обработчика-инструмента из реестра кода (spawn-animal/spawn-plant/infect/damage/kill).
+ public string Kind { get; init; } = "";
+
+ /// Параметр инструмента: defName вида/хедифа и т.п. (по смыслу Kind).
+ public string Target { get; init; } = "";
+}
+
+///
+/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
+/// ускоряется страстью) и медленно угасает без неё к полу . Эффект навыка —
+/// код по его id (как у нужд/действий): добыча→выпас, охота→урон, уклонение→бегство. Модер добавляет
+/// навык строкой JSON; новый эффект — код. — ключ локализации (skill.*).
+///
+public sealed class SkillDef : Def
+{
+ /// Категория (для группировки в UI), напр. "survival".
+ public string Category { get; init; } = "";
+
+ /// Скорость обучения за игровой день непрерывной практики (при страсти ×1).
+ public float LearnRate { get; init; } = 0.5f;
+
+ /// Угасание уровня за игровой день без практики.
+ public float DecayPerDay { get; init; } = 0.02f;
+
+ /// Пол угасания (ниже не падает) — «не забывается совсем».
+ public float FloorLevel { get; init; }
+}
+
///
/// Часть тела/орган (вложенный объект ) как ДАННЫЕ: иерархия (родитель),
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
diff --git a/src/LittleSim/Content/SkillSet.cs b/src/LittleSim/Content/SkillSet.cs
new file mode 100644
index 0000000..67ee459
--- /dev/null
+++ b/src/LittleSim/Content/SkillSet.cs
@@ -0,0 +1,81 @@
+namespace LittleSim.Content;
+
+///
+/// Готовый реестр навыков из : каждому навыку — индекс (по нему системы читают
+/// уровни в Skills.Levels без словарей), плюс генерация стартовых уровней и страсти особи.
+/// Делает набор навыков расширяемым данными (как ): добавил —
+/// у особей появляется навык, без правки кода (если его эффект/начисление XP уже есть в коде).
+///
+public sealed class SkillSet
+{
+ // Веса страсти при генерации особи: нет / интерес / страсть (множители обучения см. PassionMultiplier).
+ private static readonly float[] PassionWeights = [0.6f, 0.3f, 0.1f];
+
+ private readonly SkillDef[] _defs;
+ private readonly Dictionary _byId = new(StringComparer.Ordinal);
+
+ /// Строит реестр из всех мода (порядок = индексы навыков).
+ public SkillSet(GameContent content)
+ {
+ _defs = content.Defs.All().ToArray();
+ for (var i = 0; i < _defs.Length; i++)
+ {
+ _byId[_defs[i].DefName] = i;
+ }
+ }
+
+ /// Число навыков.
+ public int Count => _defs.Length;
+
+ /// Деф навыка по индексу.
+ public SkillDef this[int index] => _defs[index];
+
+ /// Индекс навыка по id, или -1.
+ public int IndexOf(string id) => _byId.TryGetValue(id, out var index) ? index : -1;
+
+ /// Стартовые уровни особи (с пола угасания каждого навыка).
+ public float[] NewLevels()
+ {
+ var levels = new float[_defs.Length];
+ for (var i = 0; i < levels.Length; i++)
+ {
+ levels[i] = _defs[i].FloorLevel;
+ }
+
+ return levels;
+ }
+
+ /// Бросает страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) детерминированно по rng.
+ public byte[] RollPassions(Random rng)
+ {
+ var passions = new byte[_defs.Length];
+ for (var i = 0; i < passions.Length; i++)
+ {
+ var roll = rng.NextSingle();
+ byte p = 0;
+ var acc = 0f;
+ for (byte k = 0; k < PassionWeights.Length; k++)
+ {
+ acc += PassionWeights[k];
+ if (roll < acc)
+ {
+ p = k;
+ break;
+ }
+ }
+
+ passions[i] = p;
+ }
+
+ return passions;
+ }
+
+ /// Множитель скорости обучения по уровню страсти (нет/интерес/страсть).
+ public static float PassionMultiplier(byte passion) =>
+ passion switch
+ {
+ 0 => 0.35f,
+ 1 => 1.0f,
+ _ => 1.7f,
+ };
+}
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index a4ac37c..5d7211d 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -67,6 +67,7 @@ public sealed class WorldScene : Scene
private PlantSet _plants = null!;
private AnimalSet _animals = null!;
private NeedSet _needs = null!;
+ private SkillSet _skills = null!;
private float[] _cellFertility = [];
private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
@@ -113,6 +114,7 @@ public sealed class WorldScene : Scene
_plants = new PlantSet(content, atlases, device);
_animals = new AnimalSet(content, atlases, device);
_needs = new NeedSet(content);
+ _skills = new SkillSet(content);
var loadingPlants = _save?.Plants is { Count: > 0 };
BuildTerrain(
content,
@@ -175,39 +177,74 @@ public sealed class WorldScene : Scene
_plants,
_animals,
_needs,
+ _skills,
content,
climate,
_selection,
brackets
);
- desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root);
+ // Режим разработчика (настройка, по умолчанию вкл): без него дев-консоль и спавнер не поднимаются.
+ var devMode = GameSettingsStore.Load().DeveloperMode;
+ var spawner = devMode
+ ? new DevSpawner(
+ Store,
+ _animals,
+ _plants,
+ _needs,
+ _skills,
+ content,
+ _selection,
+ _cellFertility,
+ _config.Width,
+ _config.Height,
+ CellSize
+ )
+ : null;
+ desktop.Root = spawner is null
+ ? Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root)
+ : Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root, spawner.Panel);
this.UseInspector(renderer);
- var console = this.UseDevConsole();
- RegisterCommands(console, content, atlases);
- console.Register(
- "light",
- "light [radius] — place a point light at the cursor (night shadows demo)",
- (c, args) =>
- {
- var radius =
- args.Length > 0
- ? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
- : 120f;
- var mouse = Mouse.GetState();
- var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
- Store.CreateEntity(
- Transform2D.At(world),
- new PointLight
- {
- Radius = radius,
- Color = Color.White,
- Intensity = 0.9f,
- }
- );
- c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
- }
- );
+
+ MrGameEng.DevConsole.DevConsole? console = null;
+ if (devMode)
+ {
+ console = this.UseDevConsole();
+ RegisterCommands(console, content, atlases);
+ console.Register(
+ "light",
+ "light [radius] — place a point light at the cursor (night shadows demo)",
+ (c, args) =>
+ {
+ var radius =
+ args.Length > 0
+ ? float.Parse(
+ args[0],
+ System.Globalization.CultureInfo.InvariantCulture
+ )
+ : 120f;
+ var mouse = Mouse.GetState();
+ var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
+ Store.CreateEntity(
+ Transform2D.At(world),
+ new PointLight
+ {
+ Radius = radius,
+ Color = Color.White,
+ Intensity = 0.9f,
+ }
+ );
+ c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
+ }
+ );
+ }
+
+ if (spawner is not null)
+ {
+ UpdateSystems.Add(
+ new DevSpawnerSystem(renderer, spawner, () => desktop.IsMouseOverGUI)
+ );
+ }
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
@@ -232,6 +269,7 @@ public sealed class WorldScene : Scene
var shore = ComputeShore();
var thoughts = new ThoughtSet(content);
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
+ UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
UpdateSystems.Add(
new AnimalRutSystem(_animals, climate, content.Defs.Get("Rut"))
);
@@ -271,6 +309,7 @@ public sealed class WorldScene : Scene
_plants,
_animals,
_needs,
+ _skills,
thoughts,
Context.Clock,
SecondsPerDay,
@@ -290,6 +329,7 @@ public sealed class WorldScene : Scene
Store,
_animals,
_needs,
+ _skills,
thoughts,
Context.Clock,
SecondsPerDay,
@@ -303,6 +343,7 @@ public sealed class WorldScene : Scene
Store,
_animals,
_needs,
+ _skills,
Context.Clock,
SecondsPerDay,
CellSize,
@@ -622,6 +663,14 @@ public sealed class WorldScene : Scene
record.Litter = preg.Litter;
}
+ if (entity.TryGetComponent(out var sk))
+ {
+ record.SkillLevels = sk.Levels ?? [];
+ record.SkillPassions = sk.Passions is null
+ ? []
+ : Array.ConvertAll(sk.Passions, p => (int)p);
+ }
+
save.Animals.Add(record);
}
);
@@ -709,6 +758,7 @@ public sealed class WorldScene : Scene
Store,
_animals,
_needs,
+ _skills,
index,
new Vector2(saved.X, saved.Y),
saved.AgeDays,
@@ -825,6 +875,23 @@ public sealed class WorldScene : Scene
}
);
}
+
+ // Навыки: накладываем сохранённые уровни/страсть, если состав совпал (иначе — свежие из фабрики).
+ if (entity.TryGetComponent(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
@@ -878,6 +945,7 @@ public sealed class WorldScene : Scene
Store,
_animals,
_needs,
+ _skills,
s,
new Vector2(px, py),
ageDays: random.NextSingle() * maxAge,
@@ -1056,6 +1124,11 @@ public sealed class WorldScene : Scene
"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",
@@ -1556,6 +1629,45 @@ public sealed class WorldScene : Scene
}
}
+ // Наблюдаемость навыков: средний уровень каждого навыка по живым животным (растут от практики).
+ 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()
+ .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(productDefName, out var product)
? content.Languages.Get(product.Label)
diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs
index 666ff0f..1d15c7c 100644
--- a/src/LittleSim/Sim/AnimalFactory.cs
+++ b/src/LittleSim/Sim/AnimalFactory.cs
@@ -23,6 +23,7 @@ public static class AnimalFactory
EntityStore store,
AnimalSet animals,
NeedSet needs,
+ SkillSet skills,
int species,
Vector2 position,
float ageDays,
@@ -41,7 +42,7 @@ public static class AnimalFactory
sprite.CenterOrigin();
sprite.Color = FurTint(traits);
- return store.CreateEntity(
+ var entity = store.CreateEntity(
new Transform2D(
position,
scale: new Vector2(Scale(sp, traits, stage, region, cellSize))
@@ -73,6 +74,28 @@ public static class AnimalFactory
Thoughts = [],
}
);
+
+ // Навыки: стартовые уровни + страсть (детерминированно по геному — воспроизводимо без RNG-параметра).
+ entity.AddComponent(
+ new Skills
+ {
+ Levels = skills.NewLevels(),
+ Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
+ }
+ );
+ return entity;
+ }
+
+ // Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
+ private static int SkillSeed(Genome genome)
+ {
+ var acc = 0;
+ foreach (var (_, allele) in genome.ToDictionary())
+ {
+ acc += (int)(allele.A * 7919f) + (int)(allele.B * 104729f);
+ }
+
+ return acc;
}
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
diff --git a/src/LittleSim/Sim/AnimalOrganism.cs b/src/LittleSim/Sim/AnimalOrganism.cs
index 394eac3..61e7102 100644
--- a/src/LittleSim/Sim/AnimalOrganism.cs
+++ b/src/LittleSim/Sim/AnimalOrganism.cs
@@ -491,6 +491,33 @@ public struct Egg : IComponent
public float IncubateDays;
}
+/// Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).
+public static class AnimalSkills
+{
+ /// Добыча корма: эффективность выпаса/поедания растений.
+ public const string Foraging = "Foraging";
+
+ /// Охота: урон по добыче.
+ public const string Hunting = "Hunting";
+
+ /// Уклонение: скорость бегства от хищника.
+ public const string Evasion = "Evasion";
+}
+
+///
+/// Навыки особи (как нужды/способности — managed-массивы по индексам ):
+/// уровни [0..1] (растут от практики, угасают без неё) и страсть к каждому навыку (скорость обучения).
+/// Есть у животных; те же навыки получат будущие люди. Добавление навыка не меняет структуру.
+///
+public struct Skills : IComponent
+{
+ /// Уровни навыков [0..1] по индексам .
+ public float[] Levels;
+
+ /// Страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) — множитель обучения.
+ public byte[] Passions;
+}
+
///
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
/// сверх грузоподъёмности () замедляет движение. Компонент появляется
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index c9ef9f1..4b03c52 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -884,6 +884,7 @@ public sealed class AnimalActionSystem : BaseSystem
private readonly PlantSet _plants;
private readonly AnimalSet _animalSet;
private readonly NeedSet _needs;
+ private readonly SkillSet _skills;
private readonly ThoughtSet _thoughts;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
@@ -892,6 +893,12 @@ public sealed class AnimalActionSystem : BaseSystem
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,
@@ -910,6 +917,7 @@ public sealed class AnimalActionSystem : BaseSystem
PlantSet plants,
AnimalSet animals,
NeedSet needs,
+ SkillSet skills,
ThoughtSet thoughts,
GameClock clock,
float secondsPerDay,
@@ -924,6 +932,10 @@ public sealed class AnimalActionSystem : BaseSystem
_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;
_clock = clock;
_secondsPerDay = secondsPerDay;
@@ -972,11 +984,9 @@ public sealed class AnimalActionSystem : BaseSystem
// даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
+ var self = entities.EntityAt(i);
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
- if (
- entities.EntityAt(i).TryGetComponent(out var carrier)
- && carrier.CarriedKg > 0f
- )
+ if (self.TryGetComponent(out var carrier) && carrier.CarriedKg > 0f)
{
var sp = _animalSet[o[i].Species];
var stageScale = entities.EntityAt(i).TryGetComponent(out var gr)
@@ -998,7 +1008,9 @@ public sealed class AnimalActionSystem : BaseSystem
case AnimalActions.Eat:
if (MoveTo(ref pos, brain.Target, speed * seconds))
{
- Refill(values, AnimalActions.Eat, days, foodEff);
+ // Навык добычи корма: опытный форажир извлекает больше сытости; растёт от практики.
+ var forageF = SkillFactor(_forageSkill, self);
+ Refill(values, AnimalActions.Eat, days, foodEff * forageF);
Graze(
brain.TargetPlant,
days,
@@ -1012,7 +1024,8 @@ public sealed class AnimalActionSystem : BaseSystem
days
);
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
- TryIngestSeed(brain.TargetPlant, entities.EntityAt(i));
+ TryIngestSeed(brain.TargetPlant, self);
+ GrantXp(_forageSkill, self, days);
}
break;
@@ -1057,11 +1070,28 @@ public sealed class AnimalActionSystem : BaseSystem
break;
case AnimalActions.Hunt:
- Hunt(ref brain, ref pos, values, def, speed * seconds, days, foodEff);
+ // Навык охоты: опытный хищник наносит больше урона; растёт от практики.
+ Hunt(
+ ref brain,
+ ref pos,
+ values,
+ def,
+ speed * seconds,
+ days,
+ foodEff,
+ SkillFactor(_huntSkill, self)
+ );
+ GrantXp(_huntSkill, self, days);
break;
case AnimalActions.Flee:
- MoveTo(ref pos, brain.Target, speed * FleeSpeedMult * seconds);
+ // Навык уклонения: опытная жертва убегает быстрее; растёт от практики бегства.
+ MoveTo(
+ ref pos,
+ brain.Target,
+ speed * FleeSpeedMult * SkillFactor(_evadeSkill, self) * seconds
+ );
+ GrantXp(_evadeSkill, self, days);
break;
default: // Wander
@@ -1151,7 +1181,8 @@ public sealed class AnimalActionSystem : BaseSystem
AnimalDef def,
float step,
float days,
- float foodEff
+ float foodEff,
+ float huntFactor
)
{
if (
@@ -1211,10 +1242,10 @@ public sealed class AnimalActionSystem : BaseSystem
}
preyHealth.ApplyInjury(
- def.AttackDamage * days,
- def.AttackBloodLoss * days,
+ def.AttackDamage * days * huntFactor,
+ def.AttackBloodLoss * days * huntFactor,
_bleeding,
- def.AttackBleed * days,
+ def.AttackBleed * days * huntFactor,
_rng
);
if (
@@ -1246,6 +1277,44 @@ public sealed class AnimalActionSystem : BaseSystem
);
}
+ // Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта).
+ private float SkillFactor(int idx, Entity self)
+ {
+ if (
+ idx < 0
+ || !self.TryGetComponent(out var s)
+ || s.Levels is null
+ || idx >= s.Levels.Length
+ )
+ {
+ return 1f;
+ }
+
+ return 0.7f + 0.6f * s.Levels[idx];
+ }
+
+ // Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача).
+ private void GrantXp(int idx, Entity self, float days)
+ {
+ if (
+ idx < 0
+ || !self.TryGetComponent(out var s)
+ || 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)
{
@@ -1742,6 +1811,7 @@ 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 GameClock _clock;
private readonly float _secondsPerDay;
@@ -1756,6 +1826,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
EntityStore store,
AnimalSet animals,
NeedSet needs,
+ SkillSet skills,
ThoughtSet thoughts,
GameClock clock,
float secondsPerDay,
@@ -1767,6 +1838,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
_store = store;
_animals = animals;
_needs = needs;
+ _skills = skills;
_thoughts = thoughts;
_clock = clock;
_secondsPerDay = secondsPerDay;
@@ -1861,6 +1933,7 @@ public sealed class AnimalPregnancySystem : BaseSystem
_store,
_animals,
_needs,
+ _skills,
birth.Species,
birth.Position + offset,
ageDays: 0f,
@@ -1899,6 +1972,7 @@ public sealed class EggSystem : BaseSystem
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly NeedSet _needs;
+ private readonly SkillSet _skills;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly int _cellSize;
@@ -1910,6 +1984,7 @@ public sealed class EggSystem : BaseSystem
EntityStore store,
AnimalSet animals,
NeedSet needs,
+ SkillSet skills,
GameClock clock,
float secondsPerDay,
int cellSize,
@@ -1919,6 +1994,7 @@ public sealed class EggSystem : BaseSystem
_store = store;
_animals = animals;
_needs = needs;
+ _skills = skills;
_clock = clock;
_secondsPerDay = secondsPerDay;
_cellSize = cellSize;
@@ -1953,6 +2029,7 @@ public sealed class EggSystem : BaseSystem
_store,
_animals,
_needs,
+ _skills,
egg.Species,
transforms.Span[i].Position + offset,
ageDays: 0f,
@@ -2274,3 +2351,42 @@ public sealed class SeedDispersalSystem : BaseSystem
}
}
}
+
+///
+/// Угасание навыков без практики: каждый уровень медленно сползает к полу своего
+/// (рост идёт от использования в ). Так навык, которым не пользуются,
+/// постепенно забывается — есть давление на специализацию.
+///
+public sealed class AnimalSkillSystem(GameClock clock, float secondsPerDay, SkillSet skills)
+ : QuerySystem
+{
+ 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
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/src/LittleSim/Sim/SimSystems.cs b/src/LittleSim/Sim/SimSystems.cs
index 30f5f89..3abc96e 100644
--- a/src/LittleSim/Sim/SimSystems.cs
+++ b/src/LittleSim/Sim/SimSystems.cs
@@ -125,13 +125,13 @@ public sealed class GodCameraSystem(
Entity cameraEntity,
InputManager input,
Renderer2D renderer,
- MrGameEng.DevConsole.DevConsole console,
+ MrGameEng.DevConsole.DevConsole? console,
Func? blocked = null
) : BaseSystem
{
protected override void OnUpdateGroup()
{
- if (console.IsOpen || blocked?.Invoke() == true)
+ if (console?.IsOpen == true || blocked?.Invoke() == true)
{
return;
}
diff --git a/src/LittleSim/UI/DevSpawner.cs b/src/LittleSim/UI/DevSpawner.cs
new file mode 100644
index 0000000..be726a8
--- /dev/null
+++ b/src/LittleSim/UI/DevSpawner.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using Friflo.Engine.ECS;
+using Friflo.Engine.ECS.Systems;
+using LittleSim.Content;
+using LittleSim.Sim;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MrGameEng.Graphics;
+using Myra.Graphics2D;
+using Myra.Graphics2D.Brushes;
+using Myra.Graphics2D.UI;
+
+namespace LittleSim.UI;
+
+///
+/// Дев-спавнер в духе RimWorld (только в режиме разработчика): боковая панель со списком по категориям.
+/// Виды животных/растений берутся ИЗ ДЕФОВ (модовый контент появляется сам); «инструменты» (урон/смерть/
+/// болезнь) и прочие модовые пункты — из . Спавн-пункт «берётся в руку»: клик по
+/// миру создаёт сущность в курсоре (ПКМ снимает). Инструменты-действия применяются к выбранной особи
+/// сразу по нажатию. Так удобно сравнивать сущности и наносить им урон. Управляет .
+///
+public sealed class DevSpawner
+{
+ private readonly EntityStore _store;
+ private readonly AnimalSet _animals;
+ private readonly PlantSet _plants;
+ private readonly NeedSet _needs;
+ private readonly SkillSet _skills;
+ private readonly GameContent _content;
+ private readonly Selection _selection;
+ private readonly float[] _cellFertility;
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _cellSize;
+ private readonly HediffDef? _fever;
+ private readonly HediffDef? _bleeding;
+ private readonly Random _rng = new(0x5A11);
+
+ private readonly VerticalStackPanel _panel;
+ private readonly Label _title;
+ private string? _armedKind;
+ private string? _armedTarget;
+
+ public DevSpawner(
+ EntityStore store,
+ AnimalSet animals,
+ PlantSet plants,
+ NeedSet needs,
+ SkillSet skills,
+ GameContent content,
+ Selection selection,
+ float[] cellFertility,
+ int width,
+ int height,
+ int cellSize
+ )
+ {
+ _store = store;
+ _animals = animals;
+ _plants = plants;
+ _needs = needs;
+ _skills = skills;
+ _content = content;
+ _selection = selection;
+ _cellFertility = cellFertility;
+ _width = width;
+ _height = height;
+ _cellSize = cellSize;
+ _fever = content.Defs.TryGet("Fever", out var f) ? f : null;
+ _bleeding = content.Defs.TryGet("Bleeding", out var b) ? b : null;
+
+ var lang = content.Languages;
+ _title = new Label { TextColor = Ui.Accent, Wrap = true };
+
+ var list = new VerticalStackPanel { Spacing = 2 };
+
+ // Инструменты-действия (по выбранной особи) + модовые DevToolDef.
+ Header(list, lang.Get("dev.tools"));
+ Entry(list, lang.Get("dev.damage"), () => ActOnSelection("damage", ""));
+ Entry(list, lang.Get("dev.kill"), () => ActOnSelection("kill", ""));
+ Entry(list, lang.Get("dev.infect"), () => ActOnSelection("infect", ""));
+ foreach (var tool in content.Defs.All())
+ {
+ var kind = tool.Kind;
+ var target = tool.Target;
+ var label = lang.Get(tool.Label);
+ if (kind is "spawn-animal" or "spawn-plant")
+ {
+ Entry(list, label, () => Arm(kind, target));
+ }
+ else
+ {
+ Entry(list, label, () => ActOnSelection(kind, target));
+ }
+ }
+
+ // Виды — автоматически из дефов (включая модовые): «взять в руку» спавн.
+ Header(list, lang.Get("dev.animals"));
+ foreach (var name in content.Defs.NamesOf("Animal"))
+ {
+ var defName = name;
+ Entry(list, "+ " + lang.Get(AnimalLabel(defName)), () => Arm("spawn-animal", defName));
+ }
+
+ Header(list, lang.Get("dev.plants"));
+ foreach (var name in content.Defs.NamesOf("Plant"))
+ {
+ var defName = name;
+ Entry(list, "+ " + lang.Get(PlantLabel(defName)), () => Arm("spawn-plant", defName));
+ }
+
+ _panel = new VerticalStackPanel
+ {
+ Spacing = 6,
+ Padding = new Thickness(12),
+ Width = 250,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(0, 14, 12, 0),
+ Background = new SolidBrush(new Color(10, 13, 18, 240)),
+ Border = new SolidBrush(new Color(96, 134, 168)),
+ BorderThickness = new Thickness(2),
+ Visible = false,
+ };
+ _panel.Widgets.Add(new Label { Text = lang.Get("dev.title"), TextColor = Ui.Accent });
+ _panel.Widgets.Add(_title);
+ _panel.Widgets.Add(new HorizontalSeparator());
+ _panel.Widgets.Add(new ScrollViewer { Height = 460, Content = list });
+ UpdateTitle();
+ }
+
+ /// Виджет панели (добавляется в корень экрана сцены).
+ public Widget Panel => _panel;
+
+ /// Взведён ли спавн-инструмент (клик по миру создаёт сущность).
+ public bool ArmedSpawn => _armedKind is "spawn-animal" or "spawn-plant";
+
+ /// Показать/скрыть панель (горячая клавиша из системы).
+ public void Toggle() => _panel.Visible = !_panel.Visible;
+
+ /// Снять «инструмент с руки» (ПКМ).
+ public void Disarm()
+ {
+ _armedKind = null;
+ _armedTarget = null;
+ UpdateTitle();
+ }
+
+ /// Создаёт взведённую сущность в точке мира (вызывает система по клику ЛКМ).
+ public void SpawnAt(Vector2 world)
+ {
+ if (
+ _armedKind == "spawn-animal"
+ && _content.Defs.TryGet(_armedTarget ?? "", out var adef)
+ )
+ {
+ var idx = _animals.IndexOf(adef);
+ var age = adef.Genome.GetValueOrDefault("GeneMaturityAge", 90f); // взрослый
+ AnimalFactory.Create(
+ _store,
+ _animals,
+ _needs,
+ _skills,
+ idx,
+ world,
+ age,
+ _animals.GenerateGenome(idx, _rng),
+ generation: 0,
+ _cellSize
+ );
+ }
+ else if (
+ _armedKind == "spawn-plant"
+ && _content.Defs.TryGet(_armedTarget ?? "", out var pdef)
+ )
+ {
+ var idx = _plants.IndexOf(pdef);
+ var cx = Math.Clamp((int)(world.X / _cellSize), 0, _width - 1);
+ var cy = Math.Clamp((int)(world.Y / _cellSize), 0, _height - 1);
+ var cell = cy * _width + cx;
+ var fertility = cell >= 0 && cell < _cellFertility.Length ? _cellFertility[cell] : 1f;
+ PlantFactory.Create(
+ _store,
+ _plants,
+ idx,
+ world,
+ _plants[idx].MaturityDays, // зрелое
+ _plants[idx].Template.Generate(_rng),
+ fertility,
+ _cellSize
+ );
+ }
+ }
+
+ // Применяет инструмент-действие к выбранной особи (урон/смерть/болезнь) — для сравнения/тестов.
+ private void ActOnSelection(string kind, string target)
+ {
+ if (
+ !_selection.HasSelection
+ || !_store.TryGetEntityById(_selection.EntityId, out var e)
+ || e.IsNull
+ )
+ {
+ return;
+ }
+
+ switch (kind)
+ {
+ case "kill" when e.HasComponent():
+ AnimalFactory.Die(_store, _animals, e, _cellSize);
+ break;
+ case "damage" when e.TryGetComponent(out var h) && h.State is not null:
+ h.State.ApplyInjury(15f, 0.08f, _bleeding, 1.5f, _rng);
+ break;
+ case "infect" when e.TryGetComponent(out var h2) && h2.State is not null:
+ var def =
+ target.Length > 0 && _content.Defs.TryGet(target, out var hd)
+ ? hd
+ : _fever;
+ if (def is not null)
+ {
+ h2.State.Add(def);
+ }
+
+ break;
+ }
+ }
+
+ private void Arm(string kind, string target)
+ {
+ _armedKind = kind;
+ _armedTarget = target;
+ UpdateTitle();
+ }
+
+ private void UpdateTitle() =>
+ _title.Text = _armedKind is null
+ ? _content.Languages.Get("dev.hint")
+ : _content.Languages.Format("dev.armed", _armedTarget ?? "");
+
+ private static void Header(VerticalStackPanel list, string text) =>
+ list.Widgets.Add(new Label { Text = text, TextColor = Ui.Muted });
+
+ private static void Entry(VerticalStackPanel list, string text, Action onClick)
+ {
+ var button = new TextButton { Text = text };
+ button.Click += (_, _) => onClick();
+ list.Widgets.Add(button);
+ }
+
+ private string AnimalLabel(string defName) =>
+ _content.Defs.TryGet(defName, out var def) ? def.Label : defName;
+
+ private string PlantLabel(string defName) =>
+ _content.Defs.TryGet(defName, out var def) ? def.Label : defName;
+}
+
+///
+/// Управление дев-спавнером: горячая клавиша F9 открывает/закрывает панель; при взведённом спавн-
+/// инструменте ЛКМ по миру (не над UI) создаёт сущность в курсоре, ПКМ снимает инструмент.
+///
+public sealed class DevSpawnerSystem(Renderer2D renderer, DevSpawner spawner, Func overUi)
+ : BaseSystem
+{
+ private bool _prevToggle;
+ private bool _prevLeft;
+ private bool _prevRight;
+
+ protected override void OnUpdateGroup()
+ {
+ var keyboard = Keyboard.GetState();
+ var toggle = keyboard.IsKeyDown(Keys.F9);
+ if (toggle && !_prevToggle)
+ {
+ spawner.Toggle();
+ }
+
+ _prevToggle = toggle;
+
+ var mouse = Mouse.GetState();
+ var left = mouse.LeftButton == ButtonState.Pressed;
+ var right = mouse.RightButton == ButtonState.Pressed;
+
+ if (spawner.ArmedSpawn && left && !_prevLeft && !overUi())
+ {
+ var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
+ spawner.SpawnAt(world);
+ }
+
+ if (right && !_prevRight)
+ {
+ spawner.Disarm();
+ }
+
+ _prevLeft = left;
+ _prevRight = right;
+ }
+}
diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs
index 9b8dd8c..3949fe8 100644
--- a/src/LittleSim/UI/InspectPanel.cs
+++ b/src/LittleSim/UI/InspectPanel.cs
@@ -31,17 +31,26 @@ internal sealed class InspectPanel
Genes,
Products,
Needs,
+ Skills,
Health,
Mood,
}
private static readonly Tab[] PlantTabs = [Tab.Overview, Tab.Genes, Tab.Products];
- private static readonly Tab[] AnimalTabs = [Tab.Overview, Tab.Needs, Tab.Health, Tab.Mood];
+ private static readonly Tab[] AnimalTabs =
+ [
+ Tab.Overview,
+ Tab.Needs,
+ Tab.Skills,
+ Tab.Health,
+ Tab.Mood,
+ ];
private readonly EntityStore _store;
private readonly PlantSet _plants;
private readonly AnimalSet _animals;
private readonly NeedSet _needs;
+ private readonly SkillSet _skills;
private readonly GameContent _content;
private readonly Climate _climate;
private readonly Selection _selection;
@@ -58,6 +67,7 @@ internal sealed class InspectPanel
PlantSet plants,
AnimalSet animals,
NeedSet needs,
+ SkillSet skills,
GameContent content,
Climate climate,
Selection selection,
@@ -68,6 +78,7 @@ internal sealed class InspectPanel
_plants = plants;
_animals = animals;
_needs = needs;
+ _skills = skills;
_content = content;
_climate = climate;
_selection = selection;
@@ -81,6 +92,7 @@ internal sealed class InspectPanel
AddTab(tabBar, Tab.Genes, "inspect.tab.genes");
AddTab(tabBar, Tab.Products, "inspect.tab.products");
AddTab(tabBar, Tab.Needs, "inspect.tab.needs");
+ AddTab(tabBar, Tab.Skills, "inspect.tab.skills");
AddTab(tabBar, Tab.Health, "inspect.tab.health");
AddTab(tabBar, Tab.Mood, "inspect.tab.mood");
@@ -246,6 +258,9 @@ internal sealed class InspectPanel
case Tab.Needs:
BuildAnimalNeeds(text, entity);
break;
+ case Tab.Skills:
+ BuildAnimalSkills(text, entity);
+ break;
case Tab.Health:
BuildAnimalHealth(text, entity);
break;
@@ -446,6 +461,37 @@ internal sealed class InspectPanel
}
}
+ // — Животное: Навыки — уровень каждого навыка (%) и страсть (★/★★), растут от практики.
+ private void BuildAnimalSkills(StringBuilder text, Entity entity)
+ {
+ var languages = _content.Languages;
+ if (!entity.TryGetComponent(out var sk) || sk.Levels is null)
+ {
+ text.AppendLine(languages.Get("inspect.health.none"));
+ return;
+ }
+
+ for (var i = 0; i < _skills.Count && i < sk.Levels.Length; i++)
+ {
+ var passion =
+ sk.Passions is not null && i < sk.Passions.Length ? sk.Passions[i] : (byte)0;
+ var stars = passion switch
+ {
+ 2 => "★★",
+ 1 => "★",
+ _ => "",
+ };
+ text.AppendLine(
+ languages.Format(
+ "inspect.skillline",
+ languages.Get(_skills[i].Label),
+ sk.Levels[i] * 100f,
+ stars
+ )
+ );
+ }
+ }
+
// — Животное: Здоровье — кровь/боль, ключевые способности, активные хедифы (раны/болезни/гон).
private void BuildAnimalHealth(StringBuilder text, Entity entity)
{
diff --git a/src/LittleSim/UI/SettingsPanel.cs b/src/LittleSim/UI/SettingsPanel.cs
index a1b1984..192fde6 100644
--- a/src/LittleSim/UI/SettingsPanel.cs
+++ b/src/LittleSim/UI/SettingsPanel.cs
@@ -98,6 +98,16 @@ internal static class SettingsPanel
// Громкость — кнопки −/+ с подписью процента.
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
+ // Режим разработчика — тумблер (дев-консоль и спавнер; по умолчанию включён).
+ column.Widgets.Add(
+ Toggle(
+ lang.Get("settings.devmode"),
+ () => settings.DeveloperMode,
+ v => settings.DeveloperMode = v,
+ refreshers
+ )
+ );
+
column.Widgets.Add(new Label { Height = 8 });
var buttons = Ui.Row(10);
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));