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)