Dev spawner: RimWorld-style moddable spawn/damage tool (dev mode only)
A side panel (F9, only when developer mode is on) lists entries by category: animals and plants are auto-populated from their defs (so modded content shows up automatically), plus action tools. Spawn entries arm a "tool in hand" — left-click the world to spawn at the cursor (right-click clears); action tools (injure/kill/ infect) apply to the currently selected creature, so you can line up entities and damage them to compare. Moddable via a new DevToolDef def type: mods add buttons referencing a code handler kind (spawn-animal/plant/infect/damage/kill) + target, without touching code. Localized dev.* ru/en. Build + --check-content clean (13 def types). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
58380adc31
commit
cdb8439edf
@@ -114,6 +114,15 @@
|
|||||||
"settings.volume": "Volume",
|
"settings.volume": "Volume",
|
||||||
"settings.uiscale": "UI scale",
|
"settings.uiscale": "UI scale",
|
||||||
"settings.devmode": "Developer mode",
|
"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.apply": "Apply",
|
||||||
"settings.back": "Back",
|
"settings.back": "Back",
|
||||||
"pause.title": "Paused",
|
"pause.title": "Paused",
|
||||||
|
|||||||
@@ -114,6 +114,15 @@
|
|||||||
"settings.volume": "Громкость",
|
"settings.volume": "Громкость",
|
||||||
"settings.uiscale": "Масштаб UI",
|
"settings.uiscale": "Масштаб UI",
|
||||||
"settings.devmode": "Режим разработчика",
|
"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.apply": "Применить",
|
||||||
"settings.back": "Назад",
|
"settings.back": "Назад",
|
||||||
"pause.title": "Пауза",
|
"pause.title": "Пауза",
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public sealed class GameContent
|
|||||||
defs.RegisterType<ThoughtDef>("Thought");
|
defs.RegisterType<ThoughtDef>("Thought");
|
||||||
defs.RegisterType<BodyDef>("Body");
|
defs.RegisterType<BodyDef>("Body");
|
||||||
defs.RegisterType<SkillDef>("Skill");
|
defs.RegisterType<SkillDef>("Skill");
|
||||||
|
defs.RegisterType<DevToolDef>("DevTool");
|
||||||
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||||
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||||
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
||||||
|
|||||||
@@ -368,6 +368,24 @@ public sealed class ThoughtDef : Def
|
|||||||
public float DurationDays { get; init; } = 1f;
|
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>
|
/// <summary>
|
||||||
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
|
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
|
||||||
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
|
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
|
||||||
|
|||||||
@@ -183,12 +183,29 @@ public sealed class WorldScene : Scene
|
|||||||
_selection,
|
_selection,
|
||||||
brackets
|
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);
|
this.UseInspector(renderer);
|
||||||
|
|
||||||
// Режим разработчика (настройка, по умолчанию вкл): без него дев-консоль и спавнер не поднимаются.
|
|
||||||
var devMode = GameSettingsStore.Load().DeveloperMode;
|
|
||||||
MrGameEng.DevConsole.DevConsole? console = null;
|
MrGameEng.DevConsole.DevConsole? console = null;
|
||||||
if (devMode)
|
if (devMode)
|
||||||
{
|
{
|
||||||
@@ -222,6 +239,13 @@ public sealed class WorldScene : Scene
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
|
||||||
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дев-спавнер в духе RimWorld (только в режиме разработчика): боковая панель со списком по категориям.
|
||||||
|
/// Виды животных/растений берутся ИЗ ДЕФОВ (модовый контент появляется сам); «инструменты» (урон/смерть/
|
||||||
|
/// болезнь) и прочие модовые пункты — из <see cref="DevToolDef"/>. Спавн-пункт «берётся в руку»: клик по
|
||||||
|
/// миру создаёт сущность в курсоре (ПКМ снимает). Инструменты-действия применяются к выбранной особи
|
||||||
|
/// сразу по нажатию. Так удобно сравнивать сущности и наносить им урон. Управляет <see cref="DevSpawnerSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
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<HediffDef>("Fever", out var f) ? f : null;
|
||||||
|
_bleeding = content.Defs.TryGet<HediffDef>("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<DevToolDef>())
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Виджет панели (добавляется в корень экрана сцены).</summary>
|
||||||
|
public Widget Panel => _panel;
|
||||||
|
|
||||||
|
/// <summary>Взведён ли спавн-инструмент (клик по миру создаёт сущность).</summary>
|
||||||
|
public bool ArmedSpawn => _armedKind is "spawn-animal" or "spawn-plant";
|
||||||
|
|
||||||
|
/// <summary>Показать/скрыть панель (горячая клавиша из системы).</summary>
|
||||||
|
public void Toggle() => _panel.Visible = !_panel.Visible;
|
||||||
|
|
||||||
|
/// <summary>Снять «инструмент с руки» (ПКМ).</summary>
|
||||||
|
public void Disarm()
|
||||||
|
{
|
||||||
|
_armedKind = null;
|
||||||
|
_armedTarget = null;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Создаёт взведённую сущность в точке мира (вызывает система по клику ЛКМ).</summary>
|
||||||
|
public void SpawnAt(Vector2 world)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
_armedKind == "spawn-animal"
|
||||||
|
&& _content.Defs.TryGet<AnimalDef>(_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<PlantDef>(_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<AnimalOrganism>():
|
||||||
|
AnimalFactory.Die(_store, _animals, e, _cellSize);
|
||||||
|
break;
|
||||||
|
case "damage" when e.TryGetComponent<Health>(out var h) && h.State is not null:
|
||||||
|
h.State.ApplyInjury(15f, 0.08f, _bleeding, 1.5f, _rng);
|
||||||
|
break;
|
||||||
|
case "infect" when e.TryGetComponent<Health>(out var h2) && h2.State is not null:
|
||||||
|
var def =
|
||||||
|
target.Length > 0 && _content.Defs.TryGet<HediffDef>(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<AnimalDef>(defName, out var def) ? def.Label : defName;
|
||||||
|
|
||||||
|
private string PlantLabel(string defName) =>
|
||||||
|
_content.Defs.TryGet<PlantDef>(defName, out var def) ? def.Label : defName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Управление дев-спавнером: горячая клавиша F9 открывает/закрывает панель; при взведённом спавн-
|
||||||
|
/// инструменте ЛКМ по миру (не над UI) создаёт сущность в курсоре, ПКМ снимает инструмент.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevSpawnerSystem(Renderer2D renderer, DevSpawner spawner, Func<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user