Multiplayer: headless dedicated server, MrGameEng.Net, desktop and browser clients #1

Merged
mrleo1nid merged 5 commits from claude/fervent-shannon-8d884e into main 2026-06-13 01:25:07 +00:00
17 changed files with 1174 additions and 1008 deletions
Showing only changes of commit 8a7e2cce52 - Show all commits
+9 -6
View File
@@ -14,12 +14,14 @@ Game design docs live in `docs/` and are written in **Russian**. Engine rules li
## Layout ## Layout
``` ```
engine/ mrgameeng git submodule (own repo, own CLAUDE.md) engine/ mrgameeng git submodule (own repo, own CLAUDE.md)
src/LittleSim the game (net8.0); references engine projects directly src/LittleSim the game (net8.0); references engine projects directly
Mods/Core the game's own content as a mod: About, Defs, Languages, Textures src/LittleSim.Server dedicated-server prototype: the world headless (engine HeadlessHost),
Cache/ runtime-built atlases (gitignored) loads mods/defs without atlases; the future network server grows here
docs/ концепт, симуляция, моды, roadmap (Russian) Mods/Core the game's own content as a mod: About, Defs, Languages, Textures
LittleSim.sln game + engine sources + engine tests — one window for everything Cache/ runtime-built atlases (gitignored)
docs/ концепт, симуляция, моды, roadmap (Russian)
LittleSim.sln game + engine sources + engine tests — one window for everything
``` ```
## Commands ## Commands
@@ -28,6 +30,7 @@ LittleSim.sln game + engine sources + engine tests — one window for everythin
git submodule update --init # after fresh clone git submodule update --init # after fresh clone
dotnet build LittleSim.sln dotnet build LittleSim.sln
dotnet run --project src/LittleSim -c Release # measure perf in Release only dotnet run --project src/LittleSim -c Release # measure perf in Release only
dotnet run --project src/LittleSim.Server -- --days 10 --tps 60 # headless fast-forward
dotnet test LittleSim.sln # runs the engine test suites dotnet test LittleSim.sln # runs the engine test suites
``` ```
+45
View File
@@ -41,6 +41,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "engine\src\MrGameEng.Host\MrGameEng.Host.csproj", "{D4222C26-40D8-4465-9B26-CD6BD0722EC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host.Tests", "engine\tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{41F0249B-106E-4D56-B022-73D2C2D74807}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Server", "src\LittleSim.Server\LittleSim.Server.csproj", "{BEFB468B-6784-468E-9B60-EB44AB078D15}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -231,6 +237,42 @@ Global
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x64.ActiveCfg = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x64.Build.0 = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x86.ActiveCfg = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x86.Build.0 = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|Any CPU.Build.0 = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x64.ActiveCfg = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x64.Build.0 = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x86.ActiveCfg = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x86.Build.0 = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x64.ActiveCfg = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x64.Build.0 = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x86.ActiveCfg = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x86.Build.0 = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|Any CPU.Build.0 = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x64.ActiveCfg = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x64.Build.0 = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x86.ActiveCfg = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x86.Build.0 = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x64.ActiveCfg = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x64.Build.0 = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x86.ActiveCfg = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x86.Build.0 = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|Any CPU.Build.0 = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x64.ActiveCfg = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x64.Build.0 = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x86.ActiveCfg = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -253,5 +295,8 @@ Global
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{D4222C26-40D8-4465-9B26-CD6BD0722EC0} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{41F0249B-106E-4D56-B022-73D2C2D74807} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{BEFB468B-6784-468E-9B60-EB44AB078D15} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+1 -1
Submodule engine updated: d0104df304...2ac074004a
@@ -0,0 +1,46 @@
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using LittleSim.Scenes;
using MrGameEng.Core;
namespace LittleSim.Server;
/// <summary>
/// Серверная сцена-зародыш: календарь и климат мира идут на фиксированном тике
/// <see cref="HeadlessHost"/>, без окна, GPU и текстур. Демонстрирует headless-режим
/// движка; дальше сюда переедет полная симуляция мира (растения, жители) и сетевой слой.
/// </summary>
internal sealed class HeadlessWorldScene : Scene
{
private readonly GameContent _content;
public HeadlessWorldScene(GameContent content) => _content = content;
protected override void OnLoad()
{
Context.Services.Add(_content);
var calendar = Context.UseCalendar(WorldScene.SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
UpdateSystems.Add(new DayReportSystem(calendar, climate));
}
/// <summary>Пишет строку состояния мира в лог на рассвете каждого игрового дня.</summary>
private sealed class DayReportSystem(Calendar calendar, Climate climate) : BaseSystem
{
private int _lastDay;
protected override void OnUpdateGroup()
{
if (calendar.Day == _lastDay)
{
return;
}
_lastDay = calendar.Day;
Log.Info(
$"День {calendar.Day} | год {climate.Year}, день года {climate.DayOfYear + 1}, "
+ $"{climate.Season} | {climate.Temperature:+0.0;-0.0;0.0} °C"
);
}
}
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LittleSim\LittleSim.csproj" />
</ItemGroup>
</Project>
+44
View File
@@ -0,0 +1,44 @@
using System.Diagnostics;
using LittleSim.Content;
using LittleSim.Scenes;
using LittleSim.Server;
using MrGameEng.Core;
// Дедикейтед-сервер LittleSim (прототип): мир без окна и GPU на HeadlessHost движка.
// Пока умеет загрузить моды/дефы (без атласов) и прогнать календарь и климат на
// фиксированном тике. Использование:
// dotnet run --project src/LittleSim.Server [-- --days N] [--tps N]
var days = ReadOption("--days", 10);
var ticksPerSecond = ReadOption("--tps", 60);
Log.MessageLogged += (level, message) => Console.WriteLine($"[{level}] {message}");
var content = GameContent.Load(buildAtlases: false);
Log.Info(
$"Моды: {string.Join(", ", content.Mods.Select(m => m.ToString()))} | "
+ $"дефов: {content.Defs.All<TerrainDef>().Count} террейна, "
+ $"{content.Defs.All<PlantDef>().Count} растений, "
+ $"{content.Defs.All<PawnDef>().Count} жителей"
);
using var host = new HeadlessHost(
new HeadlessHostOptions { TicksPerSecond = ticksPerSecond, Realtime = false },
new HeadlessWorldScene(content)
);
var ticks = (long)((double)days * WorldScene.SecondsPerDay * ticksPerSecond);
var stopwatch = Stopwatch.StartNew();
host.RunTicks(ticks);
Log.Info(
$"{days} игровых дней за {stopwatch.Elapsed.TotalSeconds:F1} с реального времени "
+ $"({ticks} тиков, {ticks / Math.Max(stopwatch.Elapsed.TotalSeconds, 0.001):F0} тиков/с)"
);
int ReadOption(string name, int fallback)
{
var index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length && int.TryParse(args[index + 1], out var value)
? value
: fallback;
}
+23 -14
View File
@@ -44,8 +44,10 @@ public sealed class GameContent
/// <summary> /// <summary>
/// Находит папку Mods (вверх по дереву от исполняемого файла), загружает моды, дефы и /// Находит папку Mods (вверх по дереву от исполняемого файла), загружает моды, дефы и
/// языки и инкрементально собирает атласы из смерженного дерева текстур всех модов. /// языки и инкрементально собирает атласы из смерженного дерева текстур всех модов.
/// С <paramref name="buildAtlases"/> = false атласы не собираются — headless-режим
/// (дедикейтед-сервер) текстур не рисует.
/// </summary> /// </summary>
public static GameContent Load() public static GameContent Load(bool buildAtlases = true)
{ {
var modsRoot = var modsRoot =
ModLoader.FindModsRoot(AppContext.BaseDirectory) ModLoader.FindModsRoot(AppContext.BaseDirectory)
@@ -71,19 +73,26 @@ public sealed class GameContent
"Cache", "Cache",
"Atlases" "Atlases"
); );
var textures = ModContentTree.Build(mods, "Textures", ".png", ".jpg", ".jpeg", ".bmp"); if (buildAtlases)
var result = AtlasBuilder.Build( {
new AtlasBuildOptions var textures = ModContentTree.Build(mods, "Textures", ".png", ".jpg", ".jpeg", ".bmp");
{ var result = AtlasBuilder.Build(
OutputDirectory = atlasCacheDirectory, new AtlasBuildOptions
GroupDepth = ModAtlases.GroupDepth, {
}, OutputDirectory = atlasCacheDirectory,
textures.Files.Select(f => (f.FullPath, f.RelativePath)) GroupDepth = ModAtlases.GroupDepth,
); },
var built = result.Groups.Count(g => !g.Skipped); textures.Files.Select(f => (f.FullPath, f.RelativePath))
Log.Info( );
$"Atlases: {built} built, {result.Groups.Count - built} up to date ({result.Groups.Count} total)" var built = result.Groups.Count(g => !g.Skipped);
); Log.Info(
$"Atlases: {built} built, {result.Groups.Count - built} up to date ({result.Groups.Count} total)"
);
}
else
{
Log.Info("Atlases: skipped (headless)");
}
return new GameContent( return new GameContent(
mods, mods,
+1
View File
@@ -6,6 +6,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Host\MrGameEng.Host.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Content\MrGameEng.Content.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Content\MrGameEng.Content.csproj" />
+1 -1
View File
@@ -1,6 +1,6 @@
using LittleSim.Scenes; using LittleSim.Scenes;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Core; using MrGameEng.Host;
// Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне. // Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне.
using var host = new GameHost( using var host = new GameHost(
+79 -78
View File
@@ -1,78 +1,79 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Audio; using MrGameEng.Audio;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.UI; using MrGameEng.Host;
using Myra.Graphics2D.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Загрузочный экран. Поднимает аудио и управление скоростью, грузит настройки и применяет /// <summary>
/// окно/громкость, затем грузит контент Core-мода в фоне (чистый CPU/диск — без GPU) с /// Загрузочный экран. Поднимает аудио и управление скоростью, грузит настройки и применяет
/// анимированной надписью. По готовности регистрирует сервисы и уходит в главное меню. /// окно/громкость, затем грузит контент Core-мода в фоне (чистый CPU/диск — без GPU) с
/// </summary> /// анимированной надписью. По готовности регистрирует сервисы и уходит в главное меню.
public sealed class BootScene : Scene /// </summary>
{ public sealed class BootScene : Scene
private readonly string[] _steps = { "Загрузка", "Loading" }; {
private Task<GameContent>? _load; private readonly string[] _steps = { "Загрузка", "Loading" };
private GameSettings _settings = new(); private Task<GameContent>? _load;
private Label _label = null!; private GameSettings _settings = new();
private Label _label = null!;
protected override void OnLoad()
{ protected override void OnLoad()
Context.UseAudio(); {
Context.UseGameSpeed(1f, 3f, 6f); Context.UseAudio();
Context.UseGameSpeed(1f, 3f, 6f);
_settings = GameSettingsStore.Load();
var host = (GameHost)Context.Services.Get<Game>(); _settings = GameSettingsStore.Load();
host.Graphics.IsFullScreen = _settings.Fullscreen; var host = (GameHost)Context.Services.Get<Game>();
host.Graphics.SynchronizeWithVerticalRetrace = _settings.VSync; host.Graphics.IsFullScreen = _settings.Fullscreen;
host.Graphics.PreferredBackBufferWidth = _settings.Width; host.Graphics.SynchronizeWithVerticalRetrace = _settings.VSync;
host.Graphics.PreferredBackBufferHeight = _settings.Height; host.Graphics.PreferredBackBufferWidth = _settings.Width;
host.Graphics.ApplyChanges(); host.Graphics.PreferredBackBufferHeight = _settings.Height;
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume; host.Graphics.ApplyChanges();
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
_label = new Label var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
{ _label = new Label
TextColor = Ui.Accent, {
HorizontalAlignment = HorizontalAlignment.Center, TextColor = Ui.Accent,
VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
}; VerticalAlignment = VerticalAlignment.Center,
desktop.Root = Ui.Screen(_label); };
desktop.Root = Ui.Screen(_label);
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
_load = Task.Run(GameContent.Load); // Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
UpdateSystems.Add(new CallbackSystem(Tick)); _load = Task.Run(() => GameContent.Load());
} UpdateSystems.Add(new CallbackSystem(Tick));
}
private void Tick()
{ private void Tick()
var word = _settings.Language == "en" ? _steps[1] : _steps[0]; {
var dots = new string('.', (int)(Context.Clock.UnscaledTotalTime * 2) % 4); var word = _settings.Language == "en" ? _steps[1] : _steps[0];
_label.Text = word + dots; var dots = new string('.', (int)(Context.Clock.UnscaledTotalTime * 2) % 4);
_label.Text = word + dots;
if (_load is null || !_load.IsCompleted || Context.Scenes.IsTransitioning)
{ if (_load is null || !_load.IsCompleted || Context.Scenes.IsTransitioning)
return; {
} return;
}
if (_load.IsFaulted)
{ if (_load.IsFaulted)
_label.Text = _load.Exception?.GetBaseException().Message ?? "load failed"; {
Log.Error($"Content load failed: {_label.Text}"); _label.Text = _load.Exception?.GetBaseException().Message ?? "load failed";
return; Log.Error($"Content load failed: {_label.Text}");
} return;
}
var content = _load.Result;
_load = null; var content = _load.Result;
Context.Services.Add(content); _load = null;
Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory)); Context.Services.Add(content);
content.Languages.SetLanguage(_settings.Language); Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory));
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.6f)); content.Languages.SetLanguage(_settings.Language);
} Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.6f));
} }
}
+47 -46
View File
@@ -1,46 +1,47 @@
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Input; using MrGameEng.Host;
using MrGameEng.UI; using MrGameEng.Input;
using MrGameEng.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>Экран «Авторы»: демонстрационный, пока один автор. Назад — кнопкой или Esc.</summary>
public sealed class CreditsScene : Scene /// <summary>Экран «Авторы»: демонстрационный, пока один автор. Назад — кнопкой или Esc.</summary>
{ public sealed class CreditsScene : Scene
protected override void OnLoad() {
{ protected override void OnLoad()
var lang = Context.Services.Get<GameContent>().Languages; {
var input = this.UseInput(); var lang = Context.Services.Get<GameContent>().Languages;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var input = this.UseInput();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12);
column.Widgets.Add(Ui.Title(lang.Get("credits.title"))); var column = Ui.Column(12);
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author"))); column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role"))); column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back)); column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
desktop.Root = Ui.Screen(column);
UpdateSystems.Add( desktop.Root = Ui.Screen(column);
new CallbackSystem(() => UpdateSystems.Add(
{ new CallbackSystem(() =>
if (input.IsKeyPressed(Keys.Escape)) {
{ if (input.IsKeyPressed(Keys.Escape))
Back(); {
} Back();
}) }
); })
} );
}
private void Back()
{ private void Back()
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); {
} Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
}
+111 -110
View File
@@ -1,110 +1,111 @@
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Input; using MrGameEng.Host;
using MrGameEng.UI; using MrGameEng.Input;
using Myra.Graphics2D.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Экран загрузки: список сохранений из <see cref="SaveStore"/> (имя, размер, дата) с /// <summary>
/// кнопками «Загрузить» и «Удалить». Загрузка пересоздаёт мир из полного состояния симуляции. /// Экран загрузки: список сохранений из <see cref="SaveStore"/> (имя, размер, дата) с
/// </summary> /// кнопками «Загрузить» и «Удалить». Загрузка пересоздаёт мир из полного состояния симуляции.
public sealed class LoadGameScene : Scene /// </summary>
{ public sealed class LoadGameScene : Scene
private readonly SaveStore _store = new(); {
private GameContent _content = null!; private readonly SaveStore _store = new();
private Desktop _desktop = null!; private GameContent _content = null!;
private Desktop _desktop = null!;
protected override void OnLoad()
{ protected override void OnLoad()
_content = Context.Services.Get<GameContent>(); {
var input = this.UseInput(); _content = Context.Services.Get<GameContent>();
_desktop = this.UseUI(); var input = this.UseInput();
Rebuild(); _desktop = this.UseUI();
Rebuild();
UpdateSystems.Add(
new CallbackSystem(() => UpdateSystems.Add(
{ new CallbackSystem(() =>
if (input.IsKeyPressed(Keys.Escape)) {
{ if (input.IsKeyPressed(Keys.Escape))
Back(); {
} Back();
}) }
); })
} );
}
private void Rebuild()
{ private void Rebuild()
var lang = _content.Languages; {
var column = Ui.Column(8); var lang = _content.Languages;
column.Widgets.Add(Ui.Title(lang.Get("load.title"))); var column = Ui.Column(8);
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.Title(lang.Get("load.title")));
column.Widgets.Add(new Label { Height = 8 });
var saves = _store.List();
if (saves.Count == 0) var saves = _store.List();
{ if (saves.Count == 0)
column.Widgets.Add(Ui.Subtitle(lang.Get("load.empty"))); {
} column.Widgets.Add(Ui.Subtitle(lang.Get("load.empty")));
}
foreach (var entry in saves)
{ foreach (var entry in saves)
var save = entry.Save; {
var row = Ui.Row(8); var save = entry.Save;
row.Widgets.Add( var row = Ui.Row(8);
new Label row.Widgets.Add(
{ new Label
TextColor = Ui.Accent, {
Text = TextColor = Ui.Accent,
$"{save.Name} — {save.Width}×{save.Height} — " Text =
+ save.SavedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm"), $"{save.Name} — {save.Width}×{save.Height} — "
} + save.SavedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
); }
var file = entry.FileName; );
row.Widgets.Add( var file = entry.FileName;
Ui.Button( row.Widgets.Add(
lang.Get("load.load"), Ui.Button(
() => lang.Get("load.load"),
{ () =>
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
Context.Scenes.Switch( {
new WorldScene(save.ToConfig(), save), Context.Scenes.Switch(
Transition.Fade(0.6f) new WorldScene(save.ToConfig(), save),
); Transitions.Fade(0.6f)
} );
}, }
width: 130 },
) width: 130
); )
row.Widgets.Add( );
Ui.Button( row.Widgets.Add(
lang.Get("load.delete"), Ui.Button(
() => lang.Get("load.delete"),
{ () =>
_store.Delete(file); {
Rebuild(); _store.Delete(file);
}, Rebuild();
width: 130 },
) width: 130
); )
column.Widgets.Add(row); );
} column.Widgets.Add(row);
}
column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back)); column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
_desktop.Root = Ui.Screen(column);
} _desktop.Root = Ui.Screen(column);
}
private void Back()
{ private void Back()
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); {
} Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
}
+44 -43
View File
@@ -1,43 +1,44 @@
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.UI; using MrGameEng.Host;
using MrGameEng.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Главное меню: заголовок и кнопки Новый мир / Загрузка / Настройки / Авторы / Выход. /// <summary>
/// Все подписи — из локализации Core-мода. Чистый UI-экран (Myra), без мира и рендерера. /// Главное меню: заголовок и кнопки Новый мир / Загрузка / Настройки / Авторы / Выход.
/// </summary> /// Все подписи — из локализации Core-мода. Чистый UI-экран (Myra), без мира и рендерера.
public sealed class MainMenuScene : Scene /// </summary>
{ public sealed class MainMenuScene : Scene
protected override void OnLoad() {
{ protected override void OnLoad()
var content = Context.Services.Get<GameContent>(); {
var lang = content.Languages; var content = Context.Services.Get<GameContent>();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var lang = content.Languages;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12);
column.Widgets.Add(Ui.Title(lang.Get("menu.title"))); var column = Ui.Column(12);
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle"))); column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene()))); column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene()))); column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene()))); column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene()))); column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
column.Widgets.Add( column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit()) column.Widgets.Add(
); Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
);
desktop.Root = Ui.Screen(column);
} desktop.Root = Ui.Screen(column);
}
private void Go(Scene scene)
{ private void Go(Scene scene)
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
Context.Scenes.Switch(scene, Transition.Fade(0.4f)); {
} Context.Scenes.Switch(scene, Transitions.Fade(0.4f));
} }
} }
}
+167 -166
View File
@@ -1,166 +1,167 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Input; using MrGameEng.Host;
using MrGameEng.UI; using MrGameEng.Input;
using Myra.Graphics2D.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Окно настройки нового мира: имя, размер (пресеты из <see cref="WorldPresetDef"/> Core-мода), /// <summary>
/// сид (с кнопкой «случайно») и сглаживание рельефа. «Создать» запускает <see cref="WorldScene"/>. /// Окно настройки нового мира: имя, размер (пресеты из <see cref="WorldPresetDef"/> Core-мода),
/// </summary> /// сид (с кнопкой «случайно») и сглаживание рельефа. «Создать» запускает <see cref="WorldScene"/>.
public sealed class NewWorldScene : Scene /// </summary>
{ public sealed class NewWorldScene : Scene
private const int MinSmoothing = 1; {
private const int MaxSmoothing = 8; private const int MinSmoothing = 1;
private const int MaxSmoothing = 8;
private WorldPresetDef _preset = null!;
private int _smoothing = 4; private WorldPresetDef _preset = null!;
private TextBox _name = null!; private int _smoothing = 4;
private TextBox _seed = null!; private TextBox _name = null!;
private readonly List<Action> _refreshers = []; private TextBox _seed = null!;
private readonly List<Action> _refreshers = [];
protected override void OnLoad()
{ protected override void OnLoad()
var content = Context.Services.Get<GameContent>(); {
var lang = content.Languages; var content = Context.Services.Get<GameContent>();
var input = this.UseInput(); var lang = content.Languages;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var input = this.UseInput();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
_preset = presets[0]; var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
_preset = presets[0];
var column = Ui.Column(10);
column.Widgets.Add(Ui.Title(lang.Get("newworld.title"))); var column = Ui.Column(10);
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
column.Widgets.Add(new Label { Height = 8 });
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name)); _name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
// Размер — сегменты пресетов.
var sizeRow = Ui.Row(6); // Размер — сегменты пресетов.
sizeRow.Widgets.Add(new Label { Text = lang.Get("newworld.size"), TextColor = Ui.Muted }); var sizeRow = Ui.Row(6);
foreach (var preset in presets) sizeRow.Widgets.Add(new Label { Text = lang.Get("newworld.size"), TextColor = Ui.Muted });
{ foreach (var preset in presets)
var value = preset; {
var button = new TextButton { Text = lang.Get(preset.Label) }; var value = preset;
button.Click += (_, _) => var button = new TextButton { Text = lang.Get(preset.Label) };
{ button.Click += (_, _) =>
_preset = value; {
Refresh(); _preset = value;
}; Refresh();
sizeRow.Widgets.Add(button); };
_refreshers.Add(() => sizeRow.Widgets.Add(button);
button.TextColor = ReferenceEquals(_preset, value) ? Ui.Accent : Ui.Muted _refreshers.Add(() =>
); button.TextColor = ReferenceEquals(_preset, value) ? Ui.Accent : Ui.Muted
} );
}
column.Widgets.Add(sizeRow);
column.Widgets.Add(sizeRow);
// Сид + случайно.
_seed = new TextBox { Text = Random.Shared.Next().ToString(), Width = 200 }; // Сид + случайно.
var seedRow = Ui.Row(6); _seed = new TextBox { Text = Random.Shared.Next().ToString(), Width = 200 };
seedRow.Widgets.Add(new Label { Text = lang.Get("newworld.seed"), TextColor = Ui.Muted }); var seedRow = Ui.Row(6);
seedRow.Widgets.Add(_seed); seedRow.Widgets.Add(new Label { Text = lang.Get("newworld.seed"), TextColor = Ui.Muted });
seedRow.Widgets.Add( seedRow.Widgets.Add(_seed);
Ui.Button( seedRow.Widgets.Add(
lang.Get("newworld.random"), Ui.Button(
() => _seed.Text = Random.Shared.Next().ToString(), lang.Get("newworld.random"),
width: 120 () => _seed.Text = Random.Shared.Next().ToString(),
) width: 120
); )
column.Widgets.Add(seedRow); );
column.Widgets.Add(seedRow);
// Сглаживание /+.
var smoothRow = Ui.Row(6); // Сглаживание /+.
var smoothLabel = new Label { TextColor = Ui.Muted }; var smoothRow = Ui.Row(6);
_refreshers.Add(() => smoothLabel.Text = $"{lang.Get("newworld.smoothing")}: {_smoothing}"); var smoothLabel = new Label { TextColor = Ui.Muted };
var minus = new TextButton { Text = "" }; _refreshers.Add(() => smoothLabel.Text = $"{lang.Get("newworld.smoothing")}: {_smoothing}");
minus.Click += (_, _) => var minus = new TextButton { Text = "" };
{ minus.Click += (_, _) =>
_smoothing = Math.Max(MinSmoothing, _smoothing - 1); {
Refresh(); _smoothing = Math.Max(MinSmoothing, _smoothing - 1);
}; Refresh();
var plus = new TextButton { Text = "+" }; };
plus.Click += (_, _) => var plus = new TextButton { Text = "+" };
{ plus.Click += (_, _) =>
_smoothing = Math.Min(MaxSmoothing, _smoothing + 1); {
Refresh(); _smoothing = Math.Min(MaxSmoothing, _smoothing + 1);
}; Refresh();
smoothRow.Widgets.Add(smoothLabel); };
smoothRow.Widgets.Add(minus); smoothRow.Widgets.Add(smoothLabel);
smoothRow.Widgets.Add(plus); smoothRow.Widgets.Add(minus);
column.Widgets.Add(smoothRow); smoothRow.Widgets.Add(plus);
column.Widgets.Add(smoothRow);
column.Widgets.Add(new Label { Height = 8 });
var buttons = Ui.Row(10); column.Widgets.Add(new Label { Height = 8 });
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.create"), Create, width: 150)); var buttons = Ui.Row(10);
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150)); buttons.Widgets.Add(Ui.Button(lang.Get("newworld.create"), Create, width: 150));
column.Widgets.Add(buttons); buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
column.Widgets.Add(buttons);
desktop.Root = Ui.Screen(column);
Refresh(); desktop.Root = Ui.Screen(column);
Refresh();
UpdateSystems.Add(
new CallbackSystem(() => UpdateSystems.Add(
{ new CallbackSystem(() =>
if (input.IsKeyPressed(Keys.Escape)) {
{ if (input.IsKeyPressed(Keys.Escape))
Back(); {
} Back();
}) }
); })
} );
}
private static Widget LabeledRow(string label, Widget control)
{ private static Widget LabeledRow(string label, Widget control)
var row = Ui.Row(6); {
row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted }); var row = Ui.Row(6);
row.Widgets.Add(control); row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted });
return row; row.Widgets.Add(control);
} return row;
}
private void Refresh()
{ private void Refresh()
foreach (var refresh in _refreshers) {
{ foreach (var refresh in _refreshers)
refresh(); {
} refresh();
} }
}
private void Create()
{ private void Create()
if (Context.Scenes.IsTransitioning) {
{ if (Context.Scenes.IsTransitioning)
return; {
} return;
}
var seed = int.TryParse(_seed.Text, out var parsed) ? parsed : Random.Shared.Next();
var name = string.IsNullOrWhiteSpace(_name.Text) ? "World" : _name.Text.Trim(); var seed = int.TryParse(_seed.Text, out var parsed) ? parsed : Random.Shared.Next();
var config = new WorldConfig var name = string.IsNullOrWhiteSpace(_name.Text) ? "World" : _name.Text.Trim();
{ var config = new WorldConfig
Name = name, {
Width = _preset.Width, Name = name,
Height = _preset.Height, Width = _preset.Width,
Population = _preset.Population, Height = _preset.Height,
Seed = seed, Population = _preset.Population,
SmoothPasses = _smoothing, Seed = seed,
}; SmoothPasses = _smoothing,
Context.Scenes.Switch(new WorldScene(config), Transition.Fade(0.6f)); };
} Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f));
}
private void Back()
{ private void Back()
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); {
} Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
}
+168 -167
View File
@@ -1,167 +1,168 @@
using System; using System;
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Audio; using MrGameEng.Audio;
using MrGameEng.Core; using MrGameEng.Core;
using Myra.Graphics2D.Brushes; using MrGameEng.Host;
using Myra.Graphics2D.UI; using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Внутриигровое меню-пауза: затемняющий оверлей поверх мира (мир остаётся загруженным). /// <summary>
/// Открытие ставит игру на паузу через <see cref="GameSpeed"/>, закрытие — снимает. /// Внутриигровое меню-пауза: затемняющий оверлей поверх мира (мир остаётся загруженным).
/// Кнопки: Продолжить / Настройки (общая <see cref="SettingsPanel"/>) / Сохранить / /// Открытие ставит игру на паузу через <see cref="GameSpeed"/>, закрытие — снимает.
/// Главное меню / Выход. <see cref="Root"/> добавляется в корневую панель сцены. /// Кнопки: Продолжить / Настройки (общая <see cref="SettingsPanel"/>) / Сохранить /
/// </summary> /// Главное меню / Выход. <see cref="Root"/> добавляется в корневую панель сцены.
internal sealed class PauseMenu /// </summary>
{ internal sealed class PauseMenu
private readonly EngineContext _context; {
private readonly GameContent _content; private readonly EngineContext _context;
private readonly GameSpeed _speed; private readonly GameContent _content;
private readonly GameHost _host; private readonly GameSpeed _speed;
private readonly AudioManager _audio; private readonly GameHost _host;
private readonly Func<string> _onSave; private readonly AudioManager _audio;
private readonly Action _onMainMenu; private readonly Func<string> _onSave;
private readonly Action _onQuit; private readonly Action _onMainMenu;
private readonly Panel _overlay; private readonly Action _onQuit;
private GameSettings _settings = GameSettingsStore.Load(); private readonly Panel _overlay;
private bool _inSettings; private GameSettings _settings = GameSettingsStore.Load();
private string? _savedToast; private bool _inSettings;
private string? _savedToast;
public PauseMenu(
EngineContext context, public PauseMenu(
GameContent content, EngineContext context,
GameSpeed speed, GameContent content,
Func<string> onSave, GameSpeed speed,
Action onMainMenu, Func<string> onSave,
Action onQuit Action onMainMenu,
) Action onQuit
{ )
_context = context; {
_content = content; _context = context;
_speed = speed; _content = content;
_host = (GameHost)context.Services.Get<Game>(); _speed = speed;
_audio = context.Services.Get<AudioManager>(); _host = (GameHost)context.Services.Get<Game>();
_onSave = onSave; _audio = context.Services.Get<AudioManager>();
_onMainMenu = onMainMenu; _onSave = onSave;
_onQuit = onQuit; _onMainMenu = onMainMenu;
_onQuit = onQuit;
_overlay = new Panel
{ _overlay = new Panel
Visible = false, {
Background = new SolidBrush(new Color(0, 0, 0, 200)), Visible = false,
HorizontalAlignment = HorizontalAlignment.Stretch, Background = new SolidBrush(new Color(0, 0, 0, 200)),
VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
}; VerticalAlignment = VerticalAlignment.Stretch,
} };
}
/// <summary>Виджет оверлея для добавления в корень сцены.</summary>
public Widget Root => _overlay; /// <summary>Виджет оверлея для добавления в корень сцены.</summary>
public Widget Root => _overlay;
/// <summary>Открыт ли оверлей паузы.</summary>
public bool IsOpen { get; private set; } /// <summary>Открыт ли оверлей паузы.</summary>
public bool IsOpen { get; private set; }
/// <summary>
/// Реакция на Esc: закрыть подменю настроек → вернуться к кнопкам; иначе открыть/закрыть /// <summary>
/// меню паузы. /// Реакция на Esc: закрыть подменю настроек → вернуться к кнопкам; иначе открыть/закрыть
/// </summary> /// меню паузы.
public void Toggle() /// </summary>
{ public void Toggle()
if (!IsOpen) {
{ if (!IsOpen)
Open(); {
} Open();
else if (_inSettings) }
{ else if (_inSettings)
_inSettings = false; {
ShowButtons(); _inSettings = false;
} ShowButtons();
else }
{ else
Close(); {
} Close();
} }
}
private void Open()
{ private void Open()
IsOpen = true; {
_inSettings = false; IsOpen = true;
_savedToast = null; _inSettings = false;
_speed.Pause(); _savedToast = null;
ShowButtons(); _speed.Pause();
_overlay.Visible = true; ShowButtons();
} _overlay.Visible = true;
}
private void Close()
{ private void Close()
IsOpen = false; {
_inSettings = false; IsOpen = false;
_overlay.Visible = false; _inSettings = false;
_speed.Resume(); _overlay.Visible = false;
} _speed.Resume();
}
private void ShowButtons()
{ private void ShowButtons()
var lang = _content.Languages; {
var column = Ui.Column(10); var lang = _content.Languages;
column.Widgets.Add(Ui.Title(lang.Get("pause.title"))); var column = Ui.Column(10);
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.Title(lang.Get("pause.title")));
column.Widgets.Add(Ui.Button(lang.Get("pause.resume"), Close)); column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add( column.Widgets.Add(Ui.Button(lang.Get("pause.resume"), Close));
Ui.Button( column.Widgets.Add(
lang.Get("pause.settings"), Ui.Button(
() => lang.Get("pause.settings"),
{ () =>
_inSettings = true; {
ShowSettings(); _inSettings = true;
} ShowSettings();
) }
); )
column.Widgets.Add(Ui.Button(lang.Get("pause.save"), Save)); );
column.Widgets.Add(Ui.Button(lang.Get("pause.mainmenu"), _onMainMenu)); column.Widgets.Add(Ui.Button(lang.Get("pause.save"), Save));
column.Widgets.Add(Ui.Button(lang.Get("pause.quit"), _onQuit)); column.Widgets.Add(Ui.Button(lang.Get("pause.mainmenu"), _onMainMenu));
column.Widgets.Add(Ui.Button(lang.Get("pause.quit"), _onQuit));
if (_savedToast is not null)
{ if (_savedToast is not null)
column.Widgets.Add( {
new Label { Text = lang.Format("pause.saved", _savedToast), TextColor = Ui.Muted } column.Widgets.Add(
); new Label { Text = lang.Format("pause.saved", _savedToast), TextColor = Ui.Muted }
} );
}
SetContent(column);
} SetContent(column);
}
private void ShowSettings() =>
SetContent( private void ShowSettings() =>
SettingsPanel.Build( SetContent(
_settings, SettingsPanel.Build(
_content.Languages, _settings,
onApply: () => _content.Languages,
{ onApply: () =>
GameSettingsStore.Save(_settings); {
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio); GameSettingsStore.Save(_settings);
ShowSettings(); // язык мог смениться GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
}, ShowSettings(); // язык мог смениться
onBack: () => },
{ onBack: () =>
_inSettings = false; {
ShowButtons(); _inSettings = false;
} ShowButtons();
) }
); )
);
private void Save()
{ private void Save()
_savedToast = _onSave(); {
ShowButtons(); _savedToast = _onSave();
} ShowButtons();
}
private void SetContent(Widget content)
{ private void SetContent(Widget content)
_overlay.Widgets.Clear(); {
_overlay.Widgets.Add(content); _overlay.Widgets.Clear();
} _overlay.Widgets.Add(content);
} }
}
+66 -65
View File
@@ -1,65 +1,66 @@
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Audio; using MrGameEng.Audio;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Input; using MrGameEng.Host;
using MrGameEng.UI; using MrGameEng.Input;
using Myra.Graphics2D.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Экран настроек игры (язык, экран, громкость, разрешение). Использует общий /// <summary>
/// <see cref="SettingsPanel"/>. «Применить» сохраняет в settings.json и применяет к движку /// Экран настроек игры (язык, экран, громкость, разрешение). Использует общий
/// (смена языка перестраивает подписи на лету); «Назад»/Esc — в главное меню. /// <see cref="SettingsPanel"/>. «Применить» сохраняет в settings.json и применяет к движку
/// </summary> /// (смена языка перестраивает подписи на лету); «Назад»/Esc — в главное меню.
public sealed class SettingsScene : Scene /// </summary>
{ public sealed class SettingsScene : Scene
private GameContent _content = null!; {
private AudioManager _audio = null!; private GameContent _content = null!;
private GameHost _host = null!; private AudioManager _audio = null!;
private Desktop _desktop = null!; private GameHost _host = null!;
private GameSettings _settings = GameSettingsStore.Load(); private Desktop _desktop = null!;
private GameSettings _settings = GameSettingsStore.Load();
protected override void OnLoad()
{ protected override void OnLoad()
_content = Context.Services.Get<GameContent>(); {
_audio = Context.Services.Get<AudioManager>(); _content = Context.Services.Get<GameContent>();
_host = (GameHost)Context.Services.Get<Game>(); _audio = Context.Services.Get<AudioManager>();
var input = this.UseInput(); _host = (GameHost)Context.Services.Get<Game>();
var input = this.UseInput();
_desktop = this.UseUI();
Rebuild(); _desktop = this.UseUI();
Rebuild();
UpdateSystems.Add(
new CallbackSystem(() => UpdateSystems.Add(
{ new CallbackSystem(() =>
if (input.IsKeyPressed(Keys.Escape)) {
{ if (input.IsKeyPressed(Keys.Escape))
Back(); {
} Back();
}) }
); })
} );
}
private void Rebuild() =>
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back)); private void Rebuild() =>
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back));
private void Apply()
{ private void Apply()
GameSettingsStore.Save(_settings); {
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio); GameSettingsStore.Save(_settings);
Rebuild(); // язык мог смениться — перестраиваем подписи GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
} Rebuild(); // язык мог смениться — перестраиваем подписи
}
private void Back()
{ private void Back()
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); {
} Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
}
+312 -311
View File
@@ -1,311 +1,312 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Friflo.Engine.ECS; using Friflo.Engine.ECS;
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Sim; using LittleSim.Sim;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets; using MrGameEng.Assets;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.DevConsole; using MrGameEng.DevConsole;
using MrGameEng.Graphics; using MrGameEng.Graphics;
using MrGameEng.Input; using MrGameEng.Host;
using MrGameEng.Inspector; using MrGameEng.Input;
using MrGameEng.Lighting; using MrGameEng.Inspector;
using MrGameEng.Tilemaps; using MrGameEng.Lighting;
using MrGameEng.UI; using MrGameEng.Tilemaps;
using Myra.Graphics2D; using MrGameEng.UI;
using Myra.Graphics2D.UI; using Myra.Graphics2D;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
namespace LittleSim.Scenes;
/// <summary>
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из /// <summary>
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка /// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа /// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие /// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн. /// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие
/// </summary> /// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн.
public sealed class WorldScene : Scene /// </summary>
{ public sealed class WorldScene : Scene
public const int CellSize = 16; {
public const int CellSize = 16;
/// <summary>
/// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые /// <summary>
/// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с. /// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые
/// </summary> /// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с.
public const float SecondsPerDay = 480f; /// </summary>
public const float SecondsPerDay = 480f;
private readonly WorldConfig _config;
private readonly WorldSave? _save; private readonly WorldConfig _config;
private readonly RectF _bounds; private readonly WorldSave? _save;
private readonly RectF _bounds;
private GameSpeed _speed = null!;
private PauseMenu _pause = null!; private GameSpeed _speed = null!;
private readonly List<Action> _speedRefreshers = []; private PauseMenu _pause = null!;
private readonly List<Action> _speedRefreshers = [];
/// <summary>Новый мир из конфига.</summary>
public WorldScene(WorldConfig config) /// <summary>Новый мир из конфига.</summary>
: this(config, null) { } public WorldScene(WorldConfig config)
: this(config, null) { }
/// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
public WorldScene(WorldConfig config, WorldSave? save) /// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
{ public WorldScene(WorldConfig config, WorldSave? save)
_config = config; {
_save = save; _config = config;
_bounds = new RectF(0f, 0f, config.Width * CellSize, config.Height * CellSize); _save = save;
} _bounds = new RectF(0f, 0f, config.Width * CellSize, config.Height * CellSize);
}
protected override void OnLoad()
{ protected override void OnLoad()
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets(); {
var content = Context.Services.Get<GameContent>(); var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
var atlases = Context.Services.Get<ModAtlases>(); var content = Context.Services.Get<GameContent>();
var device = Context.GraphicsDevice; var atlases = Context.Services.Get<ModAtlases>();
var input = this.UseInput(); var device = Context.GetGraphicsDevice();
_speed = Context.Services.Get<GameSpeed>(); var input = this.UseInput();
_speed.SetStep(0); // новый мир/загрузка стартуют на x1 _speed = Context.Services.Get<GameSpeed>();
_speed.SetStep(0); // новый мир/загрузка стартуют на x1
var renderer = this.UseRenderer2D(
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) } var renderer = this.UseRenderer2D(
); new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
GameLayers.EnsureRegistered(renderer); );
this.UseTilemaps(); GameLayers.EnsureRegistered(renderer);
this.UseTilemaps();
var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default); var calendar = Context.UseCalendar(SecondsPerDay);
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер var climate = Context.UseClimate(ClimateSettings.Default);
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
// Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed).
var plants = new PlantSet(content, atlases, device); // Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed).
var random = new Random(_config.Seed); var plants = new PlantSet(content, atlases, device);
BuildTerrain(content, atlases, device, assets, plants, random); var random = new Random(_config.Seed);
BuildTerrain(content, atlases, device, assets, plants, random);
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
// HUD, полоса скорости и меню-пауза в одной корневой панели.
var desktop = this.UseUI(); // HUD, полоса скорости и меню-пауза в одной корневой панели.
var hudLabel = new Label { Left = 10, Top = 8 }; var desktop = this.UseUI();
var speedBar = BuildSpeedBar(content); var hudLabel = new Label { Left = 10, Top = 8 };
_pause = new PauseMenu( var speedBar = BuildSpeedBar(content);
Context, _pause = new PauseMenu(
content, Context,
_speed, content,
onSave: SaveWorld, _speed,
onMainMenu: () => Switch(new MainMenuScene()), onSave: SaveWorld,
onQuit: () => Context.Services.Get<Game>().Exit() onMainMenu: () => Switch(new MainMenuScene()),
); onQuit: () => Context.Services.Get<Game>().Exit()
desktop.Root = Ui.Screen(hudLabel, speedBar, _pause.Root); );
desktop.Root = Ui.Screen(hudLabel, speedBar, _pause.Root);
this.UseInspector(renderer);
var console = this.UseDevConsole(); this.UseInspector(renderer);
RegisterCommands(console, content, atlases); var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize));
UpdateSystems.Add( UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize));
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) UpdateSystems.Add(
); new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
UpdateSystems.Add( );
new HudSystem( UpdateSystems.Add(
Context, new HudSystem(
content.Languages, Context,
hudLabel, content.Languages,
"hud.world", hudLabel,
_config.Seed, "hud.world",
calendar, _config.Seed,
climate calendar,
) climate
); )
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input))); );
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
Log.Info(
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}" Log.Info(
+ (_save is null ? " (new)" : " (loaded)") $"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
); + (_save is null ? " (new)" : " (loaded)")
} );
}
/// <summary>
/// Строит одну сущность-<see cref="Tilemap"/>: тайлсет из дефов рельефа (поверхность из /// <summary>
/// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а /// Строит одну сущность-<see cref="Tilemap"/>: тайлсет из дефов рельефа (поверхность из
/// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости). /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а
/// </summary> /// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости).
private void BuildTerrain( /// </summary>
GameContent content, private void BuildTerrain(
ModAtlases atlases, GameContent content,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device, ModAtlases atlases,
AssetManager assets, Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
PlantSet plants, AssetManager assets,
Random random PlantSet plants,
) Random random
{ )
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); {
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
var tiles = new TileSet(); // Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
var tileByDef = new Dictionary<TerrainDef, ushort>(); var tiles = new TileSet();
foreach (var terrain in content.Terrains.All) var tileByDef = new Dictionary<TerrainDef, ushort>();
{ foreach (var terrain in content.Terrains.All)
tileByDef[terrain] = terrain.Surface is { } surface {
? tiles.Add(atlases.GetRegion(device, surface)) tileByDef[terrain] = terrain.Surface is { } surface
: tiles.Add(white, terrain.Tint); ? tiles.Add(atlases.GetRegion(device, surface))
} : tiles.Add(white, terrain.Tint);
}
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
var heights = WorldGenerator.Generate( // Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
_config.Width, var heights = WorldGenerator.Generate(
_config.Height, _config.Width,
_config.Seed, _config.Height,
_config.SmoothPasses _config.Seed,
); _config.SmoothPasses
var grid = new TileGrid(_config.Width, _config.Height); );
for (var x = 0; x < _config.Width; x++) var grid = new TileGrid(_config.Width, _config.Height);
{ for (var x = 0; x < _config.Width; x++)
for (var y = 0; y < _config.Height; y++) {
{ for (var y = 0; y < _config.Height; y++)
var terrain = content.Terrains.Classify(heights[x, y]); {
grid[x, y] = tileByDef[terrain]; var terrain = content.Terrains.Classify(heights[x, y]);
ScatterSpawner.Spawn( grid[x, y] = tileByDef[terrain];
this, ScatterSpawner.Spawn(
content, this,
plants, content,
terrain, plants,
new Point(x, y), terrain,
CellSize, new Point(x, y),
random CellSize,
); random
} );
} }
}
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
} Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
}
private HorizontalStackPanel BuildSpeedBar(GameContent content)
{ private HorizontalStackPanel BuildSpeedBar(GameContent content)
var bar = Ui.Row(6); {
bar.HorizontalAlignment = HorizontalAlignment.Center; var bar = Ui.Row(6);
bar.VerticalAlignment = VerticalAlignment.Bottom; bar.HorizontalAlignment = HorizontalAlignment.Center;
bar.Margin = new Thickness(0, 0, 0, 12); bar.VerticalAlignment = VerticalAlignment.Bottom;
bar.Margin = new Thickness(0, 0, 0, 12);
var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
pause.Click += (_, _) => _speed.Pause(); var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
bar.Widgets.Add(pause); pause.Click += (_, _) => _speed.Pause();
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted); bar.Widgets.Add(pause);
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
for (var i = 0; i < _speed.Steps.Count; i++)
{ for (var i = 0; i < _speed.Steps.Count; i++)
var index = i; {
var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" }; var index = i;
button.Click += (_, _) => _speed.SetStep(index); var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
bar.Widgets.Add(button); button.Click += (_, _) => _speed.SetStep(index);
_speedRefreshers.Add(() => bar.Widgets.Add(button);
button.TextColor = _speedRefreshers.Add(() =>
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted button.TextColor =
); !_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
} );
}
void Refresh()
{ void Refresh()
foreach (var refresh in _speedRefreshers) {
{ foreach (var refresh in _speedRefreshers)
refresh(); {
} refresh();
} }
}
_speed.Changed += Refresh;
RegisterUnload(() => _speed.Changed -= Refresh); _speed.Changed += Refresh;
Refresh(); RegisterUnload(() => _speed.Changed -= Refresh);
return bar; Refresh();
} return bar;
}
private void Hotkeys(InputManager input)
{ private void Hotkeys(InputManager input)
if (input.IsKeyPressed(Keys.Escape)) {
{ if (input.IsKeyPressed(Keys.Escape))
_pause.Toggle(); {
} _pause.Toggle();
}
if (_pause.IsOpen)
{ if (_pause.IsOpen)
return; {
} return;
}
if (input.IsKeyPressed(Keys.Space))
{ if (input.IsKeyPressed(Keys.Space))
_speed.TogglePause(); {
} _speed.TogglePause();
}
if (input.IsKeyPressed(Keys.D1))
{ if (input.IsKeyPressed(Keys.D1))
_speed.SetStep(0); {
} _speed.SetStep(0);
}
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
{ if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
_speed.SetStep(1); {
} _speed.SetStep(1);
}
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
{ if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
_speed.SetStep(2); {
} _speed.SetStep(2);
} }
}
private string SaveWorld()
{ private string SaveWorld()
// Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида. {
var save = new WorldSave // Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
{ var save = new WorldSave
Name = _config.Name, {
Width = _config.Width, Name = _config.Name,
Height = _config.Height, Width = _config.Width,
Seed = _config.Seed, Height = _config.Height,
SmoothPasses = _config.SmoothPasses, Seed = _config.Seed,
Population = _config.Population, SmoothPasses = _config.SmoothPasses,
SavedUtc = DateTime.UtcNow, Population = _config.Population,
ElapsedSeconds = Context.Clock.TotalTime, SavedUtc = DateTime.UtcNow,
}; ElapsedSeconds = Context.Clock.TotalTime,
};
new SaveStore().Write(save);
Log.Info($"World '{_config.Name}' saved"); new SaveStore().Write(save);
return _config.Name; Log.Info($"World '{_config.Name}' saved");
} return _config.Name;
}
private void Switch(Scene scene)
{ private void Switch(Scene scene)
if (!Context.Scenes.IsTransitioning) {
{ if (!Context.Scenes.IsTransitioning)
_speed.Resume(); {
Context.Scenes.Switch(scene, Transition.Fade(0.5f)); _speed.Resume();
} Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
} }
}
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
{ private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
ContentCommands.Register(console, content, atlases); {
console.Register( ContentCommands.Register(console, content, atlases);
"regen", console.Register(
"regen [seed] — regenerate the world with a new seed", "regen",
(c, args) => "regen [seed] — regenerate the world with a new seed",
{ (c, args) =>
if (Context.Scenes.IsTransitioning) {
{ if (Context.Scenes.IsTransitioning)
return; {
} return;
}
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
c.WriteLine($"regenerating world, seed {seed}"); var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
Context.Scenes.Switch( c.WriteLine($"regenerating world, seed {seed}");
new WorldScene(_config with { Seed = seed }), Context.Scenes.Switch(
Transition.Fade(0.6f) new WorldScene(_config with { Seed = seed }),
); Transitions.Fade(0.6f)
} );
); }
console.Register( );
"menu", console.Register(
"menu — return to the main menu", "menu",
(_, _) => Switch(new MainMenuScene()) "menu — return to the main menu",
); (_, _) => Switch(new MainMenuScene())
} );
} }
}