Животные A8 часть 2: инспектор животных (вкладки нужды/здоровье/настроение)

InspectPanel осматривает и животных: вкладки Обзор (стадия/возраст/пол/поколение/беременность), Нужды, Здоровье (кровь/боль, способности, хедифы), Настроение (значение + мысли). Набор вкладок по типу выбранного. SelectionSystem пикает и животных (запрос расширен PlantGrowth → +AnimalOrganism). Локализация inspect.*/need.*/hediff.*/cap.*/animalstage.* ru+en. Базовый план зоологии A1-A8 закрыт. Сборка + --check-content чистые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 06:19:23 +03:00
co-authored by Claude Opus 4.8
parent 1b12831817
commit 003f235440
6 changed files with 367 additions and 45 deletions
+32
View File
@@ -10,6 +10,9 @@
"inspect.tab.overview": "Overview", "inspect.tab.overview": "Overview",
"inspect.tab.genes": "Genes", "inspect.tab.genes": "Genes",
"inspect.tab.products": "Products", "inspect.tab.products": "Products",
"inspect.tab.needs": "Needs",
"inspect.tab.health": "Health",
"inspect.tab.mood": "Mood",
"inspect.stage": "Stage: {0} ({1}/{2})", "inspect.stage": "Stage: {0} ({1}/{2})",
"inspect.growing": "Growth: {0:0}% to next stage", "inspect.growing": "Growth: {0:0}% to next stage",
"inspect.mature": "Fully grown", "inspect.mature": "Fully grown",
@@ -31,6 +34,35 @@
"inspect.harvest": "Harvest: {0} ×{1:0.#}", "inspect.harvest": "Harvest: {0} ×{1:0.#}",
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}", "inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
"inspect.barren": "Bears no fruit", "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", "inspect.hint": "LMB — select · RMB/Esc — clear",
"hud.paused": "PAUSED", "hud.paused": "PAUSED",
"menu.title": "LittleSim", "menu.title": "LittleSim",
+32
View File
@@ -10,6 +10,9 @@
"inspect.tab.overview": "Обзор", "inspect.tab.overview": "Обзор",
"inspect.tab.genes": "Гены", "inspect.tab.genes": "Гены",
"inspect.tab.products": "Продукты", "inspect.tab.products": "Продукты",
"inspect.tab.needs": "Нужды",
"inspect.tab.health": "Здоровье",
"inspect.tab.mood": "Настроение",
"inspect.stage": "Стадия: {0} ({1}/{2})", "inspect.stage": "Стадия: {0} ({1}/{2})",
"inspect.growing": "Рост: {0:0}% до следующей стадии", "inspect.growing": "Рост: {0:0}% до следующей стадии",
"inspect.mature": "Полностью выросло", "inspect.mature": "Полностью выросло",
@@ -31,6 +34,35 @@
"inspect.harvest": "Сбор: {0} ×{1:0.#}", "inspect.harvest": "Сбор: {0} ×{1:0.#}",
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}", "inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
"inspect.barren": "Не плодоносит", "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 — снять", "inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
"hud.paused": "ПАУЗА", "hud.paused": "ПАУЗА",
"menu.title": "LittleSim", "menu.title": "LittleSim",
+11 -2
View File
@@ -1,7 +1,16 @@
# Животные (зоология) # Животные (зоология)
> Статус: **A1A7 + A8 часть 1 (ядро настроения) реализованы**, осталось A8 часть 2 (инспектор). Это > Статус: **A1A8 реализованы целиком** — базовый план зоологии закрыт; дальше экологический горизонт
> живой план — правится по мере реализации фаз A1–A8. Стиль и правила как в [концепте](concept.md) и [модах](mods.md). > (хищники, опека, эпидемии, миграция) и бэклог реализма. Это живой план — стиль и правила как в
> [концепте](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` (мысли: роды/ > **A8 часть 1 (готово): настроение — ядро.** Тип дефа `ThoughtDef` + `thoughts.json` (мысли: роды/
> спаривание/утоление жажды — сила и длительность данными); компонент `Mood` (значение 0..1, активность по > спаривание/утоление жажды — сила и длительность данными); компонент `Mood` (значение 0..1, активность по
+10 -1
View File
@@ -163,7 +163,16 @@ public sealed class WorldScene : Scene
atlases.GetRegion(device, "ui/overlays/selectionbracket"), atlases.GetRegion(device, "ui/overlays/selectionbracket"),
GameLayers.Selection 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); desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root);
this.UseInspector(renderer); this.UseInspector(renderer);
+45 -23
View File
@@ -1,3 +1,4 @@
using System;
using Friflo.Engine.ECS; using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems; using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@@ -74,41 +75,62 @@ public sealed class SelectionSystem(
} }
} }
// Ближайшее растение, чья ограничивающая окружность спрайта накрывает точку мира. Запрос // Ближайший осматриваемый объект (растение или животное), чья ограничивающая окружность спрайта
// ограничен PlantGrowth — так из пикинга выпадают спрайты рамки выделения и прочий не-контент. // накрывает точку мира. Запросы ограничены PlantGrowth/AnimalOrganism — так из пикинга выпадают
// спрайты рамки выделения, трупы и прочий не-контент. Из двух кандидатов берётся более близкий.
private bool TryPick(Vector2 world, out int id) private bool TryPick(Vector2 world, out int id)
{ {
id = -1; id = -1;
var bestDistance = float.MaxValue; var best = float.MaxValue;
foreach ( foreach (
var (transforms, sprites, _, entities) in store var (transforms, sprites, _, entities) in store
.Query<Transform2D, Sprite, PlantGrowth>() .Query<Transform2D, Sprite, PlantGrowth>()
.Chunks .Chunks
) )
{ {
var t = transforms.Span; Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best);
var s = sprites.Span; }
for (var i = 0; i < t.Length; i++)
{
if (s[i].Region is not { } region)
{
continue;
}
var (center, radius) = CullingMath.SpriteBoundingCircle( foreach (
in t[i], var (transforms, sprites, _, entities) in store
region, .Query<Transform2D, Sprite, AnimalOrganism>()
s[i].Origin .Chunks
); )
var distance = Vector2.DistanceSquared(center, world); {
if (distance <= radius * radius && distance < bestDistance) Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best);
{
bestDistance = distance;
id = entities.EntityAt(i).Id;
}
}
} }
return id >= 0; return id >= 0;
} }
// Обновляет лучшего кандидата (id/дистанцию) по чанку спрайтов: точка должна попасть в окружность.
private static void Scan(
Vector2 world,
Span<Transform2D> transforms,
Span<Sprite> 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;
}
}
}
} }
+237 -19
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Friflo.Engine.ECS; using Friflo.Engine.ECS;
@@ -16,10 +17,11 @@ namespace LittleSim.UI;
/// <summary> /// <summary>
/// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта /// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта
/// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками — «Обзор», /// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками. Набор вкладок
/// «Гены», «Продукты». Источник выделения — <see cref="Selection"/>; панель только читает компоненты /// зависит от типа: растение — «Обзор/Гены/Продукты», животное — «Обзор/Нужды/Здоровье/Настроение».
/// и обновляется каждый кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут /// Источник выделения — <see cref="Selection"/>; панель только читает компоненты и обновляется каждый
/// через <see cref="LanguageManager"/> (ключи <c>inspect.*</c>). /// кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут через
/// <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
/// </summary> /// </summary>
internal sealed class InspectPanel internal sealed class InspectPanel
{ {
@@ -28,10 +30,18 @@ internal sealed class InspectPanel
Overview, Overview,
Genes, Genes,
Products, 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 EntityStore _store;
private readonly PlantSet _plants; private readonly PlantSet _plants;
private readonly AnimalSet _animals;
private readonly NeedSet _needs;
private readonly GameContent _content; private readonly GameContent _content;
private readonly Climate _climate; private readonly Climate _climate;
private readonly Selection _selection; private readonly Selection _selection;
@@ -46,6 +56,8 @@ internal sealed class InspectPanel
public InspectPanel( public InspectPanel(
EntityStore store, EntityStore store,
PlantSet plants, PlantSet plants,
AnimalSet animals,
NeedSet needs,
GameContent content, GameContent content,
Climate climate, Climate climate,
Selection selection, Selection selection,
@@ -54,6 +66,8 @@ internal sealed class InspectPanel
{ {
_store = store; _store = store;
_plants = plants; _plants = plants;
_animals = animals;
_needs = needs;
_content = content; _content = content;
_climate = climate; _climate = climate;
_selection = selection; _selection = selection;
@@ -66,6 +80,9 @@ internal sealed class InspectPanel
AddTab(tabBar, Tab.Overview, "inspect.tab.overview"); AddTab(tabBar, Tab.Overview, "inspect.tab.overview");
AddTab(tabBar, Tab.Genes, "inspect.tab.genes"); AddTab(tabBar, Tab.Genes, "inspect.tab.genes");
AddTab(tabBar, Tab.Products, "inspect.tab.products"); 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 _panel = new VerticalStackPanel
{ {
@@ -99,7 +116,7 @@ internal sealed class InspectPanel
/// <summary> /// <summary>
/// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает /// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает
/// рамку к объекту и пересобирает текст активной вкладки. Не-растения панель не показывает. /// рамку к объекту и пересобирает текст активной вкладки. Осматриваются растения и животные.
/// </summary> /// </summary>
public void Refresh() public void Refresh()
{ {
@@ -107,19 +124,19 @@ internal sealed class InspectPanel
!_selection.HasSelection !_selection.HasSelection
|| !_store.TryGetEntityById(_selection.EntityId, out var entity) || !_store.TryGetEntityById(_selection.EntityId, out var entity)
|| entity.IsNull || entity.IsNull
|| !entity.HasComponent<PlantGrowth>()
|| !entity.HasComponent<PlantOrganism>()
|| !entity.HasComponent<Transform2D>() || !entity.HasComponent<Transform2D>()
|| !entity.HasComponent<Sprite>() || !entity.HasComponent<Sprite>()
) )
{ {
if (_selection.HasSelection) ClearSelection();
{ return;
_selection.Clear(); // сущность исчезла (растение погибло) }
}
_panel.Visible = false; var isPlant = entity.HasComponent<PlantGrowth>() && entity.HasComponent<PlantOrganism>();
_brackets.Hide(); var isAnimal = entity.HasComponent<AnimalOrganism>();
if (!isPlant && !isAnimal)
{
ClearSelection(); // не контент (труп/прочее) — пока не осматривается
return; return;
} }
@@ -135,10 +152,29 @@ internal sealed class InspectPanel
_brackets.Show(center, radius); _brackets.Show(center, radius);
} }
UpdateText(entity); if (isPlant)
{
UpdatePlant(entity);
}
else
{
UpdateAnimal(entity);
}
_panel.Visible = true; _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) private void AddTab(HorizontalStackPanel bar, Tab tab, string labelKey)
{ {
var button = new TextButton { Text = _content.Languages.Get(labelKey) }; var button = new TextButton { Text = _content.Languages.Get(labelKey) };
@@ -147,7 +183,23 @@ internal sealed class InspectPanel
_tabs.Add((tab, button)); _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; var languages = _content.Languages;
ref readonly var grow = ref entity.GetComponent<PlantGrowth>(); ref readonly var grow = ref entity.GetComponent<PlantGrowth>();
@@ -157,10 +209,7 @@ internal sealed class InspectPanel
var traits = org.Traits; var traits = org.Traits;
_title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : ""); _title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : "");
foreach (var (tab, button) in _tabs) ShowTabs(PlantTabs);
{
button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted;
}
var text = new StringBuilder(); var text = new StringBuilder();
switch (_tab) switch (_tab)
@@ -179,6 +228,35 @@ internal sealed class InspectPanel
_body.Text = text.ToString().TrimEnd(); _body.Text = text.ToString().TrimEnd();
} }
private void UpdateAnimal(Entity entity)
{
var languages = _content.Languages;
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
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( private void BuildOverview(
StringBuilder text, StringBuilder text,
Entity entity, 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<AnimalGrowth>();
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<Pregnant>(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<AnimalNeeds>().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<Health>().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<Mood>();
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) => private string StageLabel(PlantDef def, int stage) =>
stage < def.Stages.Count && def.Stages[stage].Label is { } key stage < def.Stages.Count && def.Stages[stage].Label is { } key
? _content.Languages.Get(key) ? _content.Languages.Get(key)