Enhance UI localization and settings functionality. Added new localization keys for inspection tabs and UI scale settings in both English and Russian. Updated GameSettings to include a UiScale property. Refactored scenes to utilize UseScaledUI for consistent UI scaling across various game scenes. Improved InspectPanel to support tabbed navigation for overview, genes, and products. Adjusted SettingsPanel to allow users to modify UI scale dynamically.
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
"season.autumn": "autumn",
|
||||
"season.winter": "winter",
|
||||
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
|
||||
"inspect.tab.overview": "Overview",
|
||||
"inspect.tab.genes": "Genes",
|
||||
"inspect.tab.products": "Products",
|
||||
"inspect.stage": "Stage: {0} ({1}/{2})",
|
||||
"inspect.growing": "Growth: {0:0}% to next stage",
|
||||
"inspect.mature": "Fully grown",
|
||||
@@ -57,6 +60,7 @@
|
||||
"settings.vsync": "Vertical sync",
|
||||
"settings.resolution": "Resolution",
|
||||
"settings.volume": "Volume",
|
||||
"settings.uiscale": "UI scale",
|
||||
"settings.apply": "Apply",
|
||||
"settings.back": "Back",
|
||||
"pause.title": "Paused",
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"season.autumn": "осень",
|
||||
"season.winter": "зима",
|
||||
"hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
|
||||
"inspect.tab.overview": "Обзор",
|
||||
"inspect.tab.genes": "Гены",
|
||||
"inspect.tab.products": "Продукты",
|
||||
"inspect.stage": "Стадия: {0} ({1}/{2})",
|
||||
"inspect.growing": "Рост: {0:0}% до следующей стадии",
|
||||
"inspect.mature": "Полностью выросло",
|
||||
@@ -57,6 +60,7 @@
|
||||
"settings.vsync": "Вертикальная синхронизация",
|
||||
"settings.resolution": "Разрешение",
|
||||
"settings.volume": "Громкость",
|
||||
"settings.uiscale": "Масштаб UI",
|
||||
"settings.apply": "Применить",
|
||||
"settings.back": "Назад",
|
||||
"pause.title": "Пауза",
|
||||
|
||||
@@ -28,6 +28,9 @@ public sealed class GameSettings
|
||||
|
||||
/// <summary>Общая громкость 0..1.</summary>
|
||||
public float Volume { get; set; } = 1f;
|
||||
|
||||
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
|
||||
public float UiScale { get; set; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed class BootScene : Scene
|
||||
host.Graphics.ApplyChanges();
|
||||
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
|
||||
|
||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
_label = new Label
|
||||
{
|
||||
TextColor = Ui.Accent,
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class CreditsScene : Scene
|
||||
{
|
||||
var lang = Context.Services.Get<GameContent>().Languages;
|
||||
var input = this.UseInput();
|
||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
|
||||
var column = Ui.Column(12);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
||||
|
||||
@@ -13,6 +13,9 @@ public static class GameLayers
|
||||
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
|
||||
public static LayerId Beings { get; private set; }
|
||||
|
||||
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
|
||||
public static LayerId Selection { get; private set; }
|
||||
|
||||
/// <summary>Регистрирует слои при первом вызове.</summary>
|
||||
public static void EnsureRegistered(Renderer2D renderer)
|
||||
{
|
||||
@@ -22,6 +25,7 @@ public static class GameLayers
|
||||
}
|
||||
|
||||
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
|
||||
Selection = renderer.Layers.Register("Selection", LayerSpace.World, LayerSortMode.Depth);
|
||||
_registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class LoadGameScene : Scene
|
||||
{
|
||||
_content = Context.Services.Get<GameContent>();
|
||||
var input = this.UseInput();
|
||||
_desktop = this.UseUI();
|
||||
_desktop = this.UseScaledUI();
|
||||
Rebuild();
|
||||
|
||||
UpdateSystems.Add(
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class MainMenuScene : Scene
|
||||
{
|
||||
var content = Context.Services.Get<GameContent>();
|
||||
var lang = content.Languages;
|
||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
|
||||
var column = Ui.Column(12);
|
||||
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||
|
||||
@@ -69,7 +69,7 @@ public sealed class MultiplayerScene : Scene
|
||||
entity.AddComponent(new NetLerp());
|
||||
};
|
||||
|
||||
var desktop = this.UseUI();
|
||||
var desktop = this.UseScaledUI();
|
||||
_hud = new Label { Left = 10, Top = 8 };
|
||||
desktop.Root = Ui.Screen(_hud);
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class NewWorldScene : Scene
|
||||
var content = Context.Services.Get<GameContent>();
|
||||
var lang = content.Languages;
|
||||
var input = this.UseInput();
|
||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||
|
||||
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
||||
_preset = presets[0];
|
||||
|
||||
@@ -24,6 +24,7 @@ internal sealed class PauseMenu
|
||||
private readonly GameSpeed _speed;
|
||||
private readonly GameHost _host;
|
||||
private readonly AudioManager _audio;
|
||||
private readonly Desktop _desktop;
|
||||
private readonly Func<string> _onSave;
|
||||
private readonly Action _onMainMenu;
|
||||
private readonly Action _onQuit;
|
||||
@@ -36,6 +37,7 @@ internal sealed class PauseMenu
|
||||
EngineContext context,
|
||||
GameContent content,
|
||||
GameSpeed speed,
|
||||
Desktop desktop,
|
||||
Func<string> onSave,
|
||||
Action onMainMenu,
|
||||
Action onQuit
|
||||
@@ -44,6 +46,7 @@ internal sealed class PauseMenu
|
||||
_context = context;
|
||||
_content = content;
|
||||
_speed = speed;
|
||||
_desktop = desktop;
|
||||
_host = (GameHost)context.Services.Get<Game>();
|
||||
_audio = context.Services.Get<AudioManager>();
|
||||
_onSave = onSave;
|
||||
@@ -144,6 +147,7 @@ internal sealed class PauseMenu
|
||||
{
|
||||
GameSettingsStore.Save(_settings);
|
||||
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||
UiScaling.Apply(_context, _desktop, _settings.UiScale); // масштаб UI — сразу
|
||||
ShowSettings(); // язык мог смениться
|
||||
},
|
||||
onBack: () =>
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class SettingsScene : Scene
|
||||
_host = (GameHost)Context.Services.Get<Game>();
|
||||
var input = this.UseInput();
|
||||
|
||||
_desktop = this.UseUI();
|
||||
_desktop = this.UseScaledUI();
|
||||
Rebuild();
|
||||
|
||||
UpdateSystems.Add(
|
||||
@@ -53,6 +53,7 @@ public sealed class SettingsScene : Scene
|
||||
{
|
||||
GameSettingsStore.Save(_settings);
|
||||
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||
UiScaling.Apply(Context, _desktop, _settings.UiScale); // масштаб UI — сразу
|
||||
Rebuild(); // язык мог смениться — перестраиваем подписи
|
||||
}
|
||||
|
||||
|
||||
@@ -123,26 +123,26 @@ public sealed class WorldScene : Scene
|
||||
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
||||
|
||||
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
||||
var desktop = this.UseUI();
|
||||
var desktop = this.UseScaledUI();
|
||||
var hudLabel = new Label { Left = 10, Top = 8 };
|
||||
var speedBar = BuildSpeedBar(content);
|
||||
_pause = new PauseMenu(
|
||||
Context,
|
||||
content,
|
||||
_speed,
|
||||
desktop,
|
||||
onSave: SaveWorld,
|
||||
onMainMenu: () => Switch(new MainMenuScene()),
|
||||
onQuit: () => Context.Services.Get<Game>().Exit()
|
||||
);
|
||||
_selection = new Selection();
|
||||
_inspect = new InspectPanel(Store, _plants, content, climate, _selection);
|
||||
desktop.Root = Ui.Screen(
|
||||
hudLabel,
|
||||
_inspect.Highlight,
|
||||
_inspect.Panel,
|
||||
speedBar,
|
||||
_pause.Root
|
||||
var brackets = new SelectionBrackets(
|
||||
Store,
|
||||
atlases.GetRegion(device, "ui/overlays/selectionbracket"),
|
||||
GameLayers.Selection
|
||||
);
|
||||
_inspect = new InspectPanel(Store, _plants, content, climate, _selection, brackets);
|
||||
desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root);
|
||||
|
||||
this.UseInspector(renderer);
|
||||
var console = this.UseDevConsole();
|
||||
@@ -199,10 +199,10 @@ public sealed class WorldScene : Scene
|
||||
renderer,
|
||||
_selection,
|
||||
() => _pause.IsOpen,
|
||||
p => _inspect.IsOverPanel(p) || speedBar.Bounds.Contains(p)
|
||||
() => desktop.IsMouseOverGUI
|
||||
)
|
||||
);
|
||||
UpdateSystems.Add(new CallbackSystem(() => _inspect.Refresh(renderer)));
|
||||
UpdateSystems.Add(new CallbackSystem(() => _inspect.Refresh()));
|
||||
UpdateSystems.Add(
|
||||
new HudSystem(
|
||||
Context,
|
||||
|
||||
@@ -40,14 +40,14 @@ public sealed class SelectionSystem(
|
||||
Renderer2D renderer,
|
||||
Selection selection,
|
||||
Func<bool> blocked,
|
||||
Func<Point, bool> overUi
|
||||
Func<bool> overUi
|
||||
) : BaseSystem
|
||||
{
|
||||
protected override void OnUpdateGroup()
|
||||
{
|
||||
if (blocked())
|
||||
if (blocked() || overUi())
|
||||
{
|
||||
return;
|
||||
return; // меню-пауза/консоль или клик пришёлся на виджет UI — это не пикинг мира
|
||||
}
|
||||
|
||||
if (input.IsMousePressed(MouseButton.Right))
|
||||
@@ -62,10 +62,6 @@ public sealed class SelectionSystem(
|
||||
}
|
||||
|
||||
var mouse = input.MousePosition;
|
||||
if (overUi(mouse))
|
||||
{
|
||||
return; // клик пришёлся на панель — это не пикинг мира
|
||||
}
|
||||
|
||||
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||||
if (TryPick(world, out var id))
|
||||
@@ -78,12 +74,17 @@ public sealed class SelectionSystem(
|
||||
}
|
||||
}
|
||||
|
||||
// Ближайшая сущность, чья ограничивающая окружность спрайта накрывает точку мира.
|
||||
// Ближайшее растение, чья ограничивающая окружность спрайта накрывает точку мира. Запрос
|
||||
// ограничен PlantGrowth — так из пикинга выпадают спрайты рамки выделения и прочий не-контент.
|
||||
private bool TryPick(Vector2 world, out int id)
|
||||
{
|
||||
id = -1;
|
||||
var bestDistance = float.MaxValue;
|
||||
foreach (var (transforms, sprites, entities) in store.Query<Transform2D, Sprite>().Chunks)
|
||||
foreach (
|
||||
var (transforms, sprites, _, entities) in store
|
||||
.Query<Transform2D, Sprite, PlantGrowth>()
|
||||
.Chunks
|
||||
)
|
||||
{
|
||||
var t = transforms.Span;
|
||||
var s = sprites.Span;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Рамка выделения в стиле RimWorld: четыре угловые скобки вокруг выбранного объекта. Реализована
|
||||
/// как четыре спрайт-сущности на слое-оверлее (<see cref="Scenes.GameLayers.Selection"/>) — их рисует
|
||||
/// штатный спрайт-рендер, а одна и та же текстура уголка (<c>ui/overlays/selectionbracket</c>)
|
||||
/// зеркалится во все четыре угла флагами <see cref="SpriteFlip"/> (батчер при флипе меняет только UV,
|
||||
/// квадрат остаётся на месте). Скобки не имеют <c>PlantGrowth</c>, поэтому не попадают под пикинг и
|
||||
/// прочие запросы растений. <see cref="Show"/> двигает их к объекту, <see cref="Hide"/> прячет.
|
||||
/// </summary>
|
||||
public sealed class SelectionBrackets
|
||||
{
|
||||
// По уголку на каждый угол: исходный арт лежит в левом-нижнем, флип разносит его по остальным.
|
||||
private static readonly SpriteFlip[] Corners =
|
||||
[
|
||||
SpriteFlip.None,
|
||||
SpriteFlip.X,
|
||||
SpriteFlip.Y,
|
||||
SpriteFlip.X | SpriteFlip.Y,
|
||||
];
|
||||
|
||||
private static readonly Color Tint = new(150, 210, 255);
|
||||
|
||||
private readonly Entity[] _brackets = new Entity[4];
|
||||
private readonly Texture2DRegion _region;
|
||||
|
||||
/// <summary>Создаёт четыре скрытые скобки на слое <paramref name="layer"/> из текстуры <paramref name="region"/>.</summary>
|
||||
public SelectionBrackets(EntityStore store, Texture2DRegion region, LayerId layer)
|
||||
{
|
||||
_region = region;
|
||||
var origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||
for (var i = 0; i < _brackets.Length; i++)
|
||||
{
|
||||
_brackets[i] = store.CreateEntity(
|
||||
Transform2D.At(Vector2.Zero),
|
||||
new Sprite
|
||||
{
|
||||
Region = null, // скрыта, пока ничего не выбрано
|
||||
Color = Tint,
|
||||
Origin = origin, // центр текстуры → квад центрируется на позиции
|
||||
Layer = layer,
|
||||
Flip = Corners[i],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ставит рамку вокруг точки <paramref name="center"/> с полуразмером <paramref name="radius"/>.</summary>
|
||||
public void Show(Vector2 center, float radius)
|
||||
{
|
||||
// Квад чуть больше объекта — скобки садятся снаружи (как в RimWorld); арт-уголок ~⅓ ширины.
|
||||
var scale = new Vector2(radius * 2.2f / _region.Width);
|
||||
for (var i = 0; i < _brackets.Length; i++)
|
||||
{
|
||||
ref var transform = ref _brackets[i].GetComponent<Transform2D>();
|
||||
ref var sprite = ref _brackets[i].GetComponent<Sprite>();
|
||||
transform.Position = center;
|
||||
transform.Scale = scale;
|
||||
sprite.Region = _region;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Прячет рамку (сбрасывает регион — рендер пропускает спрайты без текстуры).</summary>
|
||||
public void Hide()
|
||||
{
|
||||
for (var i = 0; i < _brackets.Length; i++)
|
||||
{
|
||||
_brackets[i].GetComponent<Sprite>().Region = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Friflo.Engine.ECS;
|
||||
using LittleSim.Content;
|
||||
@@ -14,31 +15,41 @@ using Myra.Graphics2D.UI;
|
||||
namespace LittleSim.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Панель осмотра выбранной сущности (как «инфо-карточка» RimWorld): рамка-подсветка вокруг объекта
|
||||
/// в мире и боковая панель с именем, процессом роста, состоянием, температурным диапазоном, полным
|
||||
/// набором генов/признаков и продуктами растения. Источник выделения — <see cref="Selection"/>;
|
||||
/// панель только читает компоненты и обновляется каждый кадр (<see cref="Refresh"/>), оставаясь
|
||||
/// живой даже на паузе. Строки идут через <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
|
||||
/// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта
|
||||
/// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками — «Обзор»,
|
||||
/// «Гены», «Продукты». Источник выделения — <see cref="Selection"/>; панель только читает компоненты
|
||||
/// и обновляется каждый кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут
|
||||
/// через <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
|
||||
/// </summary>
|
||||
internal sealed class InspectPanel
|
||||
{
|
||||
private enum Tab
|
||||
{
|
||||
Overview,
|
||||
Genes,
|
||||
Products,
|
||||
}
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly PlantSet _plants;
|
||||
private readonly GameContent _content;
|
||||
private readonly Climate _climate;
|
||||
private readonly Selection _selection;
|
||||
private readonly SelectionBrackets _brackets;
|
||||
|
||||
private readonly Panel _highlight;
|
||||
private readonly VerticalStackPanel _panel;
|
||||
private readonly Label _title;
|
||||
private readonly Label _body;
|
||||
private readonly List<(Tab Tab, TextButton Button)> _tabs = [];
|
||||
private Tab _tab = Tab.Overview;
|
||||
|
||||
public InspectPanel(
|
||||
EntityStore store,
|
||||
PlantSet plants,
|
||||
GameContent content,
|
||||
Climate climate,
|
||||
Selection selection
|
||||
Selection selection,
|
||||
SelectionBrackets brackets
|
||||
)
|
||||
{
|
||||
_store = store;
|
||||
@@ -46,52 +57,60 @@ internal sealed class InspectPanel
|
||||
_content = content;
|
||||
_climate = climate;
|
||||
_selection = selection;
|
||||
_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 };
|
||||
AddTab(tabBar, Tab.Overview, "inspect.tab.overview");
|
||||
AddTab(tabBar, Tab.Genes, "inspect.tab.genes");
|
||||
AddTab(tabBar, Tab.Products, "inspect.tab.products");
|
||||
|
||||
_panel = new VerticalStackPanel
|
||||
{
|
||||
Spacing = 6,
|
||||
Padding = new Thickness(10),
|
||||
Width = 290,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, 10, 10, 0),
|
||||
Background = new SolidBrush(new Color(8, 10, 14, 235)),
|
||||
Visible = false,
|
||||
};
|
||||
_panel.Widgets.Add(_title);
|
||||
_panel.Widgets.Add(new HorizontalSeparator());
|
||||
_panel.Widgets.Add(_body);
|
||||
|
||||
_highlight = new Panel
|
||||
{
|
||||
Border = new SolidBrush(Ui.Accent),
|
||||
Padding = new Thickness(14),
|
||||
Width = 380,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
Margin = new Thickness(12, 0, 0, 14),
|
||||
Background = new SolidBrush(new Color(10, 13, 18, 240)),
|
||||
Border = new SolidBrush(new Color(96, 134, 168)),
|
||||
BorderThickness = new Thickness(2),
|
||||
Visible = false,
|
||||
};
|
||||
_panel.Widgets.Add(_title);
|
||||
_panel.Widgets.Add(tabBar);
|
||||
_panel.Widgets.Add(new HorizontalSeparator());
|
||||
_panel.Widgets.Add(_body);
|
||||
_panel.Widgets.Add(
|
||||
new Label
|
||||
{
|
||||
Text = _content.Languages.Get("inspect.hint"),
|
||||
TextColor = Ui.Muted,
|
||||
Wrap = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Рамка-подсветка в мире — добавляется в корень экрана позади панели.</summary>
|
||||
public Panel Highlight => _highlight;
|
||||
|
||||
/// <summary>Боковая инфо-панель — добавляется в корень экрана.</summary>
|
||||
public VerticalStackPanel Panel => _panel;
|
||||
|
||||
/// <summary>Над панелью ли точка экрана (тогда клик не считается пикингом мира).</summary>
|
||||
public bool IsOverPanel(Point screen) => _panel.Visible && _panel.Bounds.Contains(screen);
|
||||
|
||||
/// <summary>
|
||||
/// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает
|
||||
/// рамку по экрану и пересобирает текст. Не-растения панель не показывает (пока осматриваем только их).
|
||||
/// рамку к объекту и пересобирает текст активной вкладки. Не-растения панель не показывает.
|
||||
/// </summary>
|
||||
public void Refresh(Renderer2D renderer)
|
||||
public void Refresh()
|
||||
{
|
||||
if (
|
||||
!_selection.HasSelection
|
||||
|| !_store.TryGetEntityById(_selection.EntityId, out var entity)
|
||||
|| entity.IsNull
|
||||
|| !entity.HasComponent<PlantGrowth>()
|
||||
|| !entity.HasComponent<PlantOrganism>()
|
||||
|| !entity.HasComponent<Transform2D>()
|
||||
|| !entity.HasComponent<Sprite>()
|
||||
)
|
||||
{
|
||||
if (_selection.HasSelection)
|
||||
@@ -99,55 +118,33 @@ internal sealed class InspectPanel
|
||||
_selection.Clear(); // сущность исчезла (растение погибло)
|
||||
}
|
||||
|
||||
Hide();
|
||||
_panel.Visible = false;
|
||||
_brackets.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!entity.HasComponent<PlantGrowth>()
|
||||
|| !entity.HasComponent<PlantOrganism>()
|
||||
|| !entity.HasComponent<Transform2D>()
|
||||
|| !entity.HasComponent<Sprite>()
|
||||
)
|
||||
var transform = entity.GetComponent<Transform2D>();
|
||||
var sprite = entity.GetComponent<Sprite>();
|
||||
if (sprite.Region is { } region)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
var (center, radius) = CullingMath.SpriteBoundingCircle(
|
||||
in transform,
|
||||
region,
|
||||
sprite.Origin
|
||||
);
|
||||
_brackets.Show(center, radius);
|
||||
}
|
||||
|
||||
UpdateHighlight(renderer, entity);
|
||||
UpdateText(entity);
|
||||
_panel.Visible = true;
|
||||
}
|
||||
|
||||
private void Hide()
|
||||
private void AddTab(HorizontalStackPanel bar, Tab tab, string labelKey)
|
||||
{
|
||||
_panel.Visible = false;
|
||||
_highlight.Visible = false;
|
||||
}
|
||||
|
||||
private void UpdateHighlight(Renderer2D renderer, Entity entity)
|
||||
{
|
||||
var transform = entity.GetComponent<Transform2D>();
|
||||
var sprite = entity.GetComponent<Sprite>();
|
||||
if (sprite.Region is not { } region)
|
||||
{
|
||||
_highlight.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var (center, radius) = CullingMath.SpriteBoundingCircle(
|
||||
in transform,
|
||||
region,
|
||||
sprite.Origin
|
||||
);
|
||||
var screenCenter = renderer.WorldToScreen(center);
|
||||
var edge = renderer.WorldToScreen(center + new Vector2(radius, 0f));
|
||||
var screenRadius = MathF.Max(4f, MathF.Abs(edge.X - screenCenter.X));
|
||||
_highlight.Visible = true;
|
||||
_highlight.Left = (int)(screenCenter.X - screenRadius);
|
||||
_highlight.Top = (int)(screenCenter.Y - screenRadius);
|
||||
_highlight.Width = (int)(screenRadius * 2f);
|
||||
_highlight.Height = (int)(screenRadius * 2f);
|
||||
var button = new TextButton { Text = _content.Languages.Get(labelKey) };
|
||||
button.Click += (_, _) => _tab = tab;
|
||||
bar.Widgets.Add(button);
|
||||
_tabs.Add((tab, button));
|
||||
}
|
||||
|
||||
private void UpdateText(Entity entity)
|
||||
@@ -160,16 +157,44 @@ 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;
|
||||
}
|
||||
|
||||
var text = new StringBuilder();
|
||||
switch (_tab)
|
||||
{
|
||||
case Tab.Overview:
|
||||
BuildOverview(text, entity, grow, traits, species);
|
||||
break;
|
||||
case Tab.Genes:
|
||||
BuildGenes(text, traits);
|
||||
break;
|
||||
case Tab.Products:
|
||||
BuildProducts(text, entity, def, traits);
|
||||
break;
|
||||
}
|
||||
|
||||
_body.Text = text.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private void BuildOverview(
|
||||
StringBuilder text,
|
||||
Entity entity,
|
||||
in PlantGrowth grow,
|
||||
in PlantPhenotype traits,
|
||||
PlantSet.Species species
|
||||
)
|
||||
{
|
||||
var languages = _content.Languages;
|
||||
var lastStage = species.Stages.Length - 1;
|
||||
var mature = grow.Stage >= lastStage;
|
||||
var text = new StringBuilder();
|
||||
|
||||
// Стадия и процесс роста.
|
||||
text.AppendLine(
|
||||
languages.Format(
|
||||
"inspect.stage",
|
||||
StageLabel(def, grow.Stage),
|
||||
StageLabel(species.Def, grow.Stage),
|
||||
grow.Stage + 1,
|
||||
species.Stages.Length
|
||||
)
|
||||
@@ -188,22 +213,16 @@ internal sealed class InspectPanel
|
||||
|
||||
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan));
|
||||
|
||||
// Состояние по температуре (рост/покой), при наличии — накопленный стресс.
|
||||
// Состояние по температуре (рост/покой) и накопленный стресс.
|
||||
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||
var temperature = _climate.Temperature;
|
||||
var suit = Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax);
|
||||
string state;
|
||||
if (suit <= 0f)
|
||||
{
|
||||
state = languages.Get(
|
||||
temperature <= tMin ? "inspect.state.dormantcold" : "inspect.state.dormanthot"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
state = languages.Get(mature ? "inspect.state.mature" : "inspect.state.growing");
|
||||
}
|
||||
|
||||
var state =
|
||||
suit <= 0f
|
||||
? languages.Get(
|
||||
temperature <= tMin ? "inspect.state.dormantcold" : "inspect.state.dormanthot"
|
||||
)
|
||||
: languages.Get(mature ? "inspect.state.mature" : "inspect.state.growing");
|
||||
text.AppendLine(languages.Format("inspect.state", state));
|
||||
if (entity.TryGetComponent<TemperatureStress>(out var stress) && stress.Days > 0.05f)
|
||||
{
|
||||
@@ -211,10 +230,11 @@ internal sealed class InspectPanel
|
||||
}
|
||||
|
||||
text.AppendLine(languages.Format("inspect.temp", tMin, tMax, tLow, tHigh));
|
||||
}
|
||||
|
||||
// Гены / фенотип.
|
||||
text.AppendLine();
|
||||
text.AppendLine(languages.Get("inspect.genes"));
|
||||
private void BuildGenes(StringBuilder text, 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)
|
||||
@@ -237,9 +257,16 @@ internal sealed class InspectPanel
|
||||
{
|
||||
text.AppendLine(languages.Get("inspect.gene.variant"));
|
||||
}
|
||||
}
|
||||
|
||||
// Продукты: сбор и плоды.
|
||||
text.AppendLine();
|
||||
private void BuildProducts(
|
||||
StringBuilder text,
|
||||
Entity entity,
|
||||
PlantDef def,
|
||||
in PlantPhenotype traits
|
||||
)
|
||||
{
|
||||
var languages = _content.Languages;
|
||||
if (def.HarvestProduct is { } harvest)
|
||||
{
|
||||
text.AppendLine(
|
||||
@@ -267,9 +294,6 @@ internal sealed class InspectPanel
|
||||
{
|
||||
text.AppendLine(languages.Get("inspect.barren"));
|
||||
}
|
||||
|
||||
text.Append(languages.Get("inspect.hint"));
|
||||
_body.Text = text.ToString();
|
||||
}
|
||||
|
||||
private string StageLabel(PlantDef def, int stage) =>
|
||||
|
||||
@@ -82,6 +82,19 @@ internal static class SettingsPanel
|
||||
)
|
||||
);
|
||||
|
||||
// Масштаб интерфейса — сегменты пресетов.
|
||||
var scales = new List<float> { 0.75f, 1f, 1.25f, 1.5f };
|
||||
column.Widgets.Add(
|
||||
Segment(
|
||||
lang.Get("settings.uiscale"),
|
||||
scales,
|
||||
s => $"{(int)MathF.Round(s * 100)}%",
|
||||
() => settings.UiScale,
|
||||
s => settings.UiScale = s,
|
||||
refreshers
|
||||
)
|
||||
);
|
||||
|
||||
// Громкость — кнопки −/+ с подписью процента.
|
||||
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using LittleSim.App;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Graphics;
|
||||
using MrGameEng.UI;
|
||||
using Myra.Graphics2D.UI;
|
||||
|
||||
namespace LittleSim.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Масштаб интерфейса для Myra-десктопа. Scale увеличивает всю отрисовку, а <c>BoundsFetcher</c>
|
||||
/// отдаёт логический размер «экран ÷ масштаб», поэтому раскладка перетекает (а не обрезается):
|
||||
/// при масштабе 1.25 виджеты крупнее, но углы/центры остаются прижаты к краям экрана. Ввод Myra
|
||||
/// трансформируется тем же преобразованием, так что попадания/<c>IsMouseOverGUI</c> остаются верными.
|
||||
/// </summary>
|
||||
internal static class UiScaling
|
||||
{
|
||||
/// <summary>Применяет масштаб <paramref name="scale"/> (клампится в разумные пределы) к десктопу.</summary>
|
||||
public static void Apply(EngineContext context, Desktop desktop, float scale)
|
||||
{
|
||||
scale = System.Math.Clamp(scale, 0.75f, 2f);
|
||||
desktop.TransformOrigin = Vector2.Zero; // масштабируем от левого-верхнего угла — края на месте
|
||||
desktop.Scale = new Vector2(scale);
|
||||
desktop.BoundsFetcher = () =>
|
||||
{
|
||||
var viewport = context.GetGraphicsDevice().Viewport;
|
||||
return new Rectangle(
|
||||
0,
|
||||
0,
|
||||
(int)MathF.Round(viewport.Width / scale),
|
||||
(int)MathF.Round(viewport.Height / scale)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Расширение сцены: создаёт Myra-десктоп и сразу применяет масштаб UI из настроек.</summary>
|
||||
internal static class ScaledUiExtensions
|
||||
{
|
||||
/// <summary>Как <c>UseUI</c>, но десктоп уже масштабирован по <see cref="GameSettings.UiScale"/>.</summary>
|
||||
public static Desktop UseScaledUI(this Scene scene)
|
||||
{
|
||||
var desktop = scene.UseUI();
|
||||
UiScaling.Apply(scene.Context, desktop, GameSettingsStore.Load().UiScale);
|
||||
return desktop;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user