Merge branch 'skills-and-devtools' into main
Developer-mode setting gating the dev console, a practice-grown skills system (with passion) for animals, and a RimWorld-style moddable dev spawner/damage tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Пауза",
|
||||
|
||||
@@ -31,6 +31,9 @@ public sealed class GameSettings
|
||||
|
||||
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
|
||||
public float UiScale { get; set; } = 1f;
|
||||
|
||||
/// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
|
||||
public bool DeveloperMode { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||
|
||||
@@ -149,6 +149,12 @@ public sealed class AnimalSave
|
||||
|
||||
/// <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>
|
||||
|
||||
@@ -68,6 +68,8 @@ public sealed class GameContent
|
||||
defs.RegisterType<NeedDef>("Need");
|
||||
defs.RegisterType<ThoughtDef>("Thought");
|
||||
defs.RegisterType<BodyDef>("Body");
|
||||
defs.RegisterType<SkillDef>("Skill");
|
||||
defs.RegisterType<DevToolDef>("DevTool");
|
||||
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
||||
|
||||
@@ -368,6 +368,45 @@ public sealed class ThoughtDef : Def
|
||||
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 (масштабируется размером тела), флаг жизненной важности и вклады в
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
namespace LittleSim.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Готовый реестр навыков из <see cref="SkillDef"/>: каждому навыку — индекс (по нему системы читают
|
||||
/// уровни в <c>Skills.Levels</c> без словарей), плюс генерация стартовых уровней и страсти особи.
|
||||
/// Делает набор навыков расширяемым данными (как <see cref="NeedSet"/>): добавил <see cref="SkillDef"/> —
|
||||
/// у особей появляется навык, без правки кода (если его эффект/начисление XP уже есть в коде).
|
||||
/// </summary>
|
||||
public sealed class SkillSet
|
||||
{
|
||||
// Веса страсти при генерации особи: нет / интерес / страсть (множители обучения см. PassionMultiplier).
|
||||
private static readonly float[] PassionWeights = [0.6f, 0.3f, 0.1f];
|
||||
|
||||
private readonly SkillDef[] _defs;
|
||||
private readonly Dictionary<string, int> _byId = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Строит реестр из всех <see cref="SkillDef"/> мода (порядок = индексы навыков).</summary>
|
||||
public SkillSet(GameContent content)
|
||||
{
|
||||
_defs = content.Defs.All<SkillDef>().ToArray();
|
||||
for (var i = 0; i < _defs.Length; i++)
|
||||
{
|
||||
_byId[_defs[i].DefName] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Число навыков.</summary>
|
||||
public int Count => _defs.Length;
|
||||
|
||||
/// <summary>Деф навыка по индексу.</summary>
|
||||
public SkillDef this[int index] => _defs[index];
|
||||
|
||||
/// <summary>Индекс навыка по id, или -1.</summary>
|
||||
public int IndexOf(string id) => _byId.TryGetValue(id, out var index) ? index : -1;
|
||||
|
||||
/// <summary>Стартовые уровни особи (с пола угасания каждого навыка).</summary>
|
||||
public float[] NewLevels()
|
||||
{
|
||||
var levels = new float[_defs.Length];
|
||||
for (var i = 0; i < levels.Length; i++)
|
||||
{
|
||||
levels[i] = _defs[i].FloorLevel;
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
/// <summary>Бросает страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) детерминированно по rng.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Множитель скорости обучения по уровню страсти (нет/интерес/страсть).</summary>
|
||||
public static float PassionMultiplier(byte passion) =>
|
||||
passion switch
|
||||
{
|
||||
0 => 0.35f,
|
||||
1 => 1.0f,
|
||||
_ => 1.7f,
|
||||
};
|
||||
}
|
||||
@@ -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<HediffDef>("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<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);
|
||||
}
|
||||
);
|
||||
@@ -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<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
|
||||
@@ -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<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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -491,6 +491,33 @@ public struct Egg : IComponent
|
||||
public float IncubateDays;
|
||||
}
|
||||
|
||||
/// <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"/>) замедляет движение. Компонент появляется
|
||||
|
||||
@@ -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<Carrier>(out var carrier)
|
||||
&& carrier.CarriedKg > 0f
|
||||
)
|
||||
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)
|
||||
@@ -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<Skills>(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<Skills>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,13 +125,13 @@ public sealed class GodCameraSystem(
|
||||
Entity cameraEntity,
|
||||
InputManager input,
|
||||
Renderer2D renderer,
|
||||
MrGameEng.DevConsole.DevConsole console,
|
||||
MrGameEng.DevConsole.DevConsole? console,
|
||||
Func<bool>? blocked = null
|
||||
) : BaseSystem
|
||||
{
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
if (console.IsOpen || blocked?.Invoke() == true)
|
||||
if (console?.IsOpen == true || blocked?.Invoke() == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Skills>(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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user