diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 4555178..78c56c5 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -27,7 +27,7 @@ "inspect.state.dormanthot": "dormant — too hot", "inspect.stress": "Stress: {0:0.0} d to death", "inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C", - "inspect.genes": "— Genes —", + "inspect.genes": "Genes", "inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d", "inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}", "inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d", @@ -48,14 +48,17 @@ "skill.hunting": "hunting", "skill.evasion": "evasion", "inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%", - "inspect.health.caps": "— Capacities —", + "inspect.health.vitals": "Vitals", + "inspect.vital.blood": "blood", + "inspect.vital.pain": "pain", + "inspect.health.caps": "Capacities", "inspect.health.cap": "{0}: {1:0}%", - "inspect.health.hediffs": "— Conditions —", + "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.thoughts": "Thoughts", "inspect.mood.thoughtline": "{0}: {1}", "inspect.mood.calm": "Calm, nothing on its mind", "animalstage.baby": "baby", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index b85e7ee..16e308f 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -27,7 +27,7 @@ "inspect.state.dormanthot": "покой — слишком жарко", "inspect.stress": "Стресс: {0:0.0} дн до гибели", "inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C", - "inspect.genes": "— Гены —", + "inspect.genes": "Гены", "inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн", "inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}", "inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн", @@ -48,14 +48,17 @@ "skill.hunting": "охота", "skill.evasion": "уклонение", "inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%", - "inspect.health.caps": "— Способности —", + "inspect.health.vitals": "Жизненные показатели", + "inspect.vital.blood": "кровь", + "inspect.vital.pain": "боль", + "inspect.health.caps": "Способности", "inspect.health.cap": "{0}: {1:0}%", - "inspect.health.hediffs": "— Состояния —", + "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.thoughts": "Мысли", "inspect.mood.thoughtline": "{0}: {1}", "inspect.mood.calm": "Спокоен, особых мыслей нет", "animalstage.baby": "детёныш", diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs index 3949fe8..a48e9be 100644 --- a/src/LittleSim/UI/InspectPanel.cs +++ b/src/LittleSim/UI/InspectPanel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Friflo.Engine.ECS; using LittleSim.Content; using LittleSim.Sim; @@ -18,10 +17,13 @@ namespace LittleSim.UI; /// /// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта /// () и боковая панель в левом нижнем углу с вкладками. Набор вкладок -/// зависит от типа: растение — «Обзор/Гены/Продукты», животное — «Обзор/Нужды/Здоровье/Настроение». -/// Источник выделения — ; панель только читает компоненты и обновляется каждый -/// кадр (), оставаясь живой даже на паузе. Все строки идут через -/// (ключи inspect.*). +/// зависит от типа: растение — «Обзор/Гены/Продукты», животное — «Обзор/Нужды/Навыки/Здоровье/Настроение». +/// Источник выделения — ; панель только читает компоненты и пересобирает тело +/// активной вкладки каждый кадр (), оставаясь живой даже на паузе. Числовые +/// показатели (нужды, навыки, способности, кровь/боль, настроение) рисуются цветными полосами +/// (), описательные данные — строками с заголовками-секциями. Тело прокручивается +/// ( фиксированной высоты), поэтому панель не «прыгает» при смене вкладок. +/// Все строки идут через (ключи inspect.*). /// internal sealed class InspectPanel { @@ -46,6 +48,14 @@ internal sealed class InspectPanel Tab.Mood, ]; + // Палитра тела панели и полос. + private static readonly Color Body = new(210, 216, 226); + private static readonly Color Warn = new(220, 150, 90); + private static readonly Color Neutral = new(86, 132, 178); // нейтральная величина (навык, влечение) + private static readonly IBrush ActiveTab = new SolidBrush(new Color(40, 54, 72)); + + private const int BarWidth = 300; + private readonly EntityStore _store; private readonly PlantSet _plants; private readonly AnimalSet _animals; @@ -58,7 +68,7 @@ internal sealed class InspectPanel private readonly VerticalStackPanel _panel; private readonly Label _title; - private readonly Label _body; + private readonly VerticalStackPanel _bodyPanel; private readonly List<(Tab Tab, TextButton Button)> _tabs = []; private Tab _tab = Tab.Overview; @@ -85,9 +95,8 @@ internal sealed class InspectPanel _brackets = brackets; _title = new Label { TextColor = Ui.Accent, Wrap = true }; - _body = new Label { TextColor = new Color(210, 216, 226), Wrap = true }; - var tabBar = new HorizontalStackPanel { Spacing = 4 }; + var tabBar = new HorizontalStackPanel { Spacing = 2 }; AddTab(tabBar, Tab.Overview, "inspect.tab.overview"); AddTab(tabBar, Tab.Genes, "inspect.tab.genes"); AddTab(tabBar, Tab.Products, "inspect.tab.products"); @@ -96,11 +105,21 @@ internal sealed class InspectPanel AddTab(tabBar, Tab.Health, "inspect.tab.health"); AddTab(tabBar, Tab.Mood, "inspect.tab.mood"); + // Тело вкладки пересобирается каждый кадр; живёт в скролле фиксированной высоты, чтобы панель + // держала постоянный размер и не «прыгала» при переключении вкладок и появлении/исчезании строк. + _bodyPanel = new VerticalStackPanel { Spacing = 3 }; + var scroll = new ScrollViewer + { + Content = _bodyPanel, + Height = 280, + ShowHorizontalScrollBar = false, + }; + _panel = new VerticalStackPanel { Spacing = 6, Padding = new Thickness(14), - Width = 380, + Width = 360, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Bottom, Margin = new Thickness(12, 0, 0, 14), @@ -112,7 +131,8 @@ internal sealed class InspectPanel _panel.Widgets.Add(_title); _panel.Widgets.Add(tabBar); _panel.Widgets.Add(new HorizontalSeparator()); - _panel.Widgets.Add(_body); + _panel.Widgets.Add(scroll); + _panel.Widgets.Add(new HorizontalSeparator()); _panel.Widgets.Add( new Label { @@ -128,7 +148,7 @@ internal sealed class InspectPanel /// /// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает - /// рамку к объекту и пересобирает текст активной вкладки. Осматриваются растения и животные. + /// рамку к объекту и пересобирает тело активной вкладки. Осматриваются растения и животные. /// public void Refresh() { @@ -164,6 +184,7 @@ internal sealed class InspectPanel _brackets.Show(center, radius); } + _bodyPanel.Widgets.Clear(); if (isPlant) { UpdatePlant(entity); @@ -189,14 +210,18 @@ internal sealed class InspectPanel 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), + Padding = new Thickness(6, 3), + }; button.Click += (_, _) => _tab = tab; bar.Widgets.Add(button); _tabs.Add((tab, button)); } - // Показывает только вкладки текущего типа, подсвечивает активную; если активная не из набора — - // откатывает на первую (Обзор) при смене типа выбранного объекта. + // Показывает только вкладки текущего типа, подсвечивает активную (цвет + фон); если активная не из + // набора — откатывает на первую (Обзор) при смене типа выбранного объекта. private void ShowTabs(Tab[] valid) { if (Array.IndexOf(valid, _tab) < 0) @@ -206,11 +231,57 @@ internal sealed class InspectPanel foreach (var (tab, button) in _tabs) { - button.Visible = Array.IndexOf(valid, tab) >= 0; - button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted; + var visible = Array.IndexOf(valid, tab) >= 0; + button.Visible = visible; + var active = tab == _tab; + button.TextColor = active ? Ui.Accent : Ui.Muted; + button.Background = active ? ActiveTab : null; } } + // — Виджеты-кирпичики тела вкладки — + + private void Header(string key) => + _bodyPanel.Widgets.Add( + new Label + { + Text = _content.Languages.Get(key), + TextColor = Ui.Accent, + Margin = new Thickness(0, 6, 0, 1), + } + ); + + private void Line(string text, Color? color = null) => + _bodyPanel.Widgets.Add( + new Label + { + Text = text, + TextColor = color ?? Body, + Wrap = true, + } + ); + + // Полоса показателя: ширина заливки = доля, цвет задаётся вызывающим (см. GoodBad/Neutral). + private void Bar(string name, float fraction, Color color, string? value = null) => + _bodyPanel.Widgets.Add( + new StatBar(BarWidth, name, fraction, value ?? Pct(fraction), color) + ); + + private static string Pct(float fraction) => + ((int)MathF.Round(Math.Clamp(fraction, 0f, 1f) * 100f)).ToString() + "%"; + + // Цвет «хорошо↔плохо»: 0 — красный, 0.5 — янтарный, 1 — зелёный (приглушённые, чтобы белый текст читался). + private static Color GoodBad(float fraction) + { + fraction = Math.Clamp(fraction, 0f, 1f); + var bad = new Color(176, 72, 66); + var mid = new Color(176, 146, 58); + var good = new Color(86, 158, 96); + return fraction < 0.5f + ? Color.Lerp(bad, mid, fraction * 2f) + : Color.Lerp(mid, good, (fraction - 0.5f) * 2f); + } + private void UpdatePlant(Entity entity) { var languages = _content.Languages; @@ -223,21 +294,18 @@ internal sealed class InspectPanel _title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : ""); ShowTabs(PlantTabs); - var text = new StringBuilder(); switch (_tab) { case Tab.Overview: - BuildOverview(text, entity, grow, traits, species); + BuildOverview(entity, grow, traits, species); break; case Tab.Genes: - BuildGenes(text, traits); + BuildGenes(traits); break; case Tab.Products: - BuildProducts(text, entity, def, traits); + BuildProducts(entity, def, traits); break; } - - _body.Text = text.ToString().TrimEnd(); } private void UpdateAnimal(Entity entity) @@ -249,31 +317,27 @@ internal sealed class InspectPanel _title.Text = languages.Get(sp.Def.Label); ShowTabs(AnimalTabs); - var text = new StringBuilder(); switch (_tab) { case Tab.Overview: - BuildAnimalOverview(text, entity, org, sp); + BuildAnimalOverview(entity, org, sp); break; case Tab.Needs: - BuildAnimalNeeds(text, entity); + BuildAnimalNeeds(entity); break; case Tab.Skills: - BuildAnimalSkills(text, entity); + BuildAnimalSkills(entity); break; case Tab.Health: - BuildAnimalHealth(text, entity); + BuildAnimalHealth(entity); break; case Tab.Mood: - BuildAnimalMood(text, entity); + BuildAnimalMood(entity); break; } - - _body.Text = text.ToString().TrimEnd(); } private void BuildOverview( - StringBuilder text, Entity entity, in PlantGrowth grow, in PlantPhenotype traits, @@ -284,7 +348,7 @@ internal sealed class InspectPanel var lastStage = species.Stages.Length - 1; var mature = grow.Stage >= lastStage; - text.AppendLine( + Line( languages.Format( "inspect.stage", StageLabel(species.Def, grow.Stage), @@ -294,17 +358,17 @@ internal sealed class InspectPanel ); if (mature) { - text.AppendLine(languages.Get("inspect.mature")); + Line(languages.Get("inspect.mature")); } else { var from = species.Stages[grow.Stage].EnterDay; var to = species.Stages[grow.Stage + 1].EnterDay; - var pct = to > from ? (grow.AgeDays - from) / (to - from) * 100f : 0f; - text.AppendLine(languages.Format("inspect.growing", Math.Clamp(pct, 0f, 100f))); + var pct = to > from ? (grow.AgeDays - from) / (to - from) : 0f; + Bar(languages.Get("inspect.tab.overview"), Math.Clamp(pct, 0f, 1f), Neutral); } - text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan)); + Line(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan)); // Вес растения (∝ кубу размера текущей стадии относительно зрелой). var plantMass = Mass.OfPlant( @@ -312,7 +376,7 @@ internal sealed class InspectPanel species.Stages[grow.Stage].SizeCells, species.Stages[^1].SizeCells ); - text.AppendLine(languages.Format("inspect.plantmass", plantMass)); + Line(languages.Format("inspect.plantmass", plantMass)); // Состояние по температуре (рост/покой) и накопленный стресс. var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); @@ -324,29 +388,25 @@ internal sealed class InspectPanel temperature <= tMin ? "inspect.state.dormantcold" : "inspect.state.dormanthot" ) : languages.Get(mature ? "inspect.state.mature" : "inspect.state.growing"); - text.AppendLine(languages.Format("inspect.state", state)); + Line(languages.Format("inspect.state", state)); if (entity.TryGetComponent(out var stress) && stress.Days > 0.05f) { - text.AppendLine(languages.Format("inspect.stress", stress.Days)); + Line(languages.Format("inspect.stress", stress.Days), Warn); } - text.AppendLine(languages.Format("inspect.temp", tMin, tMax, tLow, tHigh)); + Line(languages.Format("inspect.temp", tMin, tMax, tLow, tHigh)); } - private void BuildGenes(StringBuilder text, in PlantPhenotype traits) + private void BuildGenes(in PlantPhenotype traits) { var languages = _content.Languages; - text.AppendLine(languages.Format("inspect.gene.vigor", traits.Vigor, traits.Lifespan)); - text.AppendLine( - languages.Format("inspect.gene.env", traits.OptimalLight, traits.OptimalFertility) - ); - text.AppendLine( + Line(languages.Format("inspect.gene.vigor", traits.Vigor, traits.Lifespan)); + Line(languages.Format("inspect.gene.env", traits.OptimalLight, traits.OptimalFertility)); + Line( languages.Format("inspect.gene.repro", traits.DispersalRange, traits.ReproduceInterval) ); - text.AppendLine( - languages.Format("inspect.gene.repro2", traits.SelfPollination, traits.MutationRate) - ); - text.AppendLine( + Line(languages.Format("inspect.gene.repro2", traits.SelfPollination, traits.MutationRate)); + Line( languages.Format( "inspect.gene.hardy", traits.ColdHardiness, @@ -356,30 +416,21 @@ internal sealed class InspectPanel ); if (traits.Toxicity > 0.01f || traits.Thorns > 0.01f) { - text.AppendLine( - languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns) - ); + Line(languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns), Warn); } if (traits.IsVariant) { - text.AppendLine(languages.Get("inspect.gene.variant")); + Line(languages.Get("inspect.gene.variant")); } } - private void BuildProducts( - StringBuilder text, - Entity entity, - PlantDef def, - in PlantPhenotype traits - ) + private void BuildProducts(Entity entity, PlantDef def, in PlantPhenotype traits) { var languages = _content.Languages; if (def.HarvestProduct is { } harvest) { - text.AppendLine( - languages.Format("inspect.harvest", ProductLabel(harvest), traits.HarvestAmount) - ); + Line(languages.Format("inspect.harvest", ProductLabel(harvest), traits.HarvestAmount)); } if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f) @@ -388,7 +439,7 @@ internal sealed class InspectPanel var season = languages.Get( "season." + ((Season)traits.FruitSeason).ToString().ToLowerInvariant() ); - text.AppendLine( + Line( languages.Format( "inspect.fruit", ProductLabel(fruit), @@ -400,29 +451,22 @@ internal sealed class InspectPanel } else { - text.AppendLine(languages.Get("inspect.barren")); + Line(languages.Get("inspect.barren")); } } - // — Животное: Обзор — стадия, возраст, пол/поколение, беременность. - private void BuildAnimalOverview( - StringBuilder text, - Entity entity, - in AnimalOrganism org, - AnimalSet.Species sp - ) + // — Животное: Обзор — стадия, возраст, пол/поколение, вес, беременность. + private void BuildAnimalOverview(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)); + Line(languages.Format("inspect.stage", stageName, grow.Stage + 1, sp.Stages.Length)); + Line(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)); + Line(languages.Format("inspect.animal.sex", sex, org.Generation)); // Вес тела и грузоподъёмность (∝ массе × способность нести; четвероногие — через Moving). var stageScale = sp.Stages[grow.Stage].Scale; @@ -431,16 +475,16 @@ internal sealed class InspectPanel entity.TryGetComponent(out var health) && health.State is not null ? Mass.CarryCapacity(mass, health.State.Capacity(AnimalCapacities.Moving)) : Mass.CarryCapacity(mass, 1f); - text.AppendLine(languages.Format("inspect.mass", mass, carryCap)); + Line(languages.Format("inspect.mass", mass, carryCap)); if (entity.TryGetComponent(out var preg)) { - text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays)); + Line(languages.Format("inspect.animal.pregnant", preg.DueInDays), Ui.Accent); } } - // — Животное: Нужды — значение каждой нужды (голод/жажда/отдых/влечение) в процентах. - private void BuildAnimalNeeds(StringBuilder text, Entity entity) + // — Животное: Нужды — полоса на каждую нужду; убывающие красят «хорошо↔плохо», влечения — нейтрально. + private void BuildAnimalNeeds(Entity entity) { var languages = _content.Languages; var values = entity.GetComponent().Values; @@ -451,23 +495,19 @@ internal sealed class InspectPanel 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 - ) - ); + var v = values[k]; + var color = _needs.IsDrive(k) ? Neutral : GoodBad(v); + Bar(languages.Get(_needs[k].Label), v, color); } } - // — Животное: Навыки — уровень каждого навыка (%) и страсть (★/★★), растут от практики. - private void BuildAnimalSkills(StringBuilder text, Entity entity) + // — Животное: Навыки — полоса уровня (нейтральный цвет) + страсть (★/★★) в подписи. + private void BuildAnimalSkills(Entity entity) { var languages = _content.Languages; if (!entity.TryGetComponent(out var sk) || sk.Levels is null) { - text.AppendLine(languages.Get("inspect.health.none")); + Line(languages.Get("inspect.health.none")); return; } @@ -477,45 +517,41 @@ internal sealed class InspectPanel sk.Passions is not null && i < sk.Passions.Length ? sk.Passions[i] : (byte)0; var stars = passion switch { - 2 => "★★", - 1 => "★", + 2 => " ★★", + 1 => " ★", _ => "", }; - text.AppendLine( - languages.Format( - "inspect.skillline", - languages.Get(_skills[i].Label), - sk.Levels[i] * 100f, - stars - ) - ); + Bar(languages.Get(_skills[i].Label) + stars, sk.Levels[i], Neutral); } } - // — Животное: Здоровье — кровь/боль, ключевые способности, активные хедифы (раны/болезни/гон). - private void BuildAnimalHealth(StringBuilder text, Entity entity) + // — Животное: Здоровье — полосы жизненных показателей, способностей и активных состояний. + private void BuildAnimalHealth(Entity entity) { var languages = _content.Languages; var state = entity.GetComponent().State; if (state is null) { - text.AppendLine(languages.Get("inspect.health.none")); + Line(languages.Get("inspect.health.none")); return; } - text.AppendLine( - languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f) - ); + Header("inspect.health.vitals"); + Bar(languages.Get("inspect.vital.blood"), state.BloodLevel, GoodBad(state.BloodLevel)); + // Боль: длина = уровень боли, цвет инвертирован (много боли — красный). + Bar(languages.Get("inspect.vital.pain"), state.Pain, GoodBad(1f - state.Pain)); + if (state.Capacities.Count > 0) { - text.AppendLine(languages.Get("inspect.health.caps")); + Header("inspect.health.caps"); // Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть. var shown = new HashSet(StringComparer.Ordinal); foreach (var capId in AnimalCapacities.DisplayOrder) { if (state.Capacities.ContainsKey(capId)) { - AppendCapacity(text, state, capId, CapacityLabel(capId)); + var v = state.Capacity(capId); + Bar(languages.Get(CapacityLabel(capId)), v, GoodBad(v)); shown.Add(capId); } } @@ -524,82 +560,65 @@ internal sealed class InspectPanel { if (shown.Add(capId)) { - AppendCapacity(text, state, capId, CapacityLabel(capId)); + var v = state.Capacity(capId); + Bar(languages.Get(CapacityLabel(capId)), v, GoodBad(v)); } } } + Header("inspect.health.hediffs"); 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 - ) + Bar( + languages.Get(hediff.Def.Label), + hediff.Severity, + GoodBad(1f - hediff.Severity) ); } } else { - text.AppendLine(languages.Get("inspect.health.none")); + Line(languages.Get("inspect.health.none")); } } // Ключ локализации метки способности: cap. (cap.consciousness, cap.bloodpumping…). private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant(); - 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) + // — Животное: Настроение — полоса значения и активные событийные мысли с их сдвигом (тир 4+). + private void BuildAnimalMood(Entity entity) { var languages = _content.Languages; var mood = entity.GetComponent(); if (!mood.Active) { - text.AppendLine(languages.Get("inspect.mood.none")); + Line(languages.Get("inspect.mood.none")); return; } - text.AppendLine(languages.Format("inspect.mood.value", mood.Value * 100f)); + Bar(languages.Get("inspect.tab.mood"), mood.Value, GoodBad(mood.Value)); if (mood.Thoughts is { Count: > 0 }) { - text.AppendLine(languages.Get("inspect.mood.thoughts")); + Header("inspect.mood.thoughts"); foreach (var thought in mood.Thoughts) { var pct = thought.Def.MoodOffset * 100f; var offset = (pct >= 0f ? "+" : "") + pct.ToString("0") + "%"; - text.AppendLine( + Line( languages.Format( "inspect.mood.thoughtline", languages.Get(thought.Def.Label), offset - ) + ), + pct >= 0f ? Body : Warn ); } } else { - text.AppendLine(languages.Get("inspect.mood.calm")); + Line(languages.Get("inspect.mood.calm")); } } @@ -613,3 +632,62 @@ internal sealed class InspectPanel ? _content.Languages.Get(product.Label) : productDefName; } + +/// +/// Полоса-индикатор показателя в стиле RimWorld: тёмная дорожка фиксированной ширины, цветная заливка +/// длиной fraction, поверх — подпись слева и значение справа. Самодостаточный виджет (ничего не +/// обновляет — пересоздаётся при каждой пересборке тела панели), цвет заливки задаёт вызывающий. +/// +internal sealed class StatBar : Panel +{ + private static readonly IBrush Track = new SolidBrush(new Color(28, 34, 44)); + private static readonly IBrush TrackBorder = new SolidBrush(new Color(64, 78, 98)); + private const int BarHeight = 22; + + public StatBar(int width, string name, float fraction, string value, Color fill) + { + Width = width; + Height = BarHeight; + Background = Track; + Border = TrackBorder; + BorderThickness = new Thickness(1); + + var fillWidth = (int)MathF.Round((width - 2) * Math.Clamp(fraction, 0f, 1f)); + if (fillWidth > 0) + { + Widgets.Add( + new Panel + { + Background = new SolidBrush(fill), + Width = fillWidth, + Height = BarHeight - 2, + Left = 1, + Top = 1, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + } + ); + } + + Widgets.Add( + new Label + { + Text = name, + TextColor = Color.White, + Left = 8, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + } + ); + Widgets.Add( + new Label + { + Text = value, + TextColor = Color.White, + Margin = new Thickness(0, 0, 8, 0), + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + } + ); + } +}