From 003f235440790353212ff842ee246a2494ff21b5 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 14 Jun 2026 06:19:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=96=D0=B8=D0=B2=D0=BE=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20A8=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=202:=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=81=D0=BF=D0=B5=D0=BA=D1=82=D0=BE=D1=80=20=D0=B6=D0=B8?= =?UTF-8?q?=D0=B2=D0=BE=D1=82=D0=BD=D1=8B=D1=85=20(=D0=B2=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=BA=D0=B8=20=D0=BD=D1=83=D0=B6=D0=B4=D1=8B/?= =?UTF-8?q?=D0=B7=D0=B4=D0=BE=D1=80=D0=BE=D0=B2=D1=8C=D0=B5/=D0=BD=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BD=D0=B8=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InspectPanel осматривает и животных: вкладки Обзор (стадия/возраст/пол/поколение/беременность), Нужды, Здоровье (кровь/боль, способности, хедифы), Настроение (значение + мысли). Набор вкладок по типу выбранного. SelectionSystem пикает и животных (запрос расширен PlantGrowth → +AnimalOrganism). Локализация inspect.*/need.*/hediff.*/cap.*/animalstage.* ru+en. Базовый план зоологии A1-A8 закрыт. Сборка + --check-content чистые. Co-Authored-By: Claude Opus 4.8 (1M context) --- Mods/Core/Languages/en/ui.json | 32 ++++ Mods/Core/Languages/ru/ui.json | 32 ++++ docs/животные.md | 13 +- src/LittleSim/Scenes/WorldScene.cs | 11 +- src/LittleSim/Sim/Selection.cs | 68 +++++--- src/LittleSim/UI/InspectPanel.cs | 256 ++++++++++++++++++++++++++--- 6 files changed, 367 insertions(+), 45 deletions(-) diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 9ace6f9..d4cc4ee 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -10,6 +10,9 @@ "inspect.tab.overview": "Overview", "inspect.tab.genes": "Genes", "inspect.tab.products": "Products", + "inspect.tab.needs": "Needs", + "inspect.tab.health": "Health", + "inspect.tab.mood": "Mood", "inspect.stage": "Stage: {0} ({1}/{2})", "inspect.growing": "Growth: {0:0}% to next stage", "inspect.mature": "Fully grown", @@ -31,6 +34,35 @@ "inspect.harvest": "Harvest: {0} ×{1:0.#}", "inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}", "inspect.barren": "Bears no fruit", + "inspect.sex.male": "male", + "inspect.sex.female": "female", + "inspect.animal.sex": "Sex: {0} · generation {1}", + "inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth", + "inspect.needline": "{0}: {1:0}%", + "inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%", + "inspect.health.caps": "— Capacities —", + "inspect.health.cap": "{0}: {1:0}%", + "inspect.health.hediffs": "— Conditions —", + "inspect.health.hediffline": "{0}: severity {1:0}%", + "inspect.health.none": "Healthy, no wounds or illness", + "inspect.mood.value": "Mood: {0:0}%", + "inspect.mood.none": "No mood (primitive brain)", + "inspect.mood.thoughts": "— Thoughts —", + "inspect.mood.thoughtline": "{0}: {1}", + "inspect.mood.calm": "Calm, nothing on its mind", + "animalstage.baby": "baby", + "animalstage.juvenile": "juvenile", + "animalstage.adult": "adult", + "animalstage.senior": "senior", + "need.hunger": "hunger", + "need.thirst": "thirst", + "need.rest": "rest", + "need.mating": "mating", + "cap.consciousness": "consciousness", + "cap.moving": "moving", + "cap.sight": "sight", + "hediff.rut": "rut", + "hediff.fever": "fever", "inspect.hint": "LMB — select · RMB/Esc — clear", "hud.paused": "PAUSED", "menu.title": "LittleSim", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 9f4f785..bd45d20 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -10,6 +10,9 @@ "inspect.tab.overview": "Обзор", "inspect.tab.genes": "Гены", "inspect.tab.products": "Продукты", + "inspect.tab.needs": "Нужды", + "inspect.tab.health": "Здоровье", + "inspect.tab.mood": "Настроение", "inspect.stage": "Стадия: {0} ({1}/{2})", "inspect.growing": "Рост: {0:0}% до следующей стадии", "inspect.mature": "Полностью выросло", @@ -31,6 +34,35 @@ "inspect.harvest": "Сбор: {0} ×{1:0.#}", "inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}", "inspect.barren": "Не плодоносит", + "inspect.sex.male": "самец", + "inspect.sex.female": "самка", + "inspect.animal.sex": "Пол: {0} · поколение {1}", + "inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов", + "inspect.needline": "{0}: {1:0}%", + "inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%", + "inspect.health.caps": "— Способности —", + "inspect.health.cap": "{0}: {1:0}%", + "inspect.health.hediffs": "— Состояния —", + "inspect.health.hediffline": "{0}: тяжесть {1:0}%", + "inspect.health.none": "Здоров, ран и болезней нет", + "inspect.mood.value": "Настроение: {0:0}%", + "inspect.mood.none": "Настроения нет (примитивный мозг)", + "inspect.mood.thoughts": "— Мысли —", + "inspect.mood.thoughtline": "{0}: {1}", + "inspect.mood.calm": "Спокоен, особых мыслей нет", + "animalstage.baby": "детёныш", + "animalstage.juvenile": "подросток", + "animalstage.adult": "взрослый", + "animalstage.senior": "старый", + "need.hunger": "голод", + "need.thirst": "жажда", + "need.rest": "отдых", + "need.mating": "влечение", + "cap.consciousness": "сознание", + "cap.moving": "движение", + "cap.sight": "зрение", + "hediff.rut": "гон", + "hediff.fever": "лихорадка", "inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять", "hud.paused": "ПАУЗА", "menu.title": "LittleSim", diff --git a/docs/животные.md b/docs/животные.md index be941d5..d82e7f9 100644 --- a/docs/животные.md +++ b/docs/животные.md @@ -1,7 +1,16 @@ # Животные (зоология) -> Статус: **A1–A7 + A8 часть 1 (ядро настроения) реализованы**, осталось A8 часть 2 (инспектор). Это -> живой план — правится по мере реализации фаз A1–A8. Стиль и правила — как в [концепте](concept.md) и [модах](mods.md). +> Статус: **A1–A8 реализованы целиком** — базовый план зоологии закрыт; дальше экологический горизонт +> (хищники, опека, эпидемии, миграция) и бэклог реализма. Это живой план — стиль и правила как в +> [концепте](concept.md) и [модах](mods.md). +> +> **A8 часть 2 (готово): инспектор животных.** `InspectPanel` теперь осматривает и животных (не только +> растения): вкладки **Обзор** (стадия/возраст/пол/поколение/беременность), **Нужды** (значения нужд %), +> **Здоровье** (кровь/боль, способности Consciousness/Moving/Sight, активные хедифы с тяжестью), +> **Настроение** (значение % + список мыслей с их сдвигом). Набор вкладок зависит от типа выбранного +> (растение vs животное). `SelectionSystem` теперь пикает и животных (запрос расширен с `PlantGrowth` на +> `AnimalOrganism`). Локализация `inspect.*`/`need.*`/`hediff.*`/`cap.*`/`animalstage.*` (ru/en). Сборка + +> `--check-content` чистые. > > **A8 часть 1 (готово): настроение — ядро.** Тип дефа `ThoughtDef` + `thoughts.json` (мысли: роды/ > спаривание/утоление жажды — сила и длительность данными); компонент `Mood` (значение 0..1, активность по diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index e172ce0..6a38477 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -163,7 +163,16 @@ public sealed class WorldScene : Scene atlases.GetRegion(device, "ui/overlays/selectionbracket"), GameLayers.Selection ); - _inspect = new InspectPanel(Store, _plants, content, climate, _selection, brackets); + _inspect = new InspectPanel( + Store, + _plants, + _animals, + _needs, + content, + climate, + _selection, + brackets + ); desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root); this.UseInspector(renderer); diff --git a/src/LittleSim/Sim/Selection.cs b/src/LittleSim/Sim/Selection.cs index 90d1689..46e4602 100644 --- a/src/LittleSim/Sim/Selection.cs +++ b/src/LittleSim/Sim/Selection.cs @@ -1,3 +1,4 @@ +using System; using Friflo.Engine.ECS; using Friflo.Engine.ECS.Systems; using Microsoft.Xna.Framework; @@ -74,41 +75,62 @@ public sealed class SelectionSystem( } } - // Ближайшее растение, чья ограничивающая окружность спрайта накрывает точку мира. Запрос - // ограничен PlantGrowth — так из пикинга выпадают спрайты рамки выделения и прочий не-контент. + // Ближайший осматриваемый объект (растение или животное), чья ограничивающая окружность спрайта + // накрывает точку мира. Запросы ограничены PlantGrowth/AnimalOrganism — так из пикинга выпадают + // спрайты рамки выделения, трупы и прочий не-контент. Из двух кандидатов берётся более близкий. private bool TryPick(Vector2 world, out int id) { id = -1; - var bestDistance = float.MaxValue; + var best = float.MaxValue; foreach ( var (transforms, sprites, _, entities) in store .Query() .Chunks ) { - var t = transforms.Span; - var s = sprites.Span; - for (var i = 0; i < t.Length; i++) - { - if (s[i].Region is not { } region) - { - continue; - } + Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best); + } - var (center, radius) = CullingMath.SpriteBoundingCircle( - in t[i], - region, - s[i].Origin - ); - var distance = Vector2.DistanceSquared(center, world); - if (distance <= radius * radius && distance < bestDistance) - { - bestDistance = distance; - id = entities.EntityAt(i).Id; - } - } + foreach ( + var (transforms, sprites, _, entities) in store + .Query() + .Chunks + ) + { + Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best); } return id >= 0; } + + // Обновляет лучшего кандидата (id/дистанцию) по чанку спрайтов: точка должна попасть в окружность. + private static void Scan( + Vector2 world, + Span transforms, + Span sprites, + ChunkEntities entities, + ref int id, + ref float best + ) + { + for (var i = 0; i < transforms.Length; i++) + { + if (sprites[i].Region is not { } region) + { + continue; + } + + var (center, radius) = CullingMath.SpriteBoundingCircle( + in transforms[i], + region, + sprites[i].Origin + ); + var distance = Vector2.DistanceSquared(center, world); + if (distance <= radius * radius && distance < best) + { + best = distance; + id = entities.EntityAt(i).Id; + } + } + } } diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs index be4673b..7d22afe 100644 --- a/src/LittleSim/UI/InspectPanel.cs +++ b/src/LittleSim/UI/InspectPanel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text; using Friflo.Engine.ECS; @@ -16,10 +17,11 @@ namespace LittleSim.UI; /// /// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта -/// () и боковая панель в левом нижнем углу с вкладками — «Обзор», -/// «Гены», «Продукты». Источник выделения — ; панель только читает компоненты -/// и обновляется каждый кадр (), оставаясь живой даже на паузе. Все строки идут -/// через (ключи inspect.*). +/// () и боковая панель в левом нижнем углу с вкладками. Набор вкладок +/// зависит от типа: растение — «Обзор/Гены/Продукты», животное — «Обзор/Нужды/Здоровье/Настроение». +/// Источник выделения — ; панель только читает компоненты и обновляется каждый +/// кадр (), оставаясь живой даже на паузе. Все строки идут через +/// (ключи inspect.*). /// internal sealed class InspectPanel { @@ -28,10 +30,18 @@ internal sealed class InspectPanel Overview, Genes, Products, + Needs, + 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 readonly EntityStore _store; private readonly PlantSet _plants; + private readonly AnimalSet _animals; + private readonly NeedSet _needs; private readonly GameContent _content; private readonly Climate _climate; private readonly Selection _selection; @@ -46,6 +56,8 @@ internal sealed class InspectPanel public InspectPanel( EntityStore store, PlantSet plants, + AnimalSet animals, + NeedSet needs, GameContent content, Climate climate, Selection selection, @@ -54,6 +66,8 @@ internal sealed class InspectPanel { _store = store; _plants = plants; + _animals = animals; + _needs = needs; _content = content; _climate = climate; _selection = selection; @@ -66,6 +80,9 @@ internal sealed class InspectPanel AddTab(tabBar, Tab.Overview, "inspect.tab.overview"); AddTab(tabBar, Tab.Genes, "inspect.tab.genes"); AddTab(tabBar, Tab.Products, "inspect.tab.products"); + AddTab(tabBar, Tab.Needs, "inspect.tab.needs"); + AddTab(tabBar, Tab.Health, "inspect.tab.health"); + AddTab(tabBar, Tab.Mood, "inspect.tab.mood"); _panel = new VerticalStackPanel { @@ -99,7 +116,7 @@ internal sealed class InspectPanel /// /// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает - /// рамку к объекту и пересобирает текст активной вкладки. Не-растения панель не показывает. + /// рамку к объекту и пересобирает текст активной вкладки. Осматриваются растения и животные. /// public void Refresh() { @@ -107,19 +124,19 @@ internal sealed class InspectPanel !_selection.HasSelection || !_store.TryGetEntityById(_selection.EntityId, out var entity) || entity.IsNull - || !entity.HasComponent() - || !entity.HasComponent() || !entity.HasComponent() || !entity.HasComponent() ) { - if (_selection.HasSelection) - { - _selection.Clear(); // сущность исчезла (растение погибло) - } + ClearSelection(); + return; + } - _panel.Visible = false; - _brackets.Hide(); + var isPlant = entity.HasComponent() && entity.HasComponent(); + var isAnimal = entity.HasComponent(); + if (!isPlant && !isAnimal) + { + ClearSelection(); // не контент (труп/прочее) — пока не осматривается return; } @@ -135,10 +152,29 @@ internal sealed class InspectPanel _brackets.Show(center, radius); } - UpdateText(entity); + if (isPlant) + { + UpdatePlant(entity); + } + else + { + UpdateAnimal(entity); + } + _panel.Visible = true; } + private void ClearSelection() + { + if (_selection.HasSelection) + { + _selection.Clear(); // сущность исчезла (растение/зверь погибли) или не осматривается + } + + _panel.Visible = false; + _brackets.Hide(); + } + private void AddTab(HorizontalStackPanel bar, Tab tab, string labelKey) { var button = new TextButton { Text = _content.Languages.Get(labelKey) }; @@ -147,7 +183,23 @@ internal sealed class InspectPanel _tabs.Add((tab, button)); } - private void UpdateText(Entity entity) + // Показывает только вкладки текущего типа, подсвечивает активную; если активная не из набора — + // откатывает на первую (Обзор) при смене типа выбранного объекта. + private void ShowTabs(Tab[] valid) + { + if (Array.IndexOf(valid, _tab) < 0) + { + _tab = valid[0]; + } + + foreach (var (tab, button) in _tabs) + { + button.Visible = Array.IndexOf(valid, tab) >= 0; + button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted; + } + } + + private void UpdatePlant(Entity entity) { var languages = _content.Languages; ref readonly var grow = ref entity.GetComponent(); @@ -157,10 +209,7 @@ internal sealed class InspectPanel var traits = org.Traits; _title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : ""); - foreach (var (tab, button) in _tabs) - { - button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted; - } + ShowTabs(PlantTabs); var text = new StringBuilder(); switch (_tab) @@ -179,6 +228,35 @@ internal sealed class InspectPanel _body.Text = text.ToString().TrimEnd(); } + private void UpdateAnimal(Entity entity) + { + var languages = _content.Languages; + ref readonly var org = ref entity.GetComponent(); + var sp = _animals[org.Species]; + + _title.Text = languages.Get(sp.Def.Label); + ShowTabs(AnimalTabs); + + var text = new StringBuilder(); + switch (_tab) + { + case Tab.Overview: + BuildAnimalOverview(text, entity, org, sp); + break; + case Tab.Needs: + BuildAnimalNeeds(text, entity); + break; + case Tab.Health: + BuildAnimalHealth(text, entity); + break; + case Tab.Mood: + BuildAnimalMood(text, entity); + break; + } + + _body.Text = text.ToString().TrimEnd(); + } + private void BuildOverview( StringBuilder text, Entity entity, @@ -296,6 +374,146 @@ internal sealed class InspectPanel } } + // — Животное: Обзор — стадия, возраст, пол/поколение, беременность. + private void BuildAnimalOverview( + StringBuilder text, + Entity entity, + in AnimalOrganism org, + AnimalSet.Species sp + ) + { + var languages = _content.Languages; + var grow = entity.GetComponent(); + var stageName = languages.Get( + "animalstage." + sp.Stages[grow.Stage].Name.ToLowerInvariant() + ); + text.AppendLine( + languages.Format("inspect.stage", stageName, grow.Stage + 1, sp.Stages.Length) + ); + text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan)); + var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female"); + text.AppendLine(languages.Format("inspect.animal.sex", sex, org.Generation)); + if (entity.TryGetComponent(out var preg)) + { + text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays)); + } + } + + // — Животное: Нужды — значение каждой нужды (голод/жажда/отдых/влечение) в процентах. + private void BuildAnimalNeeds(StringBuilder text, Entity entity) + { + var languages = _content.Languages; + var values = entity.GetComponent().Values; + if (values is null) + { + return; + } + + for (var k = 0; k < _needs.Count && k < values.Length; k++) + { + text.AppendLine( + languages.Format( + "inspect.needline", + languages.Get(_needs[k].Label), + values[k] * 100f + ) + ); + } + } + + // — Животное: Здоровье — кровь/боль, ключевые способности, активные хедифы (раны/болезни/гон). + private void BuildAnimalHealth(StringBuilder text, Entity entity) + { + var languages = _content.Languages; + var state = entity.GetComponent().State; + if (state is null) + { + text.AppendLine(languages.Get("inspect.health.none")); + return; + } + + text.AppendLine( + languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f) + ); + if (state.Parts.Length > 0) + { + text.AppendLine(languages.Get("inspect.health.caps")); + AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness"); + AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving"); + AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight"); + } + + if (state.Hediffs.Count > 0) + { + text.AppendLine(languages.Get("inspect.health.hediffs")); + foreach (var hediff in state.Hediffs) + { + text.AppendLine( + languages.Format( + "inspect.health.hediffline", + languages.Get(hediff.Def.Label), + hediff.Severity * 100f + ) + ); + } + } + else + { + text.AppendLine(languages.Get("inspect.health.none")); + } + } + + private void AppendCapacity( + StringBuilder text, + HealthState state, + string capId, + string labelKey + ) + { + var languages = _content.Languages; + text.AppendLine( + languages.Format( + "inspect.health.cap", + languages.Get(labelKey), + state.Capacity(capId) * 100f + ) + ); + } + + // — Животное: Настроение — значение и активные событийные мысли с их сдвигом (тир 4+). + private void BuildAnimalMood(StringBuilder text, Entity entity) + { + var languages = _content.Languages; + var mood = entity.GetComponent(); + if (!mood.Active) + { + text.AppendLine(languages.Get("inspect.mood.none")); + return; + } + + text.AppendLine(languages.Format("inspect.mood.value", mood.Value * 100f)); + if (mood.Thoughts is { Count: > 0 }) + { + text.AppendLine(languages.Get("inspect.mood.thoughts")); + foreach (var thought in mood.Thoughts) + { + var pct = thought.Def.MoodOffset * 100f; + var offset = (pct >= 0f ? "+" : "") + pct.ToString("0") + "%"; + text.AppendLine( + languages.Format( + "inspect.mood.thoughtline", + languages.Get(thought.Def.Label), + offset + ) + ); + } + } + else + { + text.AppendLine(languages.Get("inspect.mood.calm")); + } + } + private string StageLabel(PlantDef def, int stage) => stage < def.Stages.Count && def.Stages[stage].Label is { } key ? _content.Languages.Get(key)