diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index 57d19a1..f9fe0da 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -210,30 +210,31 @@ "hud.perf": "Perf", "events.title": "Event log", "events.empty": "Nothing has happened yet", + "events.hint": "Double-click — jump to the creature", "perf.title": "Performance", "perf.fps": "{0} FPS · {1:0.0} ms/frame", "perf.root": "{0}: {1:0.000} ms · systems {2}", "perf.systemline": "{0} {1:0.000} ms {2:0}%", "perf.hint": "Refreshes ~5/s · costliest systems shown", - "event.mated": "{0} mated with {1}", - "event.pregnant": "{0} became pregnant", - "event.birth": "{0} gave birth ×{1}", - "event.hatch": "{0}: eggs hatched ×{1}", - "event.hunt": "{0} started hunting: target — {1}", - "event.injured": "{0} wounded: {1}", - "event.struck": "{0} struck {1} (dmg {2:0})", - "event.tookhit": "{0}: hit by {1} (dmg {2:0})", - "event.killed": "{0} killed prey: {1}", - "event.ate": "{0} ate", - "event.drank": "{0} drank", + "event.mated": "Pair: {0} + {1}", + "event.pregnant": "{0}: pregnant", + "event.birth": "{0}: birth ×{1}", + "event.hatch": "{0}: hatched ×{1}", + "event.hunt": "Hunt: {0} → {1}", + "event.injured": "Wound: {0} → {1}", + "event.struck": "Hit: {0} → {1} ({2:0})", + "event.tookhit": "Hit: {1} → {0} ({2:0})", + "event.killed": "Kill: {0} → {1}", + "event.ate": "{0} is feeding", + "event.drank": "{0} is drinking", "event.rest": "{0} is resting", "event.dig": "{0} digs for food", "event.wallow": "{0} wallows in mud", "event.fled": "{0} is fleeing", "event.grew": "{0}: new stage — {1}", - "event.death.starve": "{0} died of hunger/thirst", - "event.death.age": "{0} died of old age", - "event.death.disease": "{0} died of disease", + "event.death.starve": "Death: {0} (hunger/thirst)", + "event.death.age": "Death: {0} (old age)", + "event.death.disease": "Death: {0} (disease)", "net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu", "net.connecting": "connecting…", "net.connected": "connected", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 4c18e11..52285e8 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -210,30 +210,31 @@ "hud.perf": "Произв.", "events.title": "Журнал событий", "events.empty": "Пока ничего не произошло", + "events.hint": "Двойной клик — перейти к существу", "perf.title": "Производительность", "perf.fps": "{0} FPS · {1:0.0} мс/кадр", "perf.root": "{0}: {1:0.000} мс · систем {2}", "perf.systemline": "{0} {1:0.000} мс {2:0}%", "perf.hint": "Обновляется ~5 раз/с · показаны самые дорогие системы", - "event.mated": "{0} спарилась с {1}", - "event.pregnant": "{0} забеременела", - "event.birth": "{0} принесла потомство ×{1}", - "event.hatch": "{0}: вылупились детёныши ×{1}", - "event.hunt": "{0} начала охоту: цель — {1}", - "event.injured": "{0} ранила: {1}", - "event.struck": "{0} ударил {1} (урон {2:0})", - "event.tookhit": "{0}: удар от {1} (урон {2:0})", - "event.killed": "{0} убила добычу: {1}", - "event.ate": "{0} поела", - "event.drank": "{0} попила", + "event.mated": "Пара: {0} + {1}", + "event.pregnant": "{0}: беременность", + "event.birth": "{0}: роды ×{1}", + "event.hatch": "{0}: вылупление ×{1}", + "event.hunt": "Охота: {0} → {1}", + "event.injured": "Ранение: {0} → {1}", + "event.struck": "Удар: {0} → {1} ({2:0})", + "event.tookhit": "Удар: {1} → {0} ({2:0})", + "event.killed": "Добыча: {0} → {1}", + "event.ate": "{0} кормится", + "event.drank": "{0} пьёт воду", "event.rest": "{0} отдыхает", "event.dig": "{0} роет землю в поисках корма", "event.wallow": "{0} валяется в грязи", "event.fled": "{0} спасается бегством", "event.grew": "{0}: новая стадия — {1}", - "event.death.starve": "{0} погибла от голода/жажды", - "event.death.age": "{0} умерла от старости", - "event.death.disease": "{0} погибла от болезни", + "event.death.starve": "Гибель: {0} (голод/жажда)", + "event.death.age": "Гибель: {0} (старость)", + "event.death.disease": "Гибель: {0} (болезнь)", "net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню", "net.connecting": "подключение…", "net.connected": "подключено", diff --git a/engine b/engine index 96e19c7..46f3931 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit 96e19c7c6151152c29c5c5478d365983d3df4069 +Subproject commit 46f3931f85d76a866203584311e217578bc8251e diff --git a/src/LittleSim/Scenes/ContentHelpers.cs b/src/LittleSim/Scenes/ContentHelpers.cs index 07f90f0..cb33af2 100644 --- a/src/LittleSim/Scenes/ContentHelpers.cs +++ b/src/LittleSim/Scenes/ContentHelpers.cs @@ -2,20 +2,27 @@ using Friflo.Engine.ECS; using LittleSim.Content; using LittleSim.Sim; using Microsoft.Xna.Framework; +using MrGameEng.AI; using MrGameEng.Core; using MrGameEng.DevConsole; using MrGameEng.Graphics; +using MrGameEng.Lighting; namespace LittleSim.Scenes; /// Спавн растительности по скаттеру дефов террейна. internal static class ScatterSpawner { + // Ниже этой световой пригодности (при полном дневном свете клетки) растение не сеем: теневому виду + // (грибы, optimalLight≈0.15) в открытой клетке не место — без полога крон ему тут не выжить, поэтому + // он и не должен появляться, пока такой ниши нет. Солнечные виды (≥0.7 пригодности) проходят свободно. + private const float ShadeSpawnThreshold = 0.15f; + /// /// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со /// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации - /// разновозрастный. Стадия и спрайт берутся из , дальше растением - /// управляет . + /// разновозрастный. Теневые виды не сеются в ярко освещённые клетки (см. ). + /// Стадия и спрайт берутся из , дальше растением управляет . /// public static void Spawn( Scene scene, @@ -27,6 +34,10 @@ internal static class ScatterSpawner Random random ) { + // Статичная освещённость клетки в полдень: затенена только светонепроницаемым террейном (полог + // крон сейчас не учитывается). По ней решаем, годится ли клетка теневому виду. + var exposure = terrain.BlocksLight ? LightmapBuilder.OccluderShade : 1f; + foreach (var entry in terrain.Scatter) { if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance) @@ -35,6 +46,14 @@ internal static class ScatterSpawner } var def = content.Defs.Get(entry.Options[random.Next(entry.Options.Count)]); + if ( + Suitability.Gaussian(exposure, def.Genome.OptimalLight, def.Genome.LightTolerance) + < ShadeSpawnThreshold + ) + { + continue; // теневому виду в ярко освещённой клетке не место + } + var index = plants.IndexOf(def); var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize; var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize; diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 038e043..14a8de5 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -247,8 +247,14 @@ public sealed class WorldScene : Scene hudChildren.Add(spawner.Panel); } - // Журнал событий — доступен всем (кнопка «События»). - _eventsPanel = new EventsPanel(content.Languages, _events); + // Журнал событий — доступен всем (кнопка «События»). Двойной клик по записи ведёт камеру к + // «ответственному» и выбирает его (если он ещё жив). + _eventsPanel = new EventsPanel( + content.Languages, + _events, + Context.Clock, + id => FocusEntity(camera, id) + ); hudChildren.Add(_eventsPanel.Panel); hudButtons.Widgets.Add( HudToggle( @@ -555,6 +561,25 @@ public sealed class WorldScene : Scene Store.CreateEntity(new Tilemap(grid, tiles, CellSize)); } + // Переход к «ответственному» за событие (двойной клик в журнале): выбирает сущность и центрирует на + // ней камеру, если она ещё существует (актёр мог умереть). Камера зажимается к границам при отрисовке. + private void FocusEntity(Entity camera, int id) + { + if ( + id < 0 + || !Store.TryGetEntityById(id, out var target) + || target.IsNull + || !target.HasComponent() + ) + { + return; + } + + _selection.Select(id); + ref var cam = ref camera.GetComponent(); + cam.Position = target.GetComponent().Position; + } + // Кнопка-переключатель оверлея HUD: подсвечивается как активная, пока оверлей открыт. private static TextButton HudToggle(string text, Action onClick, Func isOpen) { @@ -575,7 +600,13 @@ public sealed class WorldScene : Scene bar.VerticalAlignment = VerticalAlignment.Bottom; bar.Margin = new Thickness(0, 0, 0, 12); - var pause = new TextButton { Text = content.Languages.Get("speed.pause") }; + var hover = new SolidBrush(new Color(40, 54, 72)); // подсветка кнопок скорости при наведении + var pause = new TextButton + { + Text = content.Languages.Get("speed.pause"), + Padding = new Thickness(8, 4), + OverBackground = hover, + }; pause.Click += (_, _) => _speed.Pause(); bar.Widgets.Add(pause); _speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted); @@ -583,7 +614,12 @@ public sealed class WorldScene : Scene for (var i = 0; i < _speed.Steps.Count; i++) { var index = i; - var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" }; + var button = new TextButton + { + Text = $"x{_speed.Steps[i]:0}", + Padding = new Thickness(8, 4), + OverBackground = hover, + }; button.Click += (_, _) => _speed.SetStep(index); bar.Widgets.Add(button); _speedRefreshers.Add(() => diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 6cb5195..3b7e166 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -638,6 +638,7 @@ public sealed class AnimalDecisionSystem : BaseSystem && TryFindMate( pos, o[i].IsMale, + o[i].Species, seeRadius, out var mateId, out var matePos @@ -1333,10 +1334,11 @@ public sealed class AnimalDecisionSystem : BaseSystem return true; } - // Ближайший подходящий партнёр в радиусе зрения: взрослый, противоположного пола, с влечением > 0.5. + // Ближайший подходящий партнёр в радиусе зрения: ТОГО ЖЕ вида, взрослый, противоположного пола. private bool TryFindMate( Vector2 from, bool selfMale, + int selfSpecies, float radius, out int mateId, out Vector2 position @@ -1348,13 +1350,13 @@ public sealed class AnimalDecisionSystem : BaseSystem // _animalBuf уже собран в DecideIn на макс. радиусе чувств — фильтруем по радиусу спаривания. foreach (var e in _animalBuf) { - if (e.IsMale == selfMale || !e.IsAdult) + if (e.Species != selfSpecies || e.IsMale == selfMale || !e.IsAdult) { - continue; // тот же пол / не взрослый (self отсеивается по полу) + continue; // другой вид / тот же пол / не взрослый (self отсеивается по полу) } - // Влечение партнёра не проверяется (учёт придёт с половым отбором); вид не фильтруется — - // межвидовые пары возможны, как и раньше. self отсекается совпадением пола и sq≈0. + // Влечение партнёра не проверяется (учёт придёт с половым отбором). self отсекается совпадением + // пола и sq≈0; межвидовые пары исключены (раньше волк/курица могли «спариться»). var sq = Vector2.DistanceSquared(from, e.Pos); if (sq > 0.01f && (sq < bestSq || (sq == bestSq && e.Id < mateId))) { diff --git a/src/LittleSim/Sim/WorldEvents.cs b/src/LittleSim/Sim/WorldEvents.cs index 20a8028..9745093 100644 --- a/src/LittleSim/Sim/WorldEvents.cs +++ b/src/LittleSim/Sim/WorldEvents.cs @@ -22,13 +22,18 @@ public enum EventCategory : byte Routine, } -/// Одна запись журнала: готовая (локализованная) строка, категория и отметка времени календаря. +/// +/// Одна запись журнала: готовая (локализованная) строка, категория, отметка времени календаря и id +/// «ответственной» сущности (для перехода камерой/выбора по двойному клику); -1 — не к кому переходить +/// (напр. смерть — особь исчезла). +/// public readonly record struct LogEntry( string Text, EventCategory Category, int Day, int Hour, - int Minute + int Minute, + int SubjectId ); /// @@ -72,7 +77,7 @@ public sealed class WorldEvents public void Mated(Entity a, int aSpecies, Entity b, int bSpecies) { var text = _lang.Format("event.mated", Label(aSpecies), Label(bSpecies)); - Global(EventCategory.Reproduction, text); + Global(EventCategory.Reproduction, text, a.Id); Creature(a, EventCategory.Reproduction, text); Creature( b, @@ -84,32 +89,32 @@ public sealed class WorldEvents public void Pregnant(Entity mother, int species) { var text = _lang.Format("event.pregnant", Label(species)); - Global(EventCategory.Reproduction, text); + Global(EventCategory.Reproduction, text, mother.Id); Creature(mother, EventCategory.Reproduction, text); } public void Born(Entity mother, int species, int litter) { var text = _lang.Format("event.birth", Label(species), litter); - Global(EventCategory.Reproduction, text); + Global(EventCategory.Reproduction, text, mother.Id); Creature(mother, EventCategory.Reproduction, text); } public void Hatched(int species, int count) => - Global(EventCategory.Reproduction, _lang.Format("event.hatch", Label(species), count)); + Global(EventCategory.Reproduction, _lang.Format("event.hatch", Label(species), count), -1); // — Охота и бой — public void HuntStarted(Entity hunter, int hunterSpecies, int preySpecies) { var text = _lang.Format("event.hunt", Label(hunterSpecies), Label(preySpecies)); - Global(EventCategory.Combat, text); + Global(EventCategory.Combat, text, hunter.Id); Creature(hunter, EventCategory.Combat, text); } public void Killed(Entity hunter, int hunterSpecies, int preySpecies) { var text = _lang.Format("event.killed", Label(hunterSpecies), Label(preySpecies)); - Global(EventCategory.Combat, text); + Global(EventCategory.Combat, text, hunter.Id); Creature(hunter, EventCategory.Combat, text); } @@ -138,7 +143,7 @@ public sealed class WorldEvents // — Смерть (умершая особь исчезает, её хроника — тоже; пишем только в глобальный журнал) — public void Died(int species, string causeKey) => - Global(EventCategory.Death, _lang.Format(causeKey, Label(species))); + Global(EventCategory.Death, _lang.Format(causeKey, Label(species)), -1); // особь исчезла // — Бытовое (только личная хроника особи, чтобы не засорять глобальный журнал) — public void Ate(Entity e, int species) => @@ -167,12 +172,13 @@ public sealed class WorldEvents private string Label(int species) => _lang.Get(_animals[species].Def.Label); - private LogEntry Make(EventCategory category, string text) => - new(text, category, _calendar.Day, _calendar.Hour, _calendar.Minute); + private LogEntry Make(EventCategory category, string text, int subjectId) => + new(text, category, _calendar.Day, _calendar.Hour, _calendar.Minute, subjectId); - private void Global(EventCategory category, string text) + // subjectId — кого выбирать/куда вести камеру по двойному клику (-1 — не к кому, напр. смерть). + private void Global(EventCategory category, string text, int subjectId) { - _global.Add(Make(category, text)); + _global.Add(Make(category, text, subjectId)); if (_global.Count > _capacity) { _global.RemoveAt(0); // вытесняем старейшее @@ -188,7 +194,7 @@ public sealed class WorldEvents return; } - log.Entries.Add(Make(category, text)); + log.Entries.Add(Make(category, text, e.Id)); // субъект хроники — сама особь if (log.Entries.Count > log.Capacity) { log.Entries.RemoveAt(0); diff --git a/src/LittleSim/UI/EventsPanel.cs b/src/LittleSim/UI/EventsPanel.cs index d67afa1..76cccb8 100644 --- a/src/LittleSim/UI/EventsPanel.cs +++ b/src/LittleSim/UI/EventsPanel.cs @@ -1,6 +1,7 @@ using System; using LittleSim.Sim; using Microsoft.Xna.Framework; +using MrGameEng.Core; using MrGameEng.Mods; using Myra.Graphics2D; using Myra.Graphics2D.Brushes; @@ -11,31 +12,44 @@ namespace LittleSim.UI; /// /// Оверлей глобального журнала событий мира (фича UI «События»): последние заметные события (спаривание, /// роды/вылупление, начало охоты, убийство, смерти) новыми сверху, цвет строки — по категории. Источник — -/// ; тело перестраивается лишь при изменении версии журнала (новое событие), -/// поэтому открытое окно почти ничего не стоит на паузе. Переключается кнопкой HUD. +/// ; тело перестраивается лишь при изменении версии журнала (новое событие). +/// Кликабельные записи подсвечиваются при наведении, по двойному клику ведут камеру к «ответственному». +/// Окно плавно проявляется при открытии. Переключается кнопкой HUD. /// internal sealed class EventsPanel { private const int MaxShown = 60; // сколько последних записей показывать + private const float FadeSeconds = 0.16f; // длительность плавного появления // Цвета строк по категории (приглушённые — читаемо на тёмном фоне). private static readonly Color Reproduction = new(120, 186, 120); private static readonly Color Combat = new(214, 150, 86); private static readonly Color Death = new(196, 92, 86); private static readonly Color Routine = new(150, 160, 175); + private static readonly IBrush RowHover = new SolidBrush(new Color(96, 134, 168, 60)); private readonly LanguageManager _lang; private readonly WorldEvents _events; + private readonly GameClock _clock; + private readonly Action _onFocus; // двойной клик по записи: перейти/выбрать «ответственного» private readonly VerticalStackPanel _panel; private readonly VerticalStackPanel _body; private int _builtVersion = -1; + private float _opacity; - public EventsPanel(LanguageManager lang, WorldEvents events) + public EventsPanel( + LanguageManager lang, + WorldEvents events, + GameClock clock, + Action onFocus + ) { _lang = lang; _events = events; + _clock = clock; + _onFocus = onFocus; - _body = new VerticalStackPanel { Spacing = 2 }; + _body = new VerticalStackPanel { Spacing = 1 }; var scroll = new ScrollViewer { Content = _body, @@ -59,6 +73,8 @@ internal sealed class EventsPanel _panel.Widgets.Add(new Label { Text = _lang.Get("events.title"), TextColor = Ui.Accent }); _panel.Widgets.Add(new HorizontalSeparator()); _panel.Widgets.Add(scroll); + _panel.Widgets.Add(new HorizontalSeparator()); + _panel.Widgets.Add(new Label { Text = _lang.Get("events.hint"), TextColor = Ui.Muted }); } /// Корневой виджет — добавляется в HUD. @@ -67,20 +83,33 @@ internal sealed class EventsPanel /// Открыт ли журнал. public bool IsOpen => _panel.Visible; - /// Показать/скрыть; при открытии форсирует пересборку тела. + /// Показать/скрыть; при открытии запускает плавное появление и форсирует пересборку тела. public void Toggle() { _panel.Visible = !_panel.Visible; if (_panel.Visible) { + _opacity = 0f; + _panel.Opacity = 0f; _builtVersion = -1; // пересобрать при следующем Refresh } } - /// Вызывать каждый кадр: перестраивает тело лишь при новом событии (по версии журнала). + /// Вызывать каждый кадр: плавное появление + пересборка тела при новом событии (по версии). public void Refresh() { - if (!_panel.Visible || _events.Version == _builtVersion) + if (!_panel.Visible) + { + return; + } + + if (_opacity < 1f) + { + _opacity = MathF.Min(1f, _opacity + _clock.UnscaledDeltaTime / FadeSeconds); + _panel.Opacity = _opacity; + } + + if (_events.Version == _builtVersion) { return; } @@ -100,14 +129,33 @@ internal sealed class EventsPanel { var entry = entries[entries.Count - 1 - i]; var stamp = _lang.Format("log.stamp", entry.Day, entry.Hour, entry.Minute); - _body.Widgets.Add( - new Label + var color = ColorOf(entry.Category); + var row = new Label + { + Text = stamp + " " + entry.Text, + TextColor = color, + Wrap = true, + Padding = new Thickness(4, 2), + }; + // Запись с «ответственным» — кликабельна: подсветка при наведении + переход по двойному клику. + var subjectId = entry.SubjectId; + if (subjectId >= 0) + { + var baseColor = color; + row.MouseEntered += (_, _) => { - Text = stamp + " " + entry.Text, - TextColor = ColorOf(entry.Category), - Wrap = true, - } - ); + row.Background = RowHover; + row.TextColor = Color.Lerp(baseColor, Color.White, 0.45f); + }; + row.MouseLeft += (_, _) => + { + row.Background = null; + row.TextColor = baseColor; + }; + row.TouchDoubleClick += (_, _) => _onFocus(subjectId); + } + + _body.Widgets.Add(row); } } diff --git a/src/LittleSim/UI/InspectPanel.cs b/src/LittleSim/UI/InspectPanel.cs index db89069..7e694de 100644 --- a/src/LittleSim/UI/InspectPanel.cs +++ b/src/LittleSim/UI/InspectPanel.cs @@ -194,7 +194,9 @@ internal sealed class InspectPanel region, sprite.Origin ); - _brackets.Show(center, radius); // позиция брекета — каждый кадр (дёшево, следит за движением) + // Лёгкая «дышащая» пульсация рамки выделения (живее статичной скобки), по реальному времени. + var pulse = 1f + 0.06f * MathF.Sin((float)_clock.UnscaledTotalTime * 5f); + _brackets.Show(center, radius * pulse); // позиция/размер каждый кадр (дёшево, следит за движением) } // Тело вкладки пересобираем только при смене выбора/вкладки или по таймеру (значения нужд/HP @@ -241,6 +243,7 @@ internal sealed class InspectPanel { Text = _content.Languages.Get(labelKey), Padding = new Thickness(6, 3), + OverBackground = new SolidBrush(new Color(40, 54, 72)), // подсветка при наведении }; button.Click += (_, _) => _tab = tab; bar.Widgets.Add(button); diff --git a/src/LittleSim/UI/PerfPanel.cs b/src/LittleSim/UI/PerfPanel.cs index 702fe6e..4e22266 100644 --- a/src/LittleSim/UI/PerfPanel.cs +++ b/src/LittleSim/UI/PerfPanel.cs @@ -22,6 +22,7 @@ internal sealed class PerfPanel { private const int TopSystems = 12; // сколько самых дорогих систем показывать на корень private const double RefreshInterval = 0.2; // сек реального времени между пересборками тела (5 Гц) + private const float FadeSeconds = 0.16f; // длительность плавного появления private readonly LanguageManager _lang; private readonly GameClock _clock; @@ -35,6 +36,7 @@ internal sealed class PerfPanel private float _accum; // накоплено реального времени для окна усреднения FPS private int _frames; private double _nextBuildAt; + private float _opacity; public PerfPanel(LanguageManager lang, GameClock clock, params SystemRoot[] roots) { @@ -92,6 +94,8 @@ internal sealed class PerfPanel _panel.Visible = !_panel.Visible; if (_panel.Visible) { + _opacity = 0f; + _panel.Opacity = 0f; // плавно проявим в Refresh PerfReport.EnableMonitoring(_roots); } } @@ -99,7 +103,14 @@ internal sealed class PerfPanel /// Вызывать каждый кадр: считает FPS (всегда) и перестраивает тело по троттлингу (если открыто). public void Refresh() { - _accum += _clock.UnscaledDeltaTime; + var dt = _clock.UnscaledDeltaTime; + if (_panel.Visible && _opacity < 1f) + { + _opacity = MathF.Min(1f, _opacity + dt / FadeSeconds); + _panel.Opacity = _opacity; + } + + _accum += dt; _frames++; var now = _clock.UnscaledTotalTime; if (now < _nextBuildAt)