Compare commits
27
Commits
@@ -14,12 +14,18 @@ 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: the world headless (engine HeadlessHost) +
|
||||||
Cache/ runtime-built atlases (gitignored)
|
WebSocket replication (MrGameEng.Net); also fast-forward and probe modes
|
||||||
docs/ концепт, симуляция, моды, roadmap (Russian)
|
src/LittleSim.Web browser client (Blazor WASM + KNI/WebGL): connects to the dedicated
|
||||||
LittleSim.sln game + engine sources + engine tests — one window for everything
|
server, renders replicated pawns; mirrors the net contract (NetContract.cs)
|
||||||
|
because KNI and DesktopGL assemblies can't mix — keep in sync with
|
||||||
|
src/LittleSim/Net/NetSchema.cs. Outside LittleSim.sln's test flow.
|
||||||
|
Mods/Core the game's own content as a mod: About, Defs, Languages, Textures
|
||||||
|
Cache/ runtime-built atlases (gitignored)
|
||||||
|
docs/ концепт, симуляция, моды, roadmap (Russian)
|
||||||
|
LittleSim.sln game + engine sources + engine tests — one window for everything
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
@@ -28,6 +34,11 @@ 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 run --project src/LittleSim.Server -- --listen # multiplayer world (WebSocket)
|
||||||
|
dotnet run --project src/LittleSim -- --connect # client → ws://localhost:9050
|
||||||
|
dotnet run --project src/LittleSim.Server -- --probe # CLI check of a running server
|
||||||
|
dotnet run --project src/LittleSim.Web # browser client (?server=ws://…)
|
||||||
dotnet test LittleSim.sln # runs the engine test suites
|
dotnet test LittleSim.sln # runs the engine test suites
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+171
-186
@@ -15,16 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core", "engine\sr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics", "engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj", "{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics", "engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj", "{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Input", "engine\src\MrGameEng.Input\MrGameEng.Input.csproj", "{7D7E001D-72ED-4204-94D5-B8A0F706D544}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio", "engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj", "{A2620CBF-3404-4583-AD55-FCC8A704BD9A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio", "engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj", "{A2620CBF-3404-4583-AD55-FCC8A704BD9A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets", "engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj", "{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI", "engine\src\MrGameEng.UI\MrGameEng.UI.csproj", "{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI", "engine\src\MrGameEng.UI\MrGameEng.UI.csproj", "{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.DevConsole", "engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj", "{0C370DCA-18AB-4A79-9C60-BE00E7A57180}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets.Generator", "engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj", "{BA456A86-0960-45C3-B48E-5882D7A3873F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets.Generator", "engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj", "{BA456A86-0960-45C3-B48E-5882D7A3873F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CD3B5CE5-C23F-38B4-04AA-A303F94731A5}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CD3B5CE5-C23F-38B4-04AA-A303F94731A5}"
|
||||||
@@ -33,27 +27,31 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets.Generator.
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core.Tests", "engine\tests\MrGameEng.Core.Tests\MrGameEng.Core.Tests.csproj", "{2994C301-DFD3-48D1-B44C-F01ED28FBE93}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core.Tests", "engine\tests\MrGameEng.Core.Tests\MrGameEng.Core.Tests.csproj", "{2994C301-DFD3-48D1-B44C-F01ED28FBE93}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.DevConsole.Tests", "engine\tests\MrGameEng.DevConsole.Tests\MrGameEng.DevConsole.Tests.csproj", "{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics.Tests", "engine\tests\MrGameEng.Graphics.Tests\MrGameEng.Graphics.Tests.csproj", "{21B1878F-2532-4D48-95DD-1C7268DCF1D5}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics.Tests", "engine\tests\MrGameEng.Graphics.Tests\MrGameEng.Graphics.Tests.csproj", "{21B1878F-2532-4D48-95DD-1C7268DCF1D5}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Input.Tests", "engine\tests\MrGameEng.Input.Tests\MrGameEng.Input.Tests.csproj", "{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Content", "engine\src\MrGameEng.Content\MrGameEng.Content.csproj", "{EC5504F5-483B-4197-AA19-E8FD710583F4}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Pathfinding", "engine\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj", "{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Simulation", "engine\src\MrGameEng.Simulation\MrGameEng.Simulation.csproj", "{9E653B28-0060-46DA-84D0-DA8655B07AF6}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Collisions", "engine\src\MrGameEng.Collisions\MrGameEng.Collisions.csproj", "{9406DB4C-FABF-48FE-BEA7-25AD319D2172}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Content.Tests", "engine\tests\MrGameEng.Content.Tests\MrGameEng.Content.Tests.csproj", "{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Pathfinding.Tests", "engine\tests\MrGameEng.Pathfinding.Tests\MrGameEng.Pathfinding.Tests.csproj", "{47E5E756-64F9-4B5A-8EC3-B016745166C4}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Simulation.Tests", "engine\tests\MrGameEng.Simulation.Tests\MrGameEng.Simulation.Tests.csproj", "{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Collisions.Tests", "engine\tests\MrGameEng.Collisions.Tests\MrGameEng.Collisions.Tests.csproj", "{B9161776-32FE-415E-9401-DC5BB87FC225}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engine\tests\MrGameEng.UI.Tests\MrGameEng.UI.Tests.csproj", "{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Mods", "engine\src\MrGameEng.Mods\MrGameEng.Mods.csproj", "{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}"
|
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.Mods.Tests", "engine\tests\MrGameEng.Mods.Tests\MrGameEng.Mods.Tests.csproj", "{07986629-FE7C-42BE-868B-0FCA1915D2A2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "engine\src\MrGameEng.Host\MrGameEng.Host.csproj", "{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.AI", "engine\src\MrGameEng.AI\MrGameEng.AI.csproj", "{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host.Tests", "engine\tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.AI.Tests", "engine\tests\MrGameEng.AI.Tests\MrGameEng.AI.Tests.csproj", "{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net", "engine\src\MrGameEng.Net\MrGameEng.Net.csproj", "{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net.Tests", "engine\tests\MrGameEng.Net.Tests\MrGameEng.Net.Tests.csproj", "{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Server", "src\LittleSim.Server\LittleSim.Server.csproj", "{95630217-333B-4263-84AE-5EE5961FC307}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Web", "src\LittleSim.Web\LittleSim.Web.csproj", "{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -101,18 +99,6 @@ Global
|
|||||||
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x64.Build.0 = Release|Any CPU
|
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.ActiveCfg = Release|Any CPU
|
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.Build.0 = Release|Any CPU
|
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -125,18 +111,6 @@ Global
|
|||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x64.Build.0 = Release|Any CPU
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.ActiveCfg = Release|Any CPU
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.Build.0 = Release|Any CPU
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -149,18 +123,6 @@ Global
|
|||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x64.Build.0 = Release|Any CPU
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.ActiveCfg = Release|Any CPU
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.Build.0 = Release|Any CPU
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -197,18 +159,6 @@ Global
|
|||||||
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x64.Build.0 = Release|Any CPU
|
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.ActiveCfg = Release|Any CPU
|
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.Build.0 = Release|Any CPU
|
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -221,114 +171,150 @@ Global
|
|||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x64.Build.0 = Release|Any CPU
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.ActiveCfg = Release|Any CPU
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.Build.0 = Release|Any CPU
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x64.Build.0 = Debug|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x86.Build.0 = Debug|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x64.ActiveCfg = Release|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x64.Build.0 = Release|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x86.ActiveCfg = Release|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x86.Build.0 = Release|Any CPU
|
{EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x64.Build.0 = Debug|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x86.Build.0 = Debug|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x64.ActiveCfg = Release|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x64.Build.0 = Release|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x86.ActiveCfg = Release|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x86.Build.0 = Release|Any CPU
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x64.Build.0 = Debug|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x86.Build.0 = Debug|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x64.ActiveCfg = Release|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x64.Build.0 = Release|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x86.ActiveCfg = Release|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x86.Build.0 = Release|Any CPU
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x64.Build.0 = Debug|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x86.Build.0 = Debug|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x64.ActiveCfg = Release|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x64.Build.0 = Release|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x86.ActiveCfg = Release|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x86.Build.0 = Release|Any CPU
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x64.Build.0 = Debug|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x86.Build.0 = Debug|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|Any CPU.Build.0 = Release|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x64.ActiveCfg = Release|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x64.Build.0 = Release|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x86.ActiveCfg = Release|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x86.Build.0 = Release|Any CPU
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x64.Build.0 = Debug|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x86.Build.0 = Debug|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x64.ActiveCfg = Release|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x64.Build.0 = Release|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x86.ActiveCfg = Release|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x86.Build.0 = Release|Any CPU
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x64.Build.0 = Debug|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x86.Build.0 = Debug|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x64.ActiveCfg = Release|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x64.Build.0 = Release|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x86.ActiveCfg = Release|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x86.Build.0 = Release|Any CPU
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x64.Build.0 = Debug|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x86.Build.0 = Debug|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|Any CPU.Build.0 = Release|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x64.ActiveCfg = Release|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x64.Build.0 = Release|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x86.ActiveCfg = Release|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x86.Build.0 = Release|Any CPU
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x64.Build.0 = Debug|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x86.Build.0 = Debug|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|Any CPU.Build.0 = Release|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x64.ActiveCfg = Release|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x64.Build.0 = Release|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x86.ActiveCfg = Release|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x86.Build.0 = Release|Any CPU
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -338,25 +324,24 @@ Global
|
|||||||
{F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} = {D585295F-7994-3649-1065-7AA403C41681}
|
{F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} = {D585295F-7994-3649-1065-7AA403C41681}
|
||||||
{FE6C04D3-E619-4C77-A971-1357463676B7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{FE6C04D3-E619-4C77-A971-1357463676B7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{7D7E001D-72ED-4204-94D5-B8A0F706D544} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
|
||||||
{A2620CBF-3404-4583-AD55-FCC8A704BD9A} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{A2620CBF-3404-4583-AD55-FCC8A704BD9A} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
|
||||||
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{0C370DCA-18AB-4A79-9C60-BE00E7A57180} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
|
||||||
{BA456A86-0960-45C3-B48E-5882D7A3873F} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{BA456A86-0960-45C3-B48E-5882D7A3873F} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{CD3B5CE5-C23F-38B4-04AA-A303F94731A5} = {D585295F-7994-3649-1065-7AA403C41681}
|
{CD3B5CE5-C23F-38B4-04AA-A303F94731A5} = {D585295F-7994-3649-1065-7AA403C41681}
|
||||||
{83548DF5-75BC-45EF-8911-83BE5173CD2D} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{83548DF5-75BC-45EF-8911-83BE5173CD2D} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{2994C301-DFD3-48D1-B44C-F01ED28FBE93} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{2994C301-DFD3-48D1-B44C-F01ED28FBE93} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
|
||||||
{21B1878F-2532-4D48-95DD-1C7268DCF1D5} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{21B1878F-2532-4D48-95DD-1C7268DCF1D5} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{EC5504F5-483B-4197-AA19-E8FD710583F4} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{9E653B28-0060-46DA-84D0-DA8655B07AF6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{9406DB4C-FABF-48FE-BEA7-25AD319D2172} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{47E5E756-64F9-4B5A-8EC3-B016745166C4} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{B9161776-32FE-415E-9401-DC5BB87FC225} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{07986629-FE7C-42BE-868B-0FCA1915D2A2} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{841EAE51-E86B-4BFB-A70C-A67E4D492DD7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
|
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
|
{95630217-333B-4263-84AE-5EE5961FC307} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
|
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Организм-агностичные гены: каждый ген задаёт, как генерируются/мутируют аллели и как ген
|
||||||
|
// вкладывается в признаки (effects: имя признака → формула; value — выраженное значение гена).
|
||||||
|
// Этот набор покрывает измерения генома растения (его используют системы роста/жизненного цикла).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
|
||||||
|
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 },
|
||||||
|
|
||||||
|
{ "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight",
|
||||||
|
"default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"],
|
||||||
|
"effects": { "optimalLight": "value" } },
|
||||||
|
{ "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance",
|
||||||
|
"default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"],
|
||||||
|
"effects": { "lightTolerance": "value" } },
|
||||||
|
{ "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature",
|
||||||
|
"default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "optimalTemperature": "value" } },
|
||||||
|
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
|
||||||
|
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "temperatureTolerance": "value" } },
|
||||||
|
{ "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
|
||||||
|
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
||||||
|
"effects": { "optimalFertility": "value" } },
|
||||||
|
{ "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance",
|
||||||
|
"default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
||||||
|
"effects": { "fertilityTolerance": "value" } },
|
||||||
|
|
||||||
|
{ "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor",
|
||||||
|
"default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"],
|
||||||
|
"effects": { "vigor": "value" } },
|
||||||
|
{ "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan",
|
||||||
|
"default": 160, "min": 5, "max": 500, "tags": ["lifecycle"],
|
||||||
|
"effects": { "lifespan": "value" } },
|
||||||
|
{ "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange",
|
||||||
|
"default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"],
|
||||||
|
"effects": { "dispersalRange": "value" } },
|
||||||
|
{ "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval",
|
||||||
|
"default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"],
|
||||||
|
"effects": { "reproduceInterval": "value" } },
|
||||||
|
{ "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination",
|
||||||
|
"default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
||||||
|
"effects": { "selfPollination": "value" } },
|
||||||
|
{ "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate",
|
||||||
|
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
||||||
|
"effects": { "mutationRate": "value" } },
|
||||||
|
|
||||||
|
// --- Контент (фаза G4): плодоношение, добыча, цвет ---
|
||||||
|
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
|
||||||
|
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
|
||||||
|
"effects": { "fruitYield": "value" } },
|
||||||
|
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
|
||||||
|
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
|
||||||
|
"effects": { "fruitSeason": "value" } },
|
||||||
|
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
|
||||||
|
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
|
||||||
|
"effects": { "harvestAmount": "value" } },
|
||||||
|
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
|
||||||
|
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
|
||||||
|
"effects": { "leafHue": "value" } },
|
||||||
|
|
||||||
|
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
|
||||||
|
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
|
||||||
|
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
|
||||||
|
"default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"],
|
||||||
|
"effects": { "hardiness": "gsum('Gene.*Tolerance')" } },
|
||||||
|
|
||||||
|
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
||||||
|
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
||||||
|
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
||||||
|
"effects": { "variant": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "Patch",
|
||||||
|
// Контент-патчи (фаза G5): применяются ко всем дефам типа defType, чьё имя подходит под regex match,
|
||||||
|
// проставляя поля set. Демонстрация: даём кустам древесину при сборе (веточки).
|
||||||
|
"patches": [
|
||||||
|
{ "defType": "Plant", "match": "Bush.*", "set": { "harvestProduct": "ProductWood" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,15 +1,46 @@
|
|||||||
{
|
{
|
||||||
"type": "Plant",
|
"type": "Plant",
|
||||||
"defs": [
|
"defs": [
|
||||||
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28 },
|
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
|
||||||
|
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
|
||||||
|
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
|
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
|
||||||
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
|
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
|
||||||
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
|
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
|
||||||
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
|
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
|
||||||
|
|
||||||
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2 },
|
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
|
||||||
|
"harvestProduct": "ProductGrass",
|
||||||
|
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
|
||||||
|
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
|
||||||
|
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
|
||||||
|
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
||||||
|
"harvestAmount": 2, "leafHue": 0.36 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "sizeCells": 1.2, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4 },
|
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
|
||||||
|
"fruitProduct": "ProductBerry",
|
||||||
|
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13,
|
||||||
|
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
|
||||||
|
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
||||||
|
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
|
||||||
|
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
|
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
|
||||||
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
|
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
|
||||||
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
|
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "Product",
|
||||||
|
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
|
||||||
|
// количество задаётся генами (harvestAmount / fruitYield).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
|
||||||
|
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
|
||||||
|
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
||||||
|
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,19 +4,19 @@
|
|||||||
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
|
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
|
||||||
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
|
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
|
||||||
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120],
|
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120],
|
||||||
"isLand": true, "surface": "terrain/surfaces/sand" },
|
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4 },
|
||||||
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70],
|
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70],
|
||||||
"isLand": true, "surface": "terrain/surfaces/mossy",
|
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
|
||||||
"scatter": [
|
"scatter": [
|
||||||
{ "chance": 0.35, "options": ["GrassA"] },
|
{ "chance": 0.35, "options": ["GrassA"] },
|
||||||
{ "chance": 0.07, "options": ["BushA", "BushB", "BushC"] }
|
{ "chance": 0.07, "options": ["BushA", "BushB", "BushC"] }
|
||||||
] },
|
] },
|
||||||
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
||||||
"isLand": true, "surface": "terrain/surfaces/soil",
|
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
||||||
"scatter": [
|
"scatter": [
|
||||||
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
|
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
|
||||||
] },
|
] },
|
||||||
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
|
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
|
||||||
"surface": "terrain/surfaces/roughhewnrock" }
|
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "WorldPreset",
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 80, "height": 50, "population": 40 },
|
||||||
|
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 120, "height": 70, "population": 80 },
|
||||||
|
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 180, "height": 110, "population": 140 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,21 +1,76 @@
|
|||||||
{
|
{
|
||||||
"hud.world": "LittleSim | seed {0} | population {1} | {2} FPS",
|
"hud.world": "LittleSim | seed {0} | {1} | {2} FPS",
|
||||||
"hud.terrain": "LittleSim | terrain | seed {0} | animals: {1}",
|
"hud.datetime": "day {0}, {1:00}:{2:00}",
|
||||||
"hud.controls": "WASD — camera, wheel — zoom, ` — console ('help' lists commands)",
|
"hud.climate": "{0} {1}°C",
|
||||||
|
"season.spring": "spring",
|
||||||
|
"season.summer": "summer",
|
||||||
|
"season.autumn": "autumn",
|
||||||
|
"season.winter": "winter",
|
||||||
|
"hud.controls": "WASD — camera, wheel — zoom, ` — console, F1 — inspector",
|
||||||
|
"hud.paused": "PAUSED",
|
||||||
|
"menu.title": "LittleSim",
|
||||||
|
"menu.subtitle": "a god-game: minimal graphics, deep simulation",
|
||||||
|
"menu.newworld": "New World",
|
||||||
|
"menu.load": "Load",
|
||||||
|
"menu.settings": "Settings",
|
||||||
|
"menu.credits": "Credits",
|
||||||
|
"menu.quit": "Quit",
|
||||||
|
"newworld.title": "New World",
|
||||||
|
"newworld.name": "Name",
|
||||||
|
"newworld.defaultname": "New World",
|
||||||
|
"newworld.size": "Size",
|
||||||
|
"newworld.seed": "Seed",
|
||||||
|
"newworld.random": "Random",
|
||||||
|
"newworld.smoothing": "Terrain smoothing",
|
||||||
|
"newworld.create": "Create",
|
||||||
|
"newworld.back": "Back",
|
||||||
|
"load.title": "Load",
|
||||||
|
"load.empty": "No saves yet",
|
||||||
|
"load.load": "Load",
|
||||||
|
"load.delete": "Delete",
|
||||||
|
"load.back": "Back",
|
||||||
|
"settings.title": "Settings",
|
||||||
|
"settings.language": "Language",
|
||||||
|
"settings.fullscreen": "Fullscreen",
|
||||||
|
"settings.vsync": "Vertical sync",
|
||||||
|
"settings.resolution": "Resolution",
|
||||||
|
"settings.volume": "Volume",
|
||||||
|
"settings.apply": "Apply",
|
||||||
|
"settings.back": "Back",
|
||||||
|
"pause.title": "Paused",
|
||||||
|
"pause.resume": "Resume",
|
||||||
|
"pause.settings": "Settings",
|
||||||
|
"pause.save": "Save",
|
||||||
|
"pause.mainmenu": "Main menu",
|
||||||
|
"pause.quit": "Quit",
|
||||||
|
"pause.saved": "Saved: {0}",
|
||||||
|
"speed.pause": "⏸",
|
||||||
|
"credits.title": "Credits",
|
||||||
|
"credits.author": "mrleo1nid",
|
||||||
|
"credits.role": "Concept, code, mrgameeng engine",
|
||||||
|
"credits.back": "Back",
|
||||||
|
"preset.small": "Small",
|
||||||
|
"preset.medium": "Medium",
|
||||||
|
"preset.large": "Large",
|
||||||
"terrain.deepwater": "deep water",
|
"terrain.deepwater": "deep water",
|
||||||
"terrain.water": "water",
|
"terrain.water": "water",
|
||||||
"terrain.sand": "sand",
|
"terrain.sand": "sand",
|
||||||
"terrain.grass": "grass",
|
"terrain.grass": "grass",
|
||||||
"terrain.forest": "forest",
|
"terrain.forest": "forest",
|
||||||
"terrain.mountain": "mountains",
|
"terrain.mountain": "mountains",
|
||||||
|
|
||||||
"plant.oak": "oak",
|
"plant.oak": "oak",
|
||||||
"plant.birch": "birch",
|
"plant.birch": "birch",
|
||||||
"plant.pine": "pine",
|
"plant.pine": "pine",
|
||||||
"plant.grass": "grass tuft",
|
"plant.grass": "grass tuft",
|
||||||
"plant.bush": "bush",
|
"plant.bush": "bush",
|
||||||
|
"plant.stage.seedling": "seedling",
|
||||||
|
"plant.stage.sprout": "sprout",
|
||||||
|
"plant.stage.sapling": "sapling",
|
||||||
|
"plant.stage.mature": "mature",
|
||||||
|
"product.wood": "wood",
|
||||||
|
"product.grass": "grass",
|
||||||
|
"product.berry": "berries",
|
||||||
|
"product.acorn": "acorn",
|
||||||
"pawn.being": "being",
|
"pawn.being": "being",
|
||||||
"pawn.bear": "bear",
|
"pawn.bear": "bear",
|
||||||
"pawn.deer": "deer",
|
"pawn.deer": "deer",
|
||||||
@@ -24,5 +79,10 @@
|
|||||||
"pawn.boar": "boar",
|
"pawn.boar": "boar",
|
||||||
"pawn.wolf": "wolf",
|
"pawn.wolf": "wolf",
|
||||||
"pawn.muffalo": "muffalo",
|
"pawn.muffalo": "muffalo",
|
||||||
"pawn.squirrel": "squirrel"
|
"pawn.squirrel": "squirrel",
|
||||||
|
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
||||||
|
"net.connecting": "connecting…",
|
||||||
|
"net.connected": "connected",
|
||||||
|
"net.failed": "connection failed",
|
||||||
|
"net.lost": "connection lost"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,76 @@
|
|||||||
{
|
{
|
||||||
"hud.world": "LittleSim | сид {0} | население {1} | {2} FPS",
|
"hud.world": "LittleSim | сид {0} | {1} | {2} FPS",
|
||||||
"hud.terrain": "LittleSim | террейн | сид {0} | животных: {1}",
|
"hud.datetime": "день {0}, {1:00}:{2:00}",
|
||||||
"hud.controls": "WASD — камера, колесо — зум, ` — консоль ('help' — список команд)",
|
"hud.climate": "{0} {1}°C",
|
||||||
|
"season.spring": "весна",
|
||||||
|
"season.summer": "лето",
|
||||||
|
"season.autumn": "осень",
|
||||||
|
"season.winter": "зима",
|
||||||
|
"hud.controls": "WASD — камера, колесо — зум, ` — консоль, F1 — инспектор",
|
||||||
|
"hud.paused": "ПАУЗА",
|
||||||
|
"menu.title": "LittleSim",
|
||||||
|
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
|
||||||
|
"menu.newworld": "Новый мир",
|
||||||
|
"menu.load": "Загрузка",
|
||||||
|
"menu.settings": "Настройки",
|
||||||
|
"menu.credits": "Авторы",
|
||||||
|
"menu.quit": "Выход",
|
||||||
|
"newworld.title": "Новый мир",
|
||||||
|
"newworld.name": "Название",
|
||||||
|
"newworld.defaultname": "Новый мир",
|
||||||
|
"newworld.size": "Размер",
|
||||||
|
"newworld.seed": "Сид",
|
||||||
|
"newworld.random": "Случайно",
|
||||||
|
"newworld.smoothing": "Сглаживание рельефа",
|
||||||
|
"newworld.create": "Создать",
|
||||||
|
"newworld.back": "Назад",
|
||||||
|
"load.title": "Загрузка",
|
||||||
|
"load.empty": "Сохранений пока нет",
|
||||||
|
"load.load": "Загрузить",
|
||||||
|
"load.delete": "Удалить",
|
||||||
|
"load.back": "Назад",
|
||||||
|
"settings.title": "Настройки",
|
||||||
|
"settings.language": "Язык",
|
||||||
|
"settings.fullscreen": "Полный экран",
|
||||||
|
"settings.vsync": "Вертикальная синхронизация",
|
||||||
|
"settings.resolution": "Разрешение",
|
||||||
|
"settings.volume": "Громкость",
|
||||||
|
"settings.apply": "Применить",
|
||||||
|
"settings.back": "Назад",
|
||||||
|
"pause.title": "Пауза",
|
||||||
|
"pause.resume": "Продолжить",
|
||||||
|
"pause.settings": "Настройки",
|
||||||
|
"pause.save": "Сохранить",
|
||||||
|
"pause.mainmenu": "Главное меню",
|
||||||
|
"pause.quit": "Выход",
|
||||||
|
"pause.saved": "Сохранено: {0}",
|
||||||
|
"speed.pause": "⏸",
|
||||||
|
"credits.title": "Авторы",
|
||||||
|
"credits.author": "mrleo1nid",
|
||||||
|
"credits.role": "Идея, код, движок mrgameeng",
|
||||||
|
"credits.back": "Назад",
|
||||||
|
"preset.small": "Маленький",
|
||||||
|
"preset.medium": "Средний",
|
||||||
|
"preset.large": "Большой",
|
||||||
"terrain.deepwater": "глубокая вода",
|
"terrain.deepwater": "глубокая вода",
|
||||||
"terrain.water": "вода",
|
"terrain.water": "вода",
|
||||||
"terrain.sand": "песок",
|
"terrain.sand": "песок",
|
||||||
"terrain.grass": "трава",
|
"terrain.grass": "трава",
|
||||||
"terrain.forest": "лес",
|
"terrain.forest": "лес",
|
||||||
"terrain.mountain": "горы",
|
"terrain.mountain": "горы",
|
||||||
|
|
||||||
"plant.oak": "дуб",
|
"plant.oak": "дуб",
|
||||||
"plant.birch": "берёза",
|
"plant.birch": "берёза",
|
||||||
"plant.pine": "сосна",
|
"plant.pine": "сосна",
|
||||||
"plant.grass": "пучок травы",
|
"plant.grass": "пучок травы",
|
||||||
"plant.bush": "куст",
|
"plant.bush": "куст",
|
||||||
|
"plant.stage.seedling": "росток",
|
||||||
|
"plant.stage.sprout": "всходы",
|
||||||
|
"plant.stage.sapling": "саженец",
|
||||||
|
"plant.stage.mature": "взрослое",
|
||||||
|
"product.wood": "древесина",
|
||||||
|
"product.grass": "трава",
|
||||||
|
"product.berry": "ягоды",
|
||||||
|
"product.acorn": "жёлудь",
|
||||||
"pawn.being": "житель",
|
"pawn.being": "житель",
|
||||||
"pawn.bear": "медведь",
|
"pawn.bear": "медведь",
|
||||||
"pawn.deer": "олень",
|
"pawn.deer": "олень",
|
||||||
@@ -24,5 +79,10 @@
|
|||||||
"pawn.boar": "кабан",
|
"pawn.boar": "кабан",
|
||||||
"pawn.wolf": "волк",
|
"pawn.wolf": "волк",
|
||||||
"pawn.muffalo": "муффало",
|
"pawn.muffalo": "муффало",
|
||||||
"pawn.squirrel": "белка"
|
"pawn.squirrel": "белка",
|
||||||
|
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||||
|
"net.connecting": "подключение…",
|
||||||
|
"net.connected": "подключено",
|
||||||
|
"net.failed": "не удалось подключиться",
|
||||||
|
"net.lost": "соединение потеряно"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
|
||||||
<SkinSet>
|
<SkinSet>
|
||||||
|
|
||||||
<lifeStage>
|
<lifeStage>
|
||||||
<appliesToAll>true</appliesToAll>
|
<appliesToAll>true</appliesToAll>
|
||||||
<variants>
|
<variants>
|
||||||
@@ -9,24 +8,21 @@
|
|||||||
<commonality>0</commonality>
|
<commonality>0</commonality>
|
||||||
<texName>GuineaPig</texName>
|
<texName>GuineaPig</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.4</commonality>
|
<commonality>0.4</commonality>
|
||||||
<texName>GuineaPig1</texName>
|
<texName>GuineaPig1</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.4</commonality>
|
<commonality>0.4</commonality>
|
||||||
<texName>GuineaPig2</texName>
|
<texName>GuineaPig2</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.4</commonality>
|
<commonality>0.4</commonality>
|
||||||
<texName>GuineaPig3</texName>
|
<texName>GuineaPig3</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
</variants>
|
||||||
|
|
||||||
</variants>
|
|
||||||
</lifeStage>
|
</lifeStage>
|
||||||
|
|
||||||
</SkinSet>
|
</SkinSet>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
|
||||||
<SkinSet>
|
<SkinSet>
|
||||||
|
|
||||||
<lifeStage>
|
<lifeStage>
|
||||||
<appliesToAll>true</appliesToAll>
|
<appliesToAll>true</appliesToAll>
|
||||||
<variants>
|
<variants>
|
||||||
@@ -9,38 +8,36 @@
|
|||||||
<commonality>0.12</commonality>
|
<commonality>0.12</commonality>
|
||||||
<texName>Horse</texName>
|
<texName>Horse</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.25</commonality>
|
<commonality>0.25</commonality>
|
||||||
<texName>Horse1</texName>
|
<texName>Horse1</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.4</commonality>
|
<commonality>0.4</commonality>
|
||||||
<texName>Horse2</texName>
|
<texName>Horse2</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.4</commonality>
|
<commonality>0.4</commonality>
|
||||||
<texName>Horse3</texName>
|
<texName>Horse3</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.4</commonality>
|
<commonality>0.4</commonality>
|
||||||
<texName>Horse4</texName>
|
<texName>Horse4</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.35</commonality>
|
<commonality>0.35</commonality>
|
||||||
<texName>Horse5</texName>
|
<texName>Horse5</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
|
||||||
<skin>
|
<skin>
|
||||||
<commonality>0.25</commonality>
|
<commonality>0.25</commonality>
|
||||||
<texName>Horse6</texName>
|
<texName>Horse6</texName>
|
||||||
</skin>
|
</skin>
|
||||||
|
</variants>
|
||||||
</variants>
|
|
||||||
</lifeStage>
|
</lifeStage>
|
||||||
|
|
||||||
</SkinSet>
|
</SkinSet>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Веб-клиент: spike KNI и решение A/B
|
||||||
|
|
||||||
|
Дата: 2026-06-12. Спайк жил в `spikes/KniWeb`; после успеха повышен до
|
||||||
|
**`src/LittleSim.Web`** — рабочего браузерного клиента мультиплеера (Blazor WASM + KNI):
|
||||||
|
он подключается к `LittleSim.Server --listen` по WebSocket, применяет дельта-снапшоты
|
||||||
|
`MrGameEng.Net` и рисует жителей через WebGL со сглаживанием позиций. Адрес сервера —
|
||||||
|
`?server=ws://host:port` в URL страницы (по умолчанию — хост страницы, порт 9050).
|
||||||
|
Сетевой контракт там продублирован бинарным зеркалом (`NetContract.cs`) — KNI- и
|
||||||
|
DesktopGL-сборки нельзя смешивать, пока графика движка не собирается пер-платформенно.
|
||||||
|
|
||||||
|
## Вопрос
|
||||||
|
|
||||||
|
Путь A — «MonoGame в браузере» через [KNI](https://github.com/kniEngine/kni)
|
||||||
|
(форк MonoGame с платформой Blazor WebAssembly/WebGL, те же неймспейсы
|
||||||
|
`Microsoft.Xna.Framework.*`). Путь B — тонкий веб-клиент без MonoGame
|
||||||
|
(TypeScript/PixiJS поверх сетевой репликации). Спайк проверял минимальную
|
||||||
|
жизнеспособность пути A: **ядро движка + Friflo + WebGL-спрайт в браузере**.
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
`dotnet new kni-blazor-gl` (пакет шаблонов `nkast.Kni.Templates`, KNI 4.2.9001,
|
||||||
|
net8.0) + ProjectReference на `engine/src/MrGameEng.Core` + мини-хост в духе
|
||||||
|
`GameHost` поверх KNI `Game`. Сцена: 300 сущностей в Friflo `EntityStore`,
|
||||||
|
`QuerySystem` двигает их в Update-фазе (отскок от краёв, seed 42), Draw-фаза
|
||||||
|
рисует через KNI `SpriteBatch` (WebGL).
|
||||||
|
|
||||||
|
## Результат — путь A жизнеспособен
|
||||||
|
|
||||||
|
- **`MrGameEng.Core` работает в Blazor WASM без изменений**: `EngineContext`,
|
||||||
|
`GameClock`, `Scene`/`SceneManager`, тайминг переходов — всё ядро завелось
|
||||||
|
как есть (заслуга расслоения Core/Host: в ядре нет ни MonoGame, ни платформы).
|
||||||
|
- **Friflo.Engine.ECS 3.6 работает в wasm**: создание сущностей, архетипы,
|
||||||
|
`QuerySystem`, `ForEachEntity` — без ошибок в консоли браузера.
|
||||||
|
- **KNI 4.2.9001 рендерит через WebGL** с XNA-API: `Game`,
|
||||||
|
`GraphicsDeviceManager`, `SpriteBatch`, `Texture2D.SetData` — совпадает с
|
||||||
|
кодом, который пишется под десктопный MonoGame.
|
||||||
|
- Сборка тривиальна: обычный `Microsoft.NET.Sdk.BlazorWebAssembly` проект,
|
||||||
|
никаких wasm-workload-плясок не понадобилось.
|
||||||
|
|
||||||
|
## Известные ограничения пути A (работа на этапе «веб-клиент»)
|
||||||
|
|
||||||
|
1. **Пер-платформенная компиляция библиотек движка.** `MrGameEng.Graphics`,
|
||||||
|
`Content`, `Audio`, `UI` ссылаются на `MonoGame.Framework.DesktopGL`; для
|
||||||
|
веба их надо собирать против пакетов `nkast.*` (типы те же по API, но другие
|
||||||
|
сборки). Решение — msbuild-условие (`KniPlatform=BlazorGL` → nkast-пакеты),
|
||||||
|
без изменения исходников.
|
||||||
|
2. **Шейдеры.** `Renderer2D` использует прекомпилированные `dotnet-mgfxc`
|
||||||
|
эффекты — KNI имеет собственный компилятор эффектов; совместимость надо
|
||||||
|
проверять отдельным спайком, прежде чем тащить батчер в веб.
|
||||||
|
3. **Нет файловой системы.** Моды/дефы/атласы в браузер приезжают по HTTP;
|
||||||
|
текущая схема «собрать атласы при старте из PNG» в вебе не работает —
|
||||||
|
атласы пре-билдятся и кладутся в `wwwroot` (или приезжают с сервера).
|
||||||
|
4. **Потоки.** `Task.Run`-загрузка контента и `Thread.Sleep`-пейсинг не для
|
||||||
|
браузера (клиенту `HeadlessHost.Run` и не нужен — цикл гонит
|
||||||
|
`requestAnimationFrame` через KNI).
|
||||||
|
5. **Производительность не мерялась** (300 спрайтов — гладко); бюджет
|
||||||
|
сущностей в wasm-интерпретаторе будет заметно ниже десктопного, замерять
|
||||||
|
на реальной сцене с включённым AOT.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
**Путь A (KNI)** — основной для веб-клиента: переиспользуем ядро, сцены и
|
||||||
|
в перспективе графику движка; код игры один на все платформы. Путь B остаётся
|
||||||
|
запасным, если упрёмся в шейдеры (п. 2) или производительность (п. 5).
|
||||||
|
|
||||||
|
Порядок работ не меняется: сначала `MrGameEng.Net` (WebSocket-транспорт +
|
||||||
|
репликация — нужен любому пути) и сетевой мультиплеер на десктопе, затем
|
||||||
|
`MrGameEng.Host.Web` поверх KNI по образцу спайка.
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Растения как экосистема
|
||||||
|
|
||||||
|
Дизайн системы растений: рост зависит от среды, среда описана факторами, реакция растения на
|
||||||
|
факторы задаётся **генами**, а размножение передаёт гены потомкам с мутацией — появляется
|
||||||
|
естественный отбор. Документ — единый ориентир для фаз A–D; детали каждой фазы уточняются её
|
||||||
|
отдельным планом.
|
||||||
|
|
||||||
|
## Модель роста
|
||||||
|
|
||||||
|
Скорость роста растения в данный момент:
|
||||||
|
|
||||||
|
```
|
||||||
|
скорость = база × пригодность(свет) × пригодность(температура) × пригодность(плодородность)
|
||||||
|
```
|
||||||
|
|
||||||
|
Каждая «пригодность» — число в [0..1] из кривой отклика (движковый `ResponseCurve`, модуль
|
||||||
|
`MrGameEng.AI`) с **оптимумом** и **шириной толерантности**, которые берутся из генов растения.
|
||||||
|
Растение вне своих оптимумов растёт медленно или стоит; в своих — быстро. Так все факторы среды
|
||||||
|
сводятся к одному механизму, а различие видов/особей — целиком в генах.
|
||||||
|
|
||||||
|
Фактор «возраст/стадия» остаётся как сейчас (`PlantGrowthStage` в дефах): пригодности умножают
|
||||||
|
прирост `AgeDays`, стадии переключают спрайт/размер по накопленному возрасту.
|
||||||
|
|
||||||
|
## Факторы среды
|
||||||
|
|
||||||
|
- **Свет** — амбиент день/ночь (`MrGameEng.Lighting.DayNight`), сэмплируемый в точке
|
||||||
|
(`SampleAt(world)`); глобальный в фазе A, локальный (с тенями) в фазе D.
|
||||||
|
- **Температура** — непрерывная, из климата (`MrGameEng.Core.Climate`): сезонный синус по дню года
|
||||||
|
+ суточные колебания (холоднее ночью).
|
||||||
|
- **Плодородность** — пока константа типа террейна (`TerrainDef.Fertility`); позже возможна
|
||||||
|
по-клеточная почва с истощением/восстановлением.
|
||||||
|
|
||||||
|
## Гены (диплоидный геном, гибридная экспрессия)
|
||||||
|
|
||||||
|
Каждое растение несёт **геном**: по паре аллелей на ген. Экспрессия гибридная:
|
||||||
|
|
||||||
|
- **Числовые гены** (оптимум света, толерантность к свету, оптимум температуры, толерантность,
|
||||||
|
потребность в плодородности, макс. размер, базовая скорость, продолжительность жизни, дальность
|
||||||
|
расселения, плодовитость, темп мутаций, способность к самоопылению) — фенотип = **смешение**
|
||||||
|
(среднее двух аллелей). Реализм отбора: промежуточные значения, плавный дрейф.
|
||||||
|
- **Дискретные гены** (например, цвет/морфа, наличие признака) — **доминант/рецессив**: рецессивный
|
||||||
|
фенотип виден только в гомозиготе (aa).
|
||||||
|
|
||||||
|
Наследование (мейоз): каждый родитель отдаёт по одному случайному аллелю на ген; потомок получает
|
||||||
|
по одному от каждого. Затем — мутация (с вероятностью из гена темпа мутаций сдвигаем числовой
|
||||||
|
аллель/переключаем дискретный). Всё детерминировано сидом мира.
|
||||||
|
|
||||||
|
Стартовая популяция: геномы генерируются из сида (вид задаёт базовый геном и разброс аллелей в
|
||||||
|
дефе; особи отклоняются).
|
||||||
|
|
||||||
|
## Размножение (полуреалистичное)
|
||||||
|
|
||||||
|
- **Перекрёстное опыление (приоритет):** зрелое растение ищет зрелого соседа того же вида в радиусе
|
||||||
|
(ген дальности; запрос через spatial-hash движка). Есть партнёр → семя с геномом от двух
|
||||||
|
родителей (Мендель + мутация).
|
||||||
|
- **Самоопыление (запас):** нет партнёра и ген «способность к самоопылению» позволяет → семя из
|
||||||
|
одного родителя (само-скрещивание аллелей + мутация).
|
||||||
|
- **Расселение:** семя падает в клетку в радиусе расселения; прорастает, если клетка подходит
|
||||||
|
(суша, не занято сверх лимита, пригодность не нулевая).
|
||||||
|
- **Лимит плотности:** не больше N растений на клетку/площадь — иначе взрыв численности.
|
||||||
|
- **Старение/смерть:** возраст превышает ген жизни → смерть; или длительный стресс (пригодность
|
||||||
|
около нуля) → гибель. Смерть освобождает место — баланс с размножением и основа отбора.
|
||||||
|
|
||||||
|
## Производительность и сейв
|
||||||
|
|
||||||
|
- Представление — **по сущности на растение** (видно в рендере и ECS-инспекторе, индивидуальные
|
||||||
|
гены). Защита от взрыва: лимит плотности, смерть, разумные радиусы, проверки размножения по
|
||||||
|
таймеру/сезону (не каждый кадр на каждое растение), spatial-hash для соседей.
|
||||||
|
- **Персист:** геномы, позиции, возраст и стадия растений сохраняются в `WorldSave` (мир больше не
|
||||||
|
воспроизводится только из сида — эволюция переживает загрузку).
|
||||||
|
- **Детерминизм:** мутации/расселение/опыление — на сидированном `Random`, как в остальной симуляции.
|
||||||
|
|
||||||
|
## Фазы реализации
|
||||||
|
|
||||||
|
- **A — климат и день/ночь (движок).** `Climate` (температура/сезон поверх `Calendar`),
|
||||||
|
`DayNight` (амбиент, `Renderer2D.AmbientLight` на World-слоях, сэмплируемый `SampleAt`),
|
||||||
|
затемнение мира ночью, HUD: сезон/температура/время. Фундамент для влияния среды на рост.
|
||||||
|
- **B — гены и рост по пригодностям (игра).** Компонент `PlantGenome`, расчёт пригодностей
|
||||||
|
(свет из `DayNight.SampleAt`, температура из `Climate`, плодородность из террейна), замена
|
||||||
|
плоского `GrowthRate`. Геном виден/правится в инспекторе.
|
||||||
|
- **C — размножение и смерть (игра).** Перекрёстное/самоопыление, расселение, лимит плотности,
|
||||||
|
старение/стресс-смерть, персист популяции в сейв.
|
||||||
|
- **D — полноценное 2D-освещение (движок).** Точечные источники + тени (окклюзия от препятствий);
|
||||||
|
`DayNight.SampleAt` начинает возвращать локальную освещённость, и затенённые растения растут хуже.
|
||||||
+1
-1
Submodule engine updated: 1f87fb0b74...d044cafad9
@@ -0,0 +1,22 @@
|
|||||||
|
param(
|
||||||
|
[ValidateSet("Debug", "Release")]
|
||||||
|
[string]$Configuration = "Release"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||||
|
$Project = Join-Path $Root "src\LittleSim\LittleSim.csproj"
|
||||||
|
|
||||||
|
Push-Location $Root
|
||||||
|
try {
|
||||||
|
dotnet build LittleSim.sln -c $Configuration
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
dotnet run --project $Project -c $Configuration --no-build @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
CONFIGURATION="${CONFIGURATION:-Release}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-c | --configuration)
|
||||||
|
CONFIGURATION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
dotnet build LittleSim.sln -c "$CONFIGURATION"
|
||||||
|
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Net;
|
||||||
|
using LittleSim.Scenes;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
namespace LittleSim.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Серверный мир: жители бродят, устают и отдыхают (те же системы, что в WorldScene) на
|
||||||
|
/// фиксированном тике <see cref="HeadlessHost"/> — без окна, GPU и спрайтов. С
|
||||||
|
/// <see cref="WebSocketServer"/> мир ещё и реплицируется подключённым клиентам по схеме
|
||||||
|
/// <see cref="NetSchema"/> (10 снапшотов в секунду, дельты). Демонстрация MrGameEng.Net.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class HeadlessWorldScene : Scene
|
||||||
|
{
|
||||||
|
/// <summary>Жителей в серверном мире.</summary>
|
||||||
|
public const int PawnCount = 24;
|
||||||
|
|
||||||
|
/// <summary>Сид мира — рельефа пока нет, но блуждание детерминировано им.</summary>
|
||||||
|
public const int Seed = 424242;
|
||||||
|
|
||||||
|
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
|
||||||
|
|
||||||
|
private readonly GameContent _content;
|
||||||
|
private readonly WebSocketServer? _server;
|
||||||
|
|
||||||
|
/// <summary>Мир без сети (fast-forward) или, с <paramref name="server"/>, онлайн-мир.</summary>
|
||||||
|
public HeadlessWorldScene(GameContent content, WebSocketServer? server = null)
|
||||||
|
{
|
||||||
|
_content = content;
|
||||||
|
_server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
Context.Services.Add(_content);
|
||||||
|
var calendar = Context.UseCalendar(WorldScene.SecondsPerDay);
|
||||||
|
var climate = Context.UseClimate(ClimateSettings.Default);
|
||||||
|
|
||||||
|
var replication = _server is null ? null : new ReplicationServer(NetSchema.Create(), Store);
|
||||||
|
var random = new Random(Seed);
|
||||||
|
for (var i = 0; i < PawnCount; i++)
|
||||||
|
{
|
||||||
|
var position = new Vector2(
|
||||||
|
random.NextSingle() * Bounds.Width,
|
||||||
|
random.NextSingle() * Bounds.Height
|
||||||
|
);
|
||||||
|
var transform = new Transform2D(position, scale: new Vector2(12f));
|
||||||
|
var needs = new PawnNeeds { Energy = 0.4f + random.NextSingle() * 0.6f };
|
||||||
|
if (replication is null)
|
||||||
|
{
|
||||||
|
Store.CreateEntity(transform, new Wander(), new PawnBrain(), needs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Store.CreateEntity(
|
||||||
|
transform,
|
||||||
|
new Wander(),
|
||||||
|
new PawnBrain(),
|
||||||
|
needs,
|
||||||
|
new NetId { Value = replication.NextNetId() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSystems.Add(new PawnDecisionSystem());
|
||||||
|
UpdateSystems.Add(new PawnNeedsSystem());
|
||||||
|
UpdateSystems.Add(new WanderSystem(Seed, Bounds));
|
||||||
|
UpdateSystems.Add(new DayReportSystem(calendar, climate));
|
||||||
|
if (_server is not null && replication is not null)
|
||||||
|
{
|
||||||
|
UpdateSystems.Add(new NetworkSystem(_server, replication));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Принимает новые соединения и шлёт дельта-снапшоты с сетевой частотой.</summary>
|
||||||
|
private sealed class NetworkSystem(WebSocketServer server, ReplicationServer replication)
|
||||||
|
: BaseSystem
|
||||||
|
{
|
||||||
|
// 60 тиков симуляции / 6 = 10 снапшотов в секунду.
|
||||||
|
private const int SendEveryTicks = 6;
|
||||||
|
|
||||||
|
private int _ticks;
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
while (server.TryAcceptConnection(out var connection))
|
||||||
|
{
|
||||||
|
Log.Info(
|
||||||
|
$"Клиент #{connection.Id} подключился ({server.Connections.Count} онлайн)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++_ticks % SendEveryTicks == 0)
|
||||||
|
{
|
||||||
|
replication.Send(server.Connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Net;
|
||||||
|
using LittleSim.Scenes;
|
||||||
|
using LittleSim.Server;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
// Дедикейтед-сервер LittleSim: мир без окна и GPU на HeadlessHost движка.
|
||||||
|
// dotnet run --project src/LittleSim.Server — fast-forward N дней
|
||||||
|
// dotnet run --project src/LittleSim.Server -- --days 30 — то же, явное число дней
|
||||||
|
// dotnet run --project src/LittleSim.Server -- --listen [--port N] — онлайн-мир (WebSocket)
|
||||||
|
// dotnet run --project src/LittleSim.Server -- --probe [--port N] — проверка: подключиться
|
||||||
|
// к серверу и показать реплицированных жителей
|
||||||
|
|
||||||
|
var days = ReadOption("--days", 10);
|
||||||
|
var ticksPerSecond = ReadOption("--tps", 60);
|
||||||
|
var port = ReadOption("--port", NetSchema.DefaultPort);
|
||||||
|
|
||||||
|
Log.MessageLogged += (level, message) => Console.WriteLine($"[{level}] {message}");
|
||||||
|
|
||||||
|
if (Array.IndexOf(args, "--probe") >= 0)
|
||||||
|
{
|
||||||
|
return await Probe(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
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} жителей"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.IndexOf(args, "--listen") >= 0)
|
||||||
|
{
|
||||||
|
using var socketServer = new WebSocketServer(port);
|
||||||
|
socketServer.Start();
|
||||||
|
using var host = new HeadlessHost(
|
||||||
|
new HeadlessHostOptions { TicksPerSecond = ticksPerSecond, Realtime = true },
|
||||||
|
new HeadlessWorldScene(content, socketServer)
|
||||||
|
);
|
||||||
|
using var cancel = new CancellationTokenSource();
|
||||||
|
Console.CancelKeyPress += (_, eventArgs) =>
|
||||||
|
{
|
||||||
|
eventArgs.Cancel = true;
|
||||||
|
cancel.Cancel();
|
||||||
|
};
|
||||||
|
Log.Info(
|
||||||
|
$"Мир онлайн: ws://localhost:{port}, {HeadlessWorldScene.PawnCount} жителей, "
|
||||||
|
+ $"{ticksPerSecond} тиков/с. Клиент: dotnet run --project src/LittleSim -- --connect. "
|
||||||
|
+ "Ctrl+C — остановка."
|
||||||
|
);
|
||||||
|
host.Run(cancel.Token);
|
||||||
|
Log.Info($"Сервер остановлен на тике {host.TickCount}.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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} тиков/с)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Подключается к работающему серверу, секунду слушает снапшоты и показывает жителей —
|
||||||
|
// консольная проверка репликации без игрового клиента.
|
||||||
|
async Task<int> Probe(int probePort)
|
||||||
|
{
|
||||||
|
var store = new EntityStore();
|
||||||
|
var replication = new ReplicationClient(NetSchema.Create(), store);
|
||||||
|
var uri = new Uri($"ws://localhost:{probePort}/");
|
||||||
|
Log.Info($"Подключение к {uri}…");
|
||||||
|
WebSocketClient connection;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
connection = await WebSocketClient.ConnectAsync(
|
||||||
|
uri,
|
||||||
|
new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Log.Error($"Не подключилось: {exception.GetBaseException().Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (connection)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
replication.Pump(connection);
|
||||||
|
var first = SamplePositions(store);
|
||||||
|
Log.Info($"Снапшот получен: {replication.EntityCount} жителей");
|
||||||
|
|
||||||
|
await Task.Delay(2000);
|
||||||
|
replication.Pump(connection);
|
||||||
|
var second = SamplePositions(store);
|
||||||
|
for (var i = 0; i < first.Count; i++)
|
||||||
|
{
|
||||||
|
Log.Info(
|
||||||
|
$" житель {first[i].NetId}: ({first[i].X:F0}, {first[i].Y:F0}) → "
|
||||||
|
+ $"({second[i].X:F0}, {second[i].Y:F0})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var moved = first.Where((p, i) => p.X != second[i].X || p.Y != second[i].Y).Count();
|
||||||
|
Log.Info($"Двигались {moved} из {first.Count} показанных — мир жив.");
|
||||||
|
return replication.EntityCount > 0 && moved > 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<(int NetId, float X, float Y)> SamplePositions(EntityStore store)
|
||||||
|
{
|
||||||
|
var result = new List<(int, float, float)>();
|
||||||
|
foreach (var entity in store.Entities)
|
||||||
|
{
|
||||||
|
if (result.Count == 5)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.HasComponent<NetId>() && entity.HasComponent<Transform2D>())
|
||||||
|
{
|
||||||
|
var position = entity.GetComponent<Transform2D>().Position;
|
||||||
|
result.Add((entity.GetComponent<NetId>().Value, position.X, position.Y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Router AppAssembly="@typeof(App).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<PageTitle>Not found</PageTitle>
|
||||||
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
|
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
#----------------------------- Global Properties ----------------------------#
|
||||||
|
|
||||||
|
/outputDir:bin/$(Platform)
|
||||||
|
/intermediateDir:obj/$(Platform)
|
||||||
|
/platform:BlazorGL
|
||||||
|
/config:
|
||||||
|
/profile:Reach
|
||||||
|
/compress:True
|
||||||
|
|
||||||
|
#-------------------------------- References --------------------------------#
|
||||||
|
|
||||||
|
|
||||||
|
#---------------------------------- Content ---------------------------------#
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<Project></Project>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
<PropertyGroup>
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<RootNamespace>LittleSim.Web</RootNamespace>
|
||||||
|
<AssemblyName>LittleSim.Web</AssemblyName>
|
||||||
|
<DefineConstants>$(DefineConstants);BLAZORGL</DefineConstants>
|
||||||
|
<KniPlatform>BlazorGL</KniPlatform>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Pages\Index.razor.cs" />
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="LittleSimWebGame.cs" />
|
||||||
|
<Compile Include="NetContract.cs" />
|
||||||
|
<Compile Include="WorldViewScene.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!--
|
||||||
|
Веб-клиент собирается против KNI (форк MonoGame с платформой Blazor/WebGL), поэтому
|
||||||
|
ссылается только на платформо-независимые библиотеки движка: Core и Net.
|
||||||
|
Графические библиотеки движка (DesktopGL) сюда подключать нельзя — у KNI свои
|
||||||
|
сборки с теми же неймспейсами.
|
||||||
|
-->
|
||||||
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Net\MrGameEng.Net.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="nkast.Xna.Framework" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Content" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Graphics" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Audio" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Media" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Input" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Game" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Devices" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Storage" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.XR" Version="4.2.9001" />
|
||||||
|
<PackageReference Include="nkast.Kni.Platform.Blazor.GL" Version="4.2.9001.2" />
|
||||||
|
<PackageReference Include="nkast.Xna.Framework.Content.Pipeline.Builder" Version="4.2.9001" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.17" />
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer"
|
||||||
|
Version="8.0.17"
|
||||||
|
PrivateAssets="all"
|
||||||
|
/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<KniContentReference Include="Content\LittleSimWebContent.mgcb" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
|
||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Веб-хост LittleSim поверх KNI (BlazorGL/WebGL) — браузерный аналог
|
||||||
|
/// MrGameEng.Host.GameHost: владеет EngineContext ядра, гонит GameClock и фазы сцены.
|
||||||
|
/// Цикл тикается из requestAnimationFrame (см. Pages/Index.razor.cs). Когда графика
|
||||||
|
/// движка научится собираться под KNI, этот класс переедет в MrGameEng.Host.Web.
|
||||||
|
/// </summary>
|
||||||
|
public class LittleSimWebGame : Game
|
||||||
|
{
|
||||||
|
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
|
||||||
|
public EngineContext Context { get; } = new EngineContext();
|
||||||
|
|
||||||
|
private readonly Uri _server;
|
||||||
|
private GraphicsDeviceManager _graphics;
|
||||||
|
private SpriteBatch _spriteBatch = null!;
|
||||||
|
private Texture2D _pixel = null!;
|
||||||
|
|
||||||
|
/// <summary>Игра, подключающаяся к серверу <paramref name="server"/>.</summary>
|
||||||
|
public LittleSimWebGame(Uri server)
|
||||||
|
{
|
||||||
|
_server = server;
|
||||||
|
_graphics = new GraphicsDeviceManager(this);
|
||||||
|
Content.RootDirectory = "Content";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
Context.Scenes.Switch(new WorldViewScene(_server, () => _spriteBatch, () => _pixel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void LoadContent()
|
||||||
|
{
|
||||||
|
_spriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
_pixel = new Texture2D(GraphicsDevice, 1, 1);
|
||||||
|
_pixel.SetData(new[] { Color.White });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Update(GameTime gameTime)
|
||||||
|
{
|
||||||
|
Context.Clock.Advance((float)gameTime.ElapsedGameTime.TotalSeconds);
|
||||||
|
Context.Scenes.Update(Context.Clock);
|
||||||
|
base.Update(gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
GraphicsDevice.Clear(new Color(12, 16, 24));
|
||||||
|
Context.Scenes.Draw(Context.Clock);
|
||||||
|
base.Draw(gameTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<main>
|
||||||
|
@Body
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
.page
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
{
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar
|
||||||
|
{
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row
|
||||||
|
{
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link
|
||||||
|
{
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover
|
||||||
|
{
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child
|
||||||
|
{
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px)
|
||||||
|
{
|
||||||
|
.top-row:not(.auth)
|
||||||
|
{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth
|
||||||
|
{
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link
|
||||||
|
{
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px)
|
||||||
|
{
|
||||||
|
.page
|
||||||
|
{
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar
|
||||||
|
{
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row
|
||||||
|
{
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child
|
||||||
|
{
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article
|
||||||
|
{
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
// ВНИМАНИЕ: бинарное зеркало сетевого контракта десктопа (src/LittleSim/Net/NetSchema.cs).
|
||||||
|
// Веб-клиент не может ссылаться на LittleSim/MrGameEng.Graphics (они собраны против
|
||||||
|
// MonoGame DesktopGL, а тут KNI), поэтому реплицируемые компоненты продублированы
|
||||||
|
// со СТРОГО тем же лейаутом и порядком регистрации. Меняешь схему там — меняй здесь.
|
||||||
|
// Уйдёт после пер-платформенной сборки графических библиотек (см. docs/web-client.md).
|
||||||
|
|
||||||
|
/// <summary>Зеркало MrGameEng.Graphics.Transform2D: Position(8) + Rotation(4) + Scale(8).</summary>
|
||||||
|
public struct NetTransform : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Позиция в мировых координатах.</summary>
|
||||||
|
public Vector2 Position;
|
||||||
|
|
||||||
|
/// <summary>Поворот в радианах.</summary>
|
||||||
|
public float Rotation;
|
||||||
|
|
||||||
|
/// <summary>Масштаб (у жителей — размер квада в пикселях).</summary>
|
||||||
|
public Vector2 Scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Зеркало LittleSim.Sim.PawnNeeds: Energy(4).</summary>
|
||||||
|
public struct NetPawnNeeds : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Запас сил жителя в [0, 1] — затемняет спрайт.</summary>
|
||||||
|
public float Energy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Схема репликации веб-клиента — порядок тот же, что в NetSchema десктопа.</summary>
|
||||||
|
public static class WebNetSchema
|
||||||
|
{
|
||||||
|
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
|
||||||
|
public const int DefaultPort = 9050;
|
||||||
|
|
||||||
|
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
|
||||||
|
public static ReplicationSchema Create() =>
|
||||||
|
new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
@page "/"
|
||||||
|
@page "/index.html"
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
@using nkast.Wasm.Canvas
|
||||||
|
|
||||||
|
<PageTitle>LittleSim</PageTitle>
|
||||||
|
|
||||||
|
<div id="canvasHolder" style="
|
||||||
|
background: #000;
|
||||||
|
margin:0%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
width:100vw;
|
||||||
|
height:100vh;
|
||||||
|
">
|
||||||
|
<canvas id="theCanvas" style="touch-action:none;"></canvas>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
namespace LittleSim.Web.Pages
|
||||||
|
{
|
||||||
|
public partial class Index
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
private NavigationManager Navigation { get; set; } = null!;
|
||||||
|
|
||||||
|
private Game? _game;
|
||||||
|
|
||||||
|
protected override void OnAfterRender(bool firstRender)
|
||||||
|
{
|
||||||
|
base.OnAfterRender(firstRender);
|
||||||
|
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void TickDotNet()
|
||||||
|
{
|
||||||
|
if (_game == null)
|
||||||
|
{
|
||||||
|
_game = new LittleSimWebGame(ResolveServerUri());
|
||||||
|
_game.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
_game.Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Адрес сервера: ?server=ws://host:port в URL страницы; по умолчанию —
|
||||||
|
// хост самой страницы на порту LittleSim.Server.
|
||||||
|
private Uri ResolveServerUri()
|
||||||
|
{
|
||||||
|
var page = new Uri(Navigation.Uri);
|
||||||
|
var query = page.Query.TrimStart('?');
|
||||||
|
foreach (var pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var separator = pair.IndexOf('=');
|
||||||
|
if (separator > 0 && pair[..separator] == "server")
|
||||||
|
{
|
||||||
|
return new Uri(Uri.UnescapeDataString(pair[(separator + 1)..]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UriBuilder
|
||||||
|
{
|
||||||
|
Scheme = "ws",
|
||||||
|
Host = page.Host,
|
||||||
|
Port = WebNetSchema.DefaultPort,
|
||||||
|
}.Uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace LittleSim.Web
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient()
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress),
|
||||||
|
});
|
||||||
|
await builder.Build().RunAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:56897",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"KniWebSpike": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "http://localhost:5259",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Браузерный клиент мира LittleSim: подключается к дедикейтед-серверу
|
||||||
|
/// (LittleSim.Server --listen), применяет дельта-снапшоты в свой EntityStore и рисует
|
||||||
|
/// жителей через KNI SpriteBatch (WebGL). Симуляция целиком на сервере — сюда приезжают
|
||||||
|
/// только компоненты схемы (позиция + потребности); позиции сглаживаются до частоты
|
||||||
|
/// кадра, усталость затемняет квадратик жителя.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldViewScene : Scene
|
||||||
|
{
|
||||||
|
private readonly Uri _server;
|
||||||
|
private readonly Func<SpriteBatch> _spriteBatch;
|
||||||
|
private readonly Func<Texture2D> _pixel;
|
||||||
|
|
||||||
|
private ReplicationClient _replication = null!;
|
||||||
|
private Task<WebSocketClient>? _connecting;
|
||||||
|
private WebSocketClient? _connection;
|
||||||
|
|
||||||
|
/// <summary>Сцена, подключающаяся к <paramref name="server"/>.</summary>
|
||||||
|
public WorldViewScene(Uri server, Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||||
|
{
|
||||||
|
_server = server;
|
||||||
|
_spriteBatch = spriteBatch;
|
||||||
|
_pixel = pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
|
||||||
|
private struct NetLerp : IComponent
|
||||||
|
{
|
||||||
|
public Vector2 Visual;
|
||||||
|
public Vector2 Target;
|
||||||
|
public bool Initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
_replication = new ReplicationClient(WebNetSchema.Create(), Store);
|
||||||
|
_replication.EntitySpawned += entity => entity.AddComponent(new NetLerp());
|
||||||
|
|
||||||
|
UpdateSystems.Add(new CallbackSystem(Pump));
|
||||||
|
UpdateSystems.Add(new NetSmoothingSystem());
|
||||||
|
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
|
||||||
|
|
||||||
|
Log.Info($"LittleSim.Web: connecting to {_server}…");
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
|
private void Pump()
|
||||||
|
{
|
||||||
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
|
{
|
||||||
|
_connecting = null;
|
||||||
|
if (finished.IsFaulted)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
$"Connect to {_server} failed: "
|
||||||
|
+ finished.Exception?.GetBaseException().Message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connection is not null)
|
||||||
|
{
|
||||||
|
_replication.Pump(_connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
|
||||||
|
private sealed class CallbackSystem(Action update) : BaseSystem
|
||||||
|
{
|
||||||
|
protected override void OnUpdateGroup() => update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Та же экспонента, что в NetSmoothingSystem десктопа (LittleSim/Net/NetSmoothing.cs).</summary>
|
||||||
|
private sealed class NetSmoothingSystem : QuerySystem<NetTransform, NetLerp>
|
||||||
|
{
|
||||||
|
private const float Rate = 12f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var blend = 1f - MathF.Exp(-Rate * Tick.deltaTime);
|
||||||
|
Query.ForEachEntity(
|
||||||
|
(ref NetTransform transform, ref NetLerp lerp, Entity _) =>
|
||||||
|
{
|
||||||
|
if (!lerp.Initialized)
|
||||||
|
{
|
||||||
|
lerp.Visual = lerp.Target = transform.Position;
|
||||||
|
lerp.Initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transform.Position != lerp.Visual)
|
||||||
|
{
|
||||||
|
lerp.Target = transform.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
lerp.Visual = Vector2.Lerp(lerp.Visual, lerp.Target, blend);
|
||||||
|
transform.Position = lerp.Visual;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Житель — квадратик размером Scale, затемняющийся с усталостью (как на десктопе).</summary>
|
||||||
|
private sealed class PawnDrawSystem(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||||
|
: QuerySystem<NetTransform, NetPawnNeeds>
|
||||||
|
{
|
||||||
|
private const float MinBrightness = 0.45f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var batch = spriteBatch();
|
||||||
|
var white = pixel();
|
||||||
|
batch.Begin();
|
||||||
|
Query.ForEachEntity(
|
||||||
|
(ref NetTransform transform, ref NetPawnNeeds needs, Entity _) =>
|
||||||
|
{
|
||||||
|
var size = transform.Scale;
|
||||||
|
var brightness =
|
||||||
|
MinBrightness + (1f - MinBrightness) * Math.Clamp(needs.Energy, 0f, 1f);
|
||||||
|
batch.Draw(
|
||||||
|
white,
|
||||||
|
new Rectangle(
|
||||||
|
(int)(transform.Position.X - size.X / 2f),
|
||||||
|
(int)(transform.Position.Y - size.Y / 2f),
|
||||||
|
(int)size.X,
|
||||||
|
(int)size.Y
|
||||||
|
),
|
||||||
|
Color.White * brightness
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
batch.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using nkast.Wasm.Canvas
|
||||||
|
@using LittleSim.Web
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
|
||||||
|
html, body
|
||||||
|
{
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:focus
|
||||||
|
{
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link
|
||||||
|
{
|
||||||
|
color: #0077cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary
|
||||||
|
{
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content
|
||||||
|
{
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox])
|
||||||
|
{
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid
|
||||||
|
{
|
||||||
|
outline: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message
|
||||||
|
{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui
|
||||||
|
{
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss
|
||||||
|
{
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary
|
||||||
|
{
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after
|
||||||
|
{
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
#theCanvas
|
||||||
|
{
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
|
||||||
|
/* Disable text highlighting and magnifying glass on iPhone/webkit */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#canvas
|
||||||
|
{
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,114 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>LittleSim</title>
|
||||||
|
<base href="./" />
|
||||||
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="css/app.css" rel="stylesheet" />
|
||||||
|
<link href="LittleSim.Web.styles.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<div id="loading" style="display: table-cell; margin: auto; width:100vw; height:100vh; vertical-align: middle; background: #ffcc10;">
|
||||||
|
<div style="display: block; margin: auto; width: 9em; color: white;font-family: 'Segoe UI', sans-serif;">
|
||||||
|
<div style="text-align: center; font-size: 0.85em;">Made with<br/><a href="https://github.com/kniEngine/kni"><img src="kni.png" border="0" alt="Kni"></a></div>
|
||||||
|
<div style="text-align: center; font-size: 1.8em;">loading <marquee style="width:0.9em; vertical-align: bottom;">. . . </marquee></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">x</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { BrotliDecode } from './js/decode.min.js';
|
||||||
|
window.BrotliDecode = BrotliDecode;
|
||||||
|
// Set this to enable Brotli (.br) decompression on static webServers
|
||||||
|
// that don't support content compression and http://.
|
||||||
|
var enableBrotliDecompression = false;
|
||||||
|
Blazor.start({
|
||||||
|
loadBootResource: function (type, name, defaultUri, integrity)
|
||||||
|
{
|
||||||
|
if (enableBrotliDecompression === true && type !== 'dotnetjs' && location.hostname !== 'localhost')
|
||||||
|
{
|
||||||
|
return (async function()
|
||||||
|
{
|
||||||
|
const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
const originalResponseBuffer = await response.arrayBuffer();
|
||||||
|
const originalResponseArray = new Int8Array(originalResponseBuffer);
|
||||||
|
const contentType = (type === 'dotnetwasm')
|
||||||
|
? 'application/wasm'
|
||||||
|
: 'application/octet-stream';
|
||||||
|
const decompressedResponseArray = BrotliDecode(originalResponseArray);
|
||||||
|
return new Response(decompressedResponseArray,
|
||||||
|
{ headers: { 'content-type': contentType }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="_content/nkast.Wasm.JSInterop/js/JSObject.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Dom/js/Window.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Dom/js/Document.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Dom/js/Navigator.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Dom/js/Gamepad.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Dom/js/Media.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.XHR/js/XHR.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Canvas/js/Canvas.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Canvas/js/CanvasGLContext.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.Audio/js/Audio.8.0.11.js"></script>
|
||||||
|
<script src="_content/nkast.Wasm.XR/js/XR.8.0.11.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function tickJS()
|
||||||
|
{
|
||||||
|
window.theInstance.invokeMethod('TickDotNet');
|
||||||
|
window.requestAnimationFrame(tickJS);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.initRenderJS = (instance) =>
|
||||||
|
{
|
||||||
|
window.theInstance = instance;
|
||||||
|
|
||||||
|
// set initial canvas size
|
||||||
|
var canvas = document.getElementById('theCanvas');
|
||||||
|
var holder = document.getElementById('canvasHolder');
|
||||||
|
canvas.width = holder.clientWidth;
|
||||||
|
canvas.height = holder.clientHeight;
|
||||||
|
// disable context menu on right click
|
||||||
|
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||||
|
|
||||||
|
// begin game loop
|
||||||
|
window.requestAnimationFrame(tickJS);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", function(event)
|
||||||
|
{
|
||||||
|
// Prevent Arrows Keys and Spacebar scrolling the outer page
|
||||||
|
// when running inside an iframe. e.g: itch.io embedding.
|
||||||
|
if ([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1)
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
window.addEventListener("wheel", function(event)
|
||||||
|
{
|
||||||
|
// Prevent Mousewheel scrolling the outer page
|
||||||
|
// when running inside an iframe. e.g: itch.io embedding.
|
||||||
|
event.preventDefault();
|
||||||
|
}, { passive: false });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
|||||||
|
// micProcessor.js
|
||||||
|
class MicProcessor extends AudioWorkletProcessor
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
// global variables for testing
|
||||||
|
var sampleRate = globalThis.sampleRate;
|
||||||
|
var currentFrame = globalThis.currentFrame;
|
||||||
|
var currentTime = globalThis.currentTime;
|
||||||
|
var currentRenderQuantum = globalThis.currentRenderQuantum;
|
||||||
|
|
||||||
|
this.SampleRate = sampleRate;
|
||||||
|
this.TargetSamples = Math.floor(this.SampleRate * 0.1); // 100ms
|
||||||
|
this.Buffer = new Float32Array(this.TargetSamples);
|
||||||
|
this.BufferIndex = 0;
|
||||||
|
|
||||||
|
this.port.onmessage = (event) =>
|
||||||
|
{
|
||||||
|
var data = event.data;
|
||||||
|
|
||||||
|
if (typeof data === 'number')
|
||||||
|
{
|
||||||
|
//this.port.postMessage(data); // echo back test
|
||||||
|
}
|
||||||
|
if (data instanceof Uint8Array)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters)
|
||||||
|
{
|
||||||
|
var inChannel0 = inputs[0][0];
|
||||||
|
if (!inChannel0) return true;
|
||||||
|
|
||||||
|
let srcIndex = 0;
|
||||||
|
var srcLen = inChannel0.length;
|
||||||
|
|
||||||
|
while (srcIndex < srcLen)
|
||||||
|
{
|
||||||
|
var remaining = this.TargetSamples - this.BufferIndex;
|
||||||
|
var copyCount = Math.min(remaining, srcLen - srcIndex);
|
||||||
|
|
||||||
|
this.Buffer.set(
|
||||||
|
inChannel0.subarray(srcIndex, srcIndex + copyCount),
|
||||||
|
this.BufferIndex);
|
||||||
|
|
||||||
|
this.BufferIndex += copyCount;
|
||||||
|
srcIndex += copyCount;
|
||||||
|
|
||||||
|
if (this.BufferIndex >= this.TargetSamples)
|
||||||
|
{
|
||||||
|
this.SendBuffer();
|
||||||
|
this.BufferIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendBuffer()
|
||||||
|
{
|
||||||
|
// convert to 16-6bit PCM
|
||||||
|
var int16 = new Int16Array(this.TargetSamples);
|
||||||
|
for (var i = 0; i < this.TargetSamples; i++)
|
||||||
|
{
|
||||||
|
int16[i] = this.Buffer[i] * 32767;
|
||||||
|
}
|
||||||
|
|
||||||
|
var byteArray = new Uint8Array(int16.buffer);
|
||||||
|
this.port.postMessage(byteArray, [byteArray.buffer]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('mic-processor', MicProcessor);
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// streamProcessor.js
|
||||||
|
class StreamProcessor extends AudioWorkletProcessor
|
||||||
|
{
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.queue = [];
|
||||||
|
|
||||||
|
this.port.onmessage = (event) =>
|
||||||
|
{
|
||||||
|
var data = event.data;
|
||||||
|
|
||||||
|
if (typeof data === 'number')
|
||||||
|
{
|
||||||
|
if (data === 2)
|
||||||
|
{
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data instanceof Uint8Array)
|
||||||
|
{
|
||||||
|
const buffer = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
|
||||||
|
buffer.offset = 0;
|
||||||
|
this.queue.push(buffer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters)
|
||||||
|
{
|
||||||
|
const output = outputs[0];
|
||||||
|
|
||||||
|
const channelCount = output.length;
|
||||||
|
const sampleCount = output[0].length;
|
||||||
|
|
||||||
|
let written = 0;
|
||||||
|
|
||||||
|
while (written < sampleCount && this.queue.length > 0)
|
||||||
|
{
|
||||||
|
const buffer = this.queue[0];
|
||||||
|
const offset = buffer.offset;
|
||||||
|
|
||||||
|
const available = buffer.length - offset;
|
||||||
|
const needed = sampleCount - written;
|
||||||
|
const copyCount = Math.min(available, needed);
|
||||||
|
|
||||||
|
for (let i = 0; i < copyCount; i++)
|
||||||
|
{
|
||||||
|
for (let c = 0; c < channelCount; c++)
|
||||||
|
{
|
||||||
|
const channel = output[c];
|
||||||
|
let value = (buffer[offset+i] / 32767);
|
||||||
|
channel[written+i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
written += copyCount;
|
||||||
|
buffer.offset += copyCount;
|
||||||
|
|
||||||
|
if (buffer.offset >= buffer.length)
|
||||||
|
{
|
||||||
|
this.queue.shift();
|
||||||
|
this.port.postMessage(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill remaining samples with silence
|
||||||
|
if (written < sampleCount)
|
||||||
|
{
|
||||||
|
for (let c = 0; c < channelCount; c++)
|
||||||
|
{
|
||||||
|
const channel = output[c];
|
||||||
|
|
||||||
|
for (let i = written; i < sampleCount; i++)
|
||||||
|
{
|
||||||
|
let value = 0;
|
||||||
|
channel[i] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor("stream-processor", StreamProcessor);
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 423 B |
@@ -0,0 +1,26 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Расположение пользовательских данных игры рядом с папкой <c>Mods</c> (где живёт и
|
||||||
|
/// <c>Cache/</c>): файл настроек и каталог сохранений. Корень ищется один раз — поднятием
|
||||||
|
/// вверх по дереву от исполняемого файла (как делает загрузка модов).
|
||||||
|
/// </summary>
|
||||||
|
public static class GamePaths
|
||||||
|
{
|
||||||
|
private static string? _root;
|
||||||
|
|
||||||
|
/// <summary>Корневая папка данных (родитель папки <c>Mods</c>).</summary>
|
||||||
|
public static string Root =>
|
||||||
|
_root ??= Path.GetDirectoryName(
|
||||||
|
ModLoader.FindModsRoot(AppContext.BaseDirectory)
|
||||||
|
?? throw new DirectoryNotFoundException("Папка Mods не найдена.")
|
||||||
|
)!;
|
||||||
|
|
||||||
|
/// <summary>Путь к файлу настроек игры.</summary>
|
||||||
|
public static string SettingsFile => Path.Combine(Root, "settings.json");
|
||||||
|
|
||||||
|
/// <summary>Каталог файлов сохранений.</summary>
|
||||||
|
public static string SavesDirectory => Path.Combine(Root, "Saves");
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Audio;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пользовательские настройки игры, сохраняемые в <c>settings.json</c>: язык интерфейса,
|
||||||
|
/// полноэкранный режим, вертикальная синхронизация, размер окна и общая громкость.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GameSettings
|
||||||
|
{
|
||||||
|
/// <summary>Код языка интерфейса (например, "ru" или "en").</summary>
|
||||||
|
public string Language { get; set; } = "ru";
|
||||||
|
|
||||||
|
/// <summary>Полноэкранный (безрамочный) режим.</summary>
|
||||||
|
public bool Fullscreen { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Вертикальная синхронизация.</summary>
|
||||||
|
public bool VSync { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Ширина окна в пикселях.</summary>
|
||||||
|
public int Width { get; set; } = 1280;
|
||||||
|
|
||||||
|
/// <summary>Высота окна в пикселях.</summary>
|
||||||
|
public int Height { get; set; } = 720;
|
||||||
|
|
||||||
|
/// <summary>Общая громкость 0..1.</summary>
|
||||||
|
public float Volume { get; set; } = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||||
|
public static class GameSettingsStore
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary>
|
||||||
|
public static GameSettings Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(GamePaths.SettingsFile))
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<GameSettings>(
|
||||||
|
File.ReadAllText(GamePaths.SettingsFile),
|
||||||
|
JsonOptions
|
||||||
|
) ?? new GameSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or JsonException)
|
||||||
|
{
|
||||||
|
// Битый файл настроек не должен ронять запуск — откатываемся к умолчаниям.
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GameSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сохраняет настройки в <see cref="GamePaths.SettingsFile"/>.</summary>
|
||||||
|
public static void Save(GameSettings settings)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(GamePaths.Root);
|
||||||
|
File.WriteAllText(GamePaths.SettingsFile, JsonSerializer.Serialize(settings, JsonOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Применяет настройки к движку: язык (<paramref name="languages"/>), полноэкранный
|
||||||
|
/// режим/VSync/размер окна (<paramref name="graphics"/>) и громкость (<paramref name="audio"/>).
|
||||||
|
/// </summary>
|
||||||
|
public static void Apply(
|
||||||
|
GameSettings settings,
|
||||||
|
GraphicsDeviceManager graphics,
|
||||||
|
LanguageManager languages,
|
||||||
|
AudioManager audio
|
||||||
|
)
|
||||||
|
{
|
||||||
|
languages.SetLanguage(settings.Language);
|
||||||
|
audio.MasterVolume = settings.Volume;
|
||||||
|
|
||||||
|
graphics.IsFullScreen = settings.Fullscreen;
|
||||||
|
graphics.SynchronizeWithVerticalRetrace = settings.VSync;
|
||||||
|
graphics.PreferredBackBufferWidth = settings.Width;
|
||||||
|
graphics.PreferredBackBufferHeight = settings.Height;
|
||||||
|
graphics.ApplyChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Параметры одного мира: имя, размер сетки, сид, число проходов сглаживания рельефа и
|
||||||
|
/// население. Заменяет хардкод в <see cref="Scenes.WorldScene"/>; строится из выбранного
|
||||||
|
/// <see cref="Content.WorldPresetDef"/> и пользовательского ввода в меню «Новый мир» либо
|
||||||
|
/// восстанавливается из <see cref="WorldSave"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record WorldConfig
|
||||||
|
{
|
||||||
|
/// <summary>Отображаемое имя мира (и имя файла сохранения).</summary>
|
||||||
|
public string Name { get; init; } = "World";
|
||||||
|
|
||||||
|
/// <summary>Ширина мира в клетках.</summary>
|
||||||
|
public int Width { get; init; } = 120;
|
||||||
|
|
||||||
|
/// <summary>Высота мира в клетках.</summary>
|
||||||
|
public int Height { get; init; } = 70;
|
||||||
|
|
||||||
|
/// <summary>Целочисленный сид — весь мир воспроизводится из него.</summary>
|
||||||
|
public int Seed { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Число проходов сглаживания рельефа (больше — крупнее «континенты»).</summary>
|
||||||
|
public int SmoothPasses { get; init; } = 4;
|
||||||
|
|
||||||
|
/// <summary>Число жителей при генерации.</summary>
|
||||||
|
public int Population { get; init; } = 80;
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
|
||||||
|
public sealed class PlantSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
|
||||||
|
public string Species { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||||
|
public float AgeDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Текущая стадия роста.</summary>
|
||||||
|
public int Stage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Плодородность клетки.</summary>
|
||||||
|
public float CellFertility { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
|
||||||
|
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
|
||||||
|
public sealed class PawnSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа жителя — по нему при загрузке берётся текстура из атласов.</summary>
|
||||||
|
public string DefName { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Масштаб трансформа (зависит от размера дефа и клетки).</summary>
|
||||||
|
public float Scale { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Запас энергии [0, 1].</summary>
|
||||||
|
public float Energy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Текущее действие (значение <see cref="Sim.PawnAction"/>).</summary>
|
||||||
|
public int Action { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
||||||
|
public float DecideIn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Направление блуждания X.</summary>
|
||||||
|
public float DirX { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Направление блуждания Y.</summary>
|
||||||
|
public float DirY { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Секунды до смены направления.</summary>
|
||||||
|
public float ChangeIn { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||||
|
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||||
|
/// детерминированно регенерируется из сида при загрузке.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя мира (и основа имени файла).</summary>
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Ширина мира в клетках.</summary>
|
||||||
|
public int Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Высота мира в клетках.</summary>
|
||||||
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Сид генерации.</summary>
|
||||||
|
public int Seed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Число проходов сглаживания рельефа.</summary>
|
||||||
|
public int SmoothPasses { get; set; } = 4;
|
||||||
|
|
||||||
|
/// <summary>Население на момент сохранения.</summary>
|
||||||
|
public int Population { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Момент сохранения (UTC).</summary>
|
||||||
|
public DateTime SavedUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Прошедшее время симуляции в секундах.</summary>
|
||||||
|
public double ElapsedSeconds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Снимок жителей.</summary>
|
||||||
|
public List<PawnSave> Pawns { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||||
|
public List<PlantSave> Plants { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||||
|
public WorldConfig ToConfig() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Width = Width,
|
||||||
|
Height = Height,
|
||||||
|
Seed = Seed,
|
||||||
|
SmoothPasses = SmoothPasses,
|
||||||
|
Population = Population,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
|
||||||
|
public sealed class SaveStore
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly string _directory;
|
||||||
|
|
||||||
|
/// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
|
||||||
|
public SaveStore(string? directory = null) =>
|
||||||
|
_directory = directory ?? GamePaths.SavesDirectory;
|
||||||
|
|
||||||
|
/// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
|
||||||
|
public readonly record struct Entry(string FileName, WorldSave Save);
|
||||||
|
|
||||||
|
/// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
|
||||||
|
public IReadOnlyList<Entry> List()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_directory))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new List<Entry>();
|
||||||
|
foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var save = JsonSerializer.Deserialize<WorldSave>(
|
||||||
|
File.ReadAllText(file),
|
||||||
|
JsonOptions
|
||||||
|
);
|
||||||
|
if (save is not null)
|
||||||
|
{
|
||||||
|
entries.Add(new Entry(Path.GetFileName(file), save));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or JsonException)
|
||||||
|
{
|
||||||
|
// Битый файл сохранения не должен ломать список — пропускаем.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
|
||||||
|
public string Write(WorldSave save)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_directory);
|
||||||
|
var fileName = Sanitize(save.Name) + ".json";
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(_directory, fileName),
|
||||||
|
JsonSerializer.Serialize(save, JsonOptions)
|
||||||
|
);
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Удаляет файл сохранения по имени.</summary>
|
||||||
|
public void Delete(string fileName)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_directory, fileName);
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Sanitize(string name)
|
||||||
|
{
|
||||||
|
var trimmed = name.Trim();
|
||||||
|
var safe = string.Concat(
|
||||||
|
trimmed.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)
|
||||||
|
);
|
||||||
|
return string.IsNullOrEmpty(safe) ? "world" : safe;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using MrGameEng.Atlases;
|
using MrGameEng.Atlases;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
using MrGameEng.Mods;
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
namespace LittleSim.Content;
|
namespace LittleSim.Content;
|
||||||
@@ -44,8 +45,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)
|
||||||
@@ -56,8 +59,19 @@ public sealed class GameContent
|
|||||||
|
|
||||||
var defs = new DefDatabase();
|
var defs = new DefDatabase();
|
||||||
defs.RegisterType<TerrainDef>("Terrain");
|
defs.RegisterType<TerrainDef>("Terrain");
|
||||||
|
defs.RegisterType<GeneDef>("Gene");
|
||||||
|
defs.RegisterType<ProductDef>("Product");
|
||||||
defs.RegisterType<PlantDef>("Plant");
|
defs.RegisterType<PlantDef>("Plant");
|
||||||
defs.RegisterType<PawnDef>("Pawn");
|
defs.RegisterType<PawnDef>("Pawn");
|
||||||
|
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||||
|
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||||
|
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
||||||
|
defs.RegisterValidator(
|
||||||
|
"Product",
|
||||||
|
"defName",
|
||||||
|
"^Product",
|
||||||
|
"product defName must start with 'Product'"
|
||||||
|
);
|
||||||
defs.Load(mods);
|
defs.Load(mods);
|
||||||
|
|
||||||
var languages = new LanguageManager(defaultLanguage: "ru");
|
var languages = new LanguageManager(defaultLanguage: "ru");
|
||||||
@@ -70,19 +84,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,
|
||||||
|
|||||||
@@ -27,27 +27,161 @@ public sealed class TerrainDef : Def
|
|||||||
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
|
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
|
||||||
public bool IsLand { get; init; }
|
public bool IsLand { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.</summary>
|
||||||
|
public bool BlocksLight { get; init; }
|
||||||
|
|
||||||
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
|
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
|
||||||
public string? Surface { get; init; }
|
public string? Surface { get; init; }
|
||||||
|
|
||||||
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
|
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
|
||||||
public List<ScatterEntry> Scatter { get; init; } = [];
|
public List<ScatterEntry> Scatter { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
|
||||||
|
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
|
||||||
|
/// </summary>
|
||||||
|
public float Fertility { get; init; } = 1f;
|
||||||
|
|
||||||
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
|
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
|
||||||
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
|
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие.</summary>
|
/// <summary>
|
||||||
|
/// Одна стадия роста растения (вложенный объект <see cref="PlantDef.Stages"/>): свой спрайт и
|
||||||
|
/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
|
||||||
|
/// Растёт на месте — последняя стадия терминальная (её <see cref="GrowDays"/> игнорируется).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlantGrowthStage
|
||||||
|
{
|
||||||
|
/// <summary>Ключ текстуры стадии; null — берётся базовая <see cref="PlantDef.Texture"/>.</summary>
|
||||||
|
public string? Texture { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Размер спрайта в клетках на этой стадии; 0 — берётся базовый <see cref="PlantDef.SizeCells"/>.</summary>
|
||||||
|
public float SizeCells { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней расти на этой стадии до перехода к следующей.</summary>
|
||||||
|
public float GrowDays { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Ключ локализации названия стадии (опционально, для будущих тултипов).</summary>
|
||||||
|
public string? Label { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||||
|
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||||
|
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
|
||||||
|
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
|
||||||
|
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GenomeDef
|
||||||
|
{
|
||||||
|
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
|
||||||
|
public float OptimalLight { get; init; } = 0.7f;
|
||||||
|
|
||||||
|
/// <summary>Ширина толерантности по свету.</summary>
|
||||||
|
public float LightTolerance { get; init; } = 0.4f;
|
||||||
|
|
||||||
|
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
||||||
|
public float OptimalTemperature { get; init; } = 18f;
|
||||||
|
|
||||||
|
/// <summary>Ширина толерантности по температуре (°C).</summary>
|
||||||
|
public float TemperatureTolerance { get; init; } = 12f;
|
||||||
|
|
||||||
|
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
||||||
|
public float OptimalFertility { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Ширина толерантности по плодородности.</summary>
|
||||||
|
public float FertilityTolerance { get; init; } = 0.8f;
|
||||||
|
|
||||||
|
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
|
||||||
|
public float Vigor { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
||||||
|
public float Lifespan { get; init; } = 80f;
|
||||||
|
|
||||||
|
/// <summary>Дальность расселения семян (в клетках).</summary>
|
||||||
|
public float DispersalRange { get; init; } = 2f;
|
||||||
|
|
||||||
|
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
|
||||||
|
public float ReproduceInterval { get; init; } = 8f;
|
||||||
|
|
||||||
|
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
|
||||||
|
public float SelfPollination { get; init; } = 0.4f;
|
||||||
|
|
||||||
|
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
|
||||||
|
public float MutationRate { get; init; } = 0.05f;
|
||||||
|
|
||||||
|
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
|
||||||
|
public float VariantChance { get; init; } = 0.15f;
|
||||||
|
|
||||||
|
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||||
|
public float Spread { get; init; } = 0.08f;
|
||||||
|
|
||||||
|
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
|
||||||
|
public float FruitYield { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
|
||||||
|
public float FruitSeason { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
|
||||||
|
public float HarvestAmount { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||||
|
public float LeafHue { get; init; } = 0.33f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||||
public sealed class PlantDef : Def
|
public sealed class PlantDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
|
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
||||||
public string Texture { get; init; } = "";
|
public string Texture { get; init; } = "";
|
||||||
|
|
||||||
/// <summary>Размер спрайта в клетках.</summary>
|
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
|
||||||
public float SizeCells { get; init; } = 1f;
|
public float SizeCells { get; init; } = 1f;
|
||||||
|
|
||||||
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
|
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
|
||||||
public float TrunkRadiusCells { get; init; }
|
public float TrunkRadiusCells { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
|
||||||
|
public List<PlantGrowthStage> Stages { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
|
||||||
|
public GenomeDef Genome { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
|
||||||
|
public string? HarvestProduct { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||||
|
public string? FruitProduct { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пресет размера мира (Defs/worldpresets.json): габариты сетки и население. Меню «Новый
|
||||||
|
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
||||||
|
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldPresetDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Ширина мира в клетках.</summary>
|
||||||
|
public int Width { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Высота мира в клетках.</summary>
|
||||||
|
public int Height { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Число жителей при генерации.</summary>
|
||||||
|
public int Population { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Порядок в списке UI (по возрастанию).</summary>
|
||||||
|
public int Order { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Существо (Defs/pawns.json): текстура, размер и вид (житель/животное).</summary>
|
/// <summary>Существо (Defs/pawns.json): текстура, размер и вид (житель/животное).</summary>
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Готовая (resolved) таблица растений для сцены: по каждому <see cref="PlantDef"/> — массив
|
||||||
|
/// стадий с уже загруженными регионами атласа, размерами и порогами входа в днях. Строится один
|
||||||
|
/// раз при загрузке сцены (как <see cref="MrGameEng.Tilemaps.TileSet"/>, потому что нужны атлас и
|
||||||
|
/// устройство). <see cref="Sim.PlantGrowthSystem"/> индексируется в неё без словарей и аллокаций.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlantSet
|
||||||
|
{
|
||||||
|
/// <summary>Разрешённая стадия: спрайт, размер в клетках и день, с которого стадия активна.</summary>
|
||||||
|
public readonly record struct Stage(Texture2DRegion Region, float SizeCells, float EnterDay);
|
||||||
|
|
||||||
|
/// <summary>Вид растения: его деф и стадии роста по возрастанию <see cref="Stage.EnterDay"/>.</summary>
|
||||||
|
public sealed class Species
|
||||||
|
{
|
||||||
|
/// <summary>Исходный деф растения.</summary>
|
||||||
|
public required PlantDef Def { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Стадии роста по порядку; последняя — терминальная.</summary>
|
||||||
|
public required Stage[] Stages { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Набор генов вида: базовые значения для генерации особи (геном из общих <see cref="GeneDef"/>).</summary>
|
||||||
|
public required GenomeTemplate Template { get; init; }
|
||||||
|
|
||||||
|
/// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary>
|
||||||
|
public float MaturityDays => Stages[^1].EnterDay;
|
||||||
|
|
||||||
|
/// <summary>Индекс стадии для данного возраста в днях.</summary>
|
||||||
|
public int StageAt(float ageDays)
|
||||||
|
{
|
||||||
|
var stage = 0;
|
||||||
|
while (stage + 1 < Stages.Length && ageDays >= Stages[stage + 1].EnterDay)
|
||||||
|
{
|
||||||
|
stage++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Species[] _species;
|
||||||
|
private readonly Dictionary<PlantDef, int> _index = new();
|
||||||
|
|
||||||
|
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков растений.</summary>
|
||||||
|
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
|
||||||
|
|
||||||
|
/// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary>
|
||||||
|
public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device)
|
||||||
|
{
|
||||||
|
var genes = content
|
||||||
|
.Defs.All<GeneDef>()
|
||||||
|
.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||||||
|
GeneRegistry = genes;
|
||||||
|
|
||||||
|
var defs = content.Defs.All<PlantDef>().ToArray();
|
||||||
|
_species = new Species[defs.Length];
|
||||||
|
for (var i = 0; i < defs.Length; i++)
|
||||||
|
{
|
||||||
|
var def = defs[i];
|
||||||
|
_species[i] = new Species
|
||||||
|
{
|
||||||
|
Def = def,
|
||||||
|
Stages = BuildStages(def, atlases, device),
|
||||||
|
Template = BuildTemplate(def.Genome, genes),
|
||||||
|
};
|
||||||
|
_index[def] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Число видов в таблице.</summary>
|
||||||
|
public int Count => _species.Length;
|
||||||
|
|
||||||
|
/// <summary>Вид по индексу.</summary>
|
||||||
|
public Species this[int index] => _species[index];
|
||||||
|
|
||||||
|
/// <summary>Индекс вида по дефу.</summary>
|
||||||
|
public int IndexOf(PlantDef def) => _index[def];
|
||||||
|
|
||||||
|
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр
|
||||||
|
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
|
||||||
|
private static GenomeTemplate BuildTemplate(
|
||||||
|
GenomeDef g,
|
||||||
|
IReadOnlyDictionary<string, GeneDef> genes
|
||||||
|
)
|
||||||
|
{
|
||||||
|
GeneDef Gene(string id) =>
|
||||||
|
genes.TryGetValue(id, out var def)
|
||||||
|
? def
|
||||||
|
: throw new KeyNotFoundException(
|
||||||
|
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?"
|
||||||
|
);
|
||||||
|
|
||||||
|
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
|
||||||
|
new(Gene(id), baseValue, g.Spread);
|
||||||
|
|
||||||
|
return new GenomeTemplate([
|
||||||
|
Numeric("GeneOptimalLight", g.OptimalLight),
|
||||||
|
Numeric("GeneLightTolerance", g.LightTolerance),
|
||||||
|
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
|
||||||
|
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
|
||||||
|
Numeric("GeneOptimalFertility", g.OptimalFertility),
|
||||||
|
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
|
||||||
|
Numeric("GeneVigor", g.Vigor),
|
||||||
|
Numeric("GeneLifespan", g.Lifespan),
|
||||||
|
Numeric("GeneDispersalRange", g.DispersalRange),
|
||||||
|
Numeric("GeneReproduceInterval", g.ReproduceInterval),
|
||||||
|
Numeric("GeneSelfPollination", g.SelfPollination),
|
||||||
|
Numeric("GeneMutationRate", g.MutationRate),
|
||||||
|
// Контент (G4): плодоношение, добыча, цвет.
|
||||||
|
Numeric("GeneFruitYield", g.FruitYield),
|
||||||
|
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
|
||||||
|
Numeric("GeneHarvestAmount", g.HarvestAmount),
|
||||||
|
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
|
||||||
|
new GenomeTemplate.Entry(
|
||||||
|
Gene("GeneMorph"),
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
[1f - g.VariantChance, g.VariantChance]
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device)
|
||||||
|
{
|
||||||
|
// Без явных стадий растение существует как одна терминальная стадия из базовой текстуры.
|
||||||
|
if (def.Stages.Count == 0)
|
||||||
|
{
|
||||||
|
var region = atlases.GetRegion(device, def.Texture);
|
||||||
|
return [new Stage(region, def.SizeCells, 0f)];
|
||||||
|
}
|
||||||
|
|
||||||
|
var stages = new Stage[def.Stages.Count];
|
||||||
|
var enterDay = 0f;
|
||||||
|
for (var i = 0; i < def.Stages.Count; i++)
|
||||||
|
{
|
||||||
|
var stage = def.Stages[i];
|
||||||
|
var region = atlases.GetRegion(device, stage.Texture ?? def.Texture);
|
||||||
|
var size = stage.SizeCells > 0f ? stage.SizeCells : def.SizeCells;
|
||||||
|
stages[i] = new Stage(region, size, enterDay);
|
||||||
|
enterDay += stage.GrowDays; // следующая стадия входит после GrowDays этой
|
||||||
|
}
|
||||||
|
|
||||||
|
return stages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,18 +6,13 @@
|
|||||||
|
|
||||||
<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.Input\MrGameEng.Input.csproj" />
|
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Content\MrGameEng.Content.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Atlases\MrGameEng.Atlases.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Simulation\MrGameEng.Simulation.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Tilemaps\MrGameEng.Tilemaps.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Net\MrGameEng.Net.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj" />
|
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.AI\MrGameEng.AI.csproj" />
|
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Collisions\MrGameEng.Collisions.csproj" />
|
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.UI\MrGameEng.UI.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.UI\MrGameEng.UI.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
|
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Mods\MrGameEng.Mods.csproj" />
|
|
||||||
<ProjectReference
|
<ProjectReference
|
||||||
Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
|
Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
|
||||||
OutputItemType="Analyzer"
|
OutputItemType="Analyzer"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using LittleSim.Sim;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
namespace LittleSim.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сетевой контракт LittleSim: какие компоненты реплицируются с сервера на клиентов.
|
||||||
|
/// Сервер (LittleSim.Server) и клиент (<see cref="Scenes.MultiplayerScene"/>) обязаны
|
||||||
|
/// строить схему одинаково — порядок регистрации определяет wire-id компонентов.
|
||||||
|
/// ВНИМАНИЕ: у веб-клиента бинарное зеркало этой схемы
|
||||||
|
/// (src/LittleSim.Web/NetContract.cs — он собран против KNI и не может ссылаться сюда);
|
||||||
|
/// меняешь состав или порядок — меняй и там.
|
||||||
|
/// </summary>
|
||||||
|
public static class NetSchema
|
||||||
|
{
|
||||||
|
/// <summary>Порт сервера по умолчанию.</summary>
|
||||||
|
public const int DefaultPort = 9050;
|
||||||
|
|
||||||
|
/// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary>
|
||||||
|
public static ReplicationSchema Create() =>
|
||||||
|
new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
|
||||||
|
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
|
||||||
|
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
|
||||||
|
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
|
||||||
|
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
|
||||||
|
/// </summary>
|
||||||
|
public struct NetLerp : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
|
||||||
|
public Vector2 Visual;
|
||||||
|
|
||||||
|
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
|
||||||
|
public Vector2 Target;
|
||||||
|
|
||||||
|
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
|
||||||
|
public bool Initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
||||||
|
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NetSmoothingSystem : QuerySystem<Transform2D, NetLerp>
|
||||||
|
{
|
||||||
|
// Скорость экспоненциального сглаживания: за ~0.25 с визуал почти догоняет цель.
|
||||||
|
private const float Rate = 12f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var blend = 1f - MathF.Exp(-Rate * Tick.deltaTime);
|
||||||
|
foreach (var (transforms, lerps, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var t = transforms.Span;
|
||||||
|
var l = lerps.Span;
|
||||||
|
for (var i = 0; i < t.Length; i++)
|
||||||
|
{
|
||||||
|
ref var lerp = ref l[i];
|
||||||
|
ref var position = ref t[i].Position;
|
||||||
|
if (!lerp.Initialized)
|
||||||
|
{
|
||||||
|
lerp.Visual = lerp.Target = position;
|
||||||
|
lerp.Initialized = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Транформ трогает только снапшот: разошёлся с нарисованным — новая цель.
|
||||||
|
if (position != lerp.Visual)
|
||||||
|
{
|
||||||
|
lerp.Target = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
lerp.Visual = Vector2.Lerp(lerp.Visual, lerp.Target, blend);
|
||||||
|
position = lerp.Visual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,48 @@
|
|||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Net;
|
||||||
using LittleSim.Scenes;
|
using LittleSim.Scenes;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
|
||||||
// Контент модов (дефы, локализация, атласы) загружается до создания окна — чистый CPU.
|
// Контент-линт без окна/GPU: грузит моды/дефы (патчи и валидаторы тоже), компилирует формулы генов
|
||||||
var content = GameContent.Load();
|
// (включая группировку gsum) и выходит. Удобно для CI.
|
||||||
|
if (args.Contains("--check-content"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = GameContent.Load();
|
||||||
|
var genes = content.Defs.All<GeneDef>();
|
||||||
|
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||||||
|
var traits = Phenotype.Compute(Genome.Generate(genes, new Random(1)), registry);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"content ok: {content.Defs.TypeKeys.Count} def types, {genes.Count} genes, "
|
||||||
|
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
|
||||||
|
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception error)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"content FAILED: {error.Message}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --connect [ws://host:port] — после загрузки контента сразу в сетевую сцену
|
||||||
|
// (см. MultiplayerScene); без аргументов — обычный запуск в главное меню.
|
||||||
|
string? connect = null;
|
||||||
|
var connectIndex = Array.IndexOf(args, "--connect");
|
||||||
|
if (connectIndex >= 0)
|
||||||
|
{
|
||||||
|
connect =
|
||||||
|
connectIndex + 1 < args.Length && !args[connectIndex + 1].StartsWith("--")
|
||||||
|
? args[connectIndex + 1]
|
||||||
|
: $"ws://localhost:{NetSchema.DefaultPort}/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне.
|
||||||
using var host = new GameHost(
|
using var host = new GameHost(
|
||||||
new GameHostOptions
|
new GameHostOptions
|
||||||
{
|
{
|
||||||
@@ -14,10 +51,7 @@ using var host = new GameHost(
|
|||||||
Height = 720,
|
Height = 720,
|
||||||
ClearColor = new Color(12, 16, 24),
|
ClearColor = new Color(12, 16, 24),
|
||||||
},
|
},
|
||||||
new WorldScene(seed: 1337)
|
new BootScene(connect)
|
||||||
);
|
);
|
||||||
|
|
||||||
host.Context.Services.Add(content);
|
|
||||||
host.Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory));
|
|
||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Audio;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загрузочный экран. Поднимает аудио и управление скоростью, грузит настройки и применяет
|
||||||
|
/// окно/громкость, затем грузит контент Core-мода в фоне (чистый CPU/диск — без GPU) с
|
||||||
|
/// анимированной надписью. По готовности регистрирует сервисы и уходит в главное меню.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BootScene : Scene
|
||||||
|
{
|
||||||
|
private readonly string[] _steps = { "Загрузка", "Loading" };
|
||||||
|
private readonly string? _connectTo;
|
||||||
|
private Task<GameContent>? _load;
|
||||||
|
private GameSettings _settings = new();
|
||||||
|
private Label _label = null!;
|
||||||
|
|
||||||
|
/// <summary>Обычный запуск — в главное меню.</summary>
|
||||||
|
public BootScene()
|
||||||
|
: this(null) { }
|
||||||
|
|
||||||
|
/// <summary>С <paramref name="connectTo"/> (ws://host:port) грузится сразу в сетевую сцену.</summary>
|
||||||
|
public BootScene(string? connectTo) => _connectTo = connectTo;
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
Context.UseAudio();
|
||||||
|
Context.UseGameSpeed(1f, 3f, 6f);
|
||||||
|
|
||||||
|
_settings = GameSettingsStore.Load();
|
||||||
|
var host = (GameHost)Context.Services.Get<Game>();
|
||||||
|
host.Graphics.IsFullScreen = _settings.Fullscreen;
|
||||||
|
host.Graphics.SynchronizeWithVerticalRetrace = _settings.VSync;
|
||||||
|
host.Graphics.PreferredBackBufferWidth = _settings.Width;
|
||||||
|
host.Graphics.PreferredBackBufferHeight = _settings.Height;
|
||||||
|
host.Graphics.ApplyChanges();
|
||||||
|
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
|
||||||
|
|
||||||
|
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
_label = new Label
|
||||||
|
{
|
||||||
|
TextColor = Ui.Accent,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
desktop.Root = Ui.Screen(_label);
|
||||||
|
|
||||||
|
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
|
||||||
|
_load = Task.Run(() => GameContent.Load());
|
||||||
|
UpdateSystems.Add(new CallbackSystem(Tick));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
var word = _settings.Language == "en" ? _steps[1] : _steps[0];
|
||||||
|
var dots = new string('.', (int)(Context.Clock.UnscaledTotalTime * 2) % 4);
|
||||||
|
_label.Text = word + dots;
|
||||||
|
|
||||||
|
if (_load is null || !_load.IsCompleted || Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_load.IsFaulted)
|
||||||
|
{
|
||||||
|
_label.Text = _load.Exception?.GetBaseException().Message ?? "load failed";
|
||||||
|
Log.Error($"Content load failed: {_label.Text}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = _load.Result;
|
||||||
|
_load = null;
|
||||||
|
Context.Services.Add(content);
|
||||||
|
Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory));
|
||||||
|
content.Languages.SetLanguage(_settings.Language);
|
||||||
|
Scene next = _connectTo is null
|
||||||
|
? new MainMenuScene()
|
||||||
|
: new MultiplayerScene(new Uri(_connectTo));
|
||||||
|
Context.Scenes.Switch(next, Transitions.Fade(0.6f));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,30 @@
|
|||||||
using Friflo.Engine.ECS;
|
using Friflo.Engine.ECS;
|
||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
using MrGameEng.DevConsole;
|
using MrGameEng.DevConsole;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
namespace LittleSim.Scenes;
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
/// <summary>Спавн растительности по скаттеру дефов террейна (общий для обеих сцен).</summary>
|
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
||||||
internal static class ScatterSpawner
|
internal static class ScatterSpawner
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение
|
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
||||||
/// со случайным смещением. Деревья (ствол > 0) получают коллайдер-препятствие,
|
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
||||||
/// когда заданы слои коллизий.
|
/// разновозрастный. Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением
|
||||||
|
/// управляет <see cref="PlantGrowthSystem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Spawn(
|
public static void Spawn(
|
||||||
Scene scene,
|
Scene scene,
|
||||||
GameContent content,
|
GameContent content,
|
||||||
ModAtlases atlases,
|
PlantSet plants,
|
||||||
GraphicsDevice device,
|
|
||||||
TerrainDef terrain,
|
TerrainDef terrain,
|
||||||
Point cell,
|
Point cell,
|
||||||
int cellSize,
|
int cellSize,
|
||||||
Random random,
|
Random random
|
||||||
uint obstacleLayer = 0,
|
|
||||||
uint collidesWith = 0
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
foreach (var entry in terrain.Scatter)
|
foreach (var entry in terrain.Scatter)
|
||||||
@@ -36,28 +34,22 @@ internal static class ScatterSpawner
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var plant = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
||||||
var region = atlases.GetRegion(device, plant.Texture);
|
var index = plants.IndexOf(def);
|
||||||
var sprite = new Sprite(region, GameLayers.Beings);
|
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||||
sprite.CenterOrigin();
|
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||||
var entity = scene.Store.CreateEntity(
|
var age = random.NextSingle() * plants[index].MaturityDays;
|
||||||
new Transform2D(
|
|
||||||
new Vector2(
|
|
||||||
(cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize,
|
|
||||||
(cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize
|
|
||||||
),
|
|
||||||
scale: new Vector2(cellSize * plant.SizeCells / region.Width)
|
|
||||||
),
|
|
||||||
sprite
|
|
||||||
);
|
|
||||||
|
|
||||||
if (obstacleLayer != 0 && plant.TrunkRadiusCells > 0f)
|
PlantFactory.Create(
|
||||||
{
|
scene.Store,
|
||||||
var trunk = MrGameEng.Collisions.Collider.Circle(cellSize * plant.TrunkRadiusCells);
|
plants,
|
||||||
trunk.Layer = obstacleLayer;
|
index,
|
||||||
trunk.CollidesWith = collidesWith;
|
new Vector2(px, py),
|
||||||
entity.AddComponent(trunk);
|
age,
|
||||||
}
|
plants[index].Template.Generate(random),
|
||||||
|
terrain.Fertility,
|
||||||
|
cellSize
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>Экран «Авторы»: демонстрационный, пока один автор. Назад — кнопкой или Esc.</summary>
|
||||||
|
public sealed class CreditsScene : Scene
|
||||||
|
{
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
var lang = Context.Services.Get<GameContent>().Languages;
|
||||||
|
var input = this.UseInput();
|
||||||
|
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
|
var column = Ui.Column(12);
|
||||||
|
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
||||||
|
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
||||||
|
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
|
||||||
|
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
|
||||||
|
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(
|
||||||
|
new CallbackSystem(() =>
|
||||||
|
{
|
||||||
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
Back();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Back()
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Экран загрузки: список сохранений из <see cref="SaveStore"/> (имя, размер, дата) с
|
||||||
|
/// кнопками «Загрузить» и «Удалить». Загрузка пересоздаёт мир из полного состояния симуляции.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoadGameScene : Scene
|
||||||
|
{
|
||||||
|
private readonly SaveStore _store = new();
|
||||||
|
private GameContent _content = null!;
|
||||||
|
private Desktop _desktop = null!;
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
_content = Context.Services.Get<GameContent>();
|
||||||
|
var input = this.UseInput();
|
||||||
|
_desktop = this.UseUI();
|
||||||
|
Rebuild();
|
||||||
|
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new CallbackSystem(() =>
|
||||||
|
{
|
||||||
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
Back();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rebuild()
|
||||||
|
{
|
||||||
|
var lang = _content.Languages;
|
||||||
|
var column = Ui.Column(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)
|
||||||
|
{
|
||||||
|
column.Widgets.Add(Ui.Subtitle(lang.Get("load.empty")));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in saves)
|
||||||
|
{
|
||||||
|
var save = entry.Save;
|
||||||
|
var row = Ui.Row(8);
|
||||||
|
row.Widgets.Add(
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
TextColor = Ui.Accent,
|
||||||
|
Text =
|
||||||
|
$"{save.Name} — {save.Width}×{save.Height} — "
|
||||||
|
+ save.SavedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var file = entry.FileName;
|
||||||
|
row.Widgets.Add(
|
||||||
|
Ui.Button(
|
||||||
|
lang.Get("load.load"),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(
|
||||||
|
new WorldScene(save.ToConfig(), save),
|
||||||
|
Transitions.Fade(0.6f)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: 130
|
||||||
|
)
|
||||||
|
);
|
||||||
|
row.Widgets.Add(
|
||||||
|
Ui.Button(
|
||||||
|
lang.Get("load.delete"),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_store.Delete(file);
|
||||||
|
Rebuild();
|
||||||
|
},
|
||||||
|
width: 130
|
||||||
|
)
|
||||||
|
);
|
||||||
|
column.Widgets.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
|
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
|
||||||
|
|
||||||
|
_desktop.Root = Ui.Screen(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Back()
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Главное меню: заголовок и кнопки Новый мир / Загрузка / Настройки / Авторы / Выход.
|
||||||
|
/// Все подписи — из локализации Core-мода. Чистый UI-экран (Myra), без мира и рендерера.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MainMenuScene : Scene
|
||||||
|
{
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
var content = Context.Services.Get<GameContent>();
|
||||||
|
var lang = content.Languages;
|
||||||
|
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
|
var column = Ui.Column(12);
|
||||||
|
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||||
|
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
||||||
|
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
||||||
|
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
||||||
|
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
||||||
|
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
||||||
|
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
||||||
|
column.Widgets.Add(
|
||||||
|
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
|
||||||
|
);
|
||||||
|
|
||||||
|
desktop.Root = Ui.Screen(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Go(Scene scene)
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(scene, Transitions.Fade(0.4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Net;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Assets;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сетевой клиент: подключается к дедикейтед-серверу (LittleSim.Server --listen) и рендерит
|
||||||
|
/// реплицированных жителей. Симуляция целиком на сервере; сюда приезжают только компоненты
|
||||||
|
/// из <see cref="NetSchema"/>, а спрайты вешаются локально при спавне — граница
|
||||||
|
/// sim/presentation проходит теперь через сеть. Esc — назад в меню, команда консоли `net` —
|
||||||
|
/// состояние соединения.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MultiplayerScene : Scene
|
||||||
|
{
|
||||||
|
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
|
||||||
|
|
||||||
|
private readonly Uri _server;
|
||||||
|
private Task<WebSocketClient>? _connecting;
|
||||||
|
private WebSocketClient? _connection;
|
||||||
|
private ReplicationClient _replication = null!;
|
||||||
|
private InputManager _input = null!;
|
||||||
|
private GameContent _content = null!;
|
||||||
|
private Label _hud = null!;
|
||||||
|
private string _statusKey = "net.connecting";
|
||||||
|
|
||||||
|
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
|
||||||
|
public MultiplayerScene(Uri server) => _server = server;
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
_content = Context.Services.Get<GameContent>();
|
||||||
|
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
||||||
|
_input = this.UseInput();
|
||||||
|
var renderer = this.UseRenderer2D(
|
||||||
|
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
||||||
|
);
|
||||||
|
GameLayers.EnsureRegistered(renderer);
|
||||||
|
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||||
|
Store.CreateEntity(new Camera(Bounds.Center, zoom: 1f, bounds: Bounds));
|
||||||
|
|
||||||
|
// Реплика пишет в ECS сцены; презентацию (спрайт) вешаем сами при спавне.
|
||||||
|
_replication = new ReplicationClient(NetSchema.Create(), Store);
|
||||||
|
_replication.EntitySpawned += entity =>
|
||||||
|
{
|
||||||
|
var sprite = new Sprite(white, GameLayers.Beings);
|
||||||
|
sprite.CenterOrigin();
|
||||||
|
entity.AddComponent(sprite);
|
||||||
|
entity.AddComponent(new NetLerp());
|
||||||
|
};
|
||||||
|
|
||||||
|
var desktop = this.UseUI();
|
||||||
|
_hud = new Label { Left = 10, Top = 8 };
|
||||||
|
desktop.Root = Ui.Screen(_hud);
|
||||||
|
|
||||||
|
UpdateSystems.Add(new CallbackSystem(Pump));
|
||||||
|
// Снапшоты приходят ~10 раз в секунду — сглаживаем позиции до частоты кадра.
|
||||||
|
UpdateSystems.Add(new NetSmoothingSystem());
|
||||||
|
// Усталость жителей видна и по сети: PawnNeeds реплицируется, спрайт темнеет локально.
|
||||||
|
UpdateSystems.Add(new PawnAppearanceSystem());
|
||||||
|
|
||||||
|
var console = this.UseDevConsole();
|
||||||
|
console.Register(
|
||||||
|
"net",
|
||||||
|
"net — connection status and replicated entity count",
|
||||||
|
(c, _) =>
|
||||||
|
{
|
||||||
|
c.WriteLine($"server: {_server}");
|
||||||
|
c.WriteLine($"state: {(_connection?.IsOpen == true ? "connected" : "offline")}");
|
||||||
|
c.WriteLine($"entities: {_replication.EntityCount}");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
|
private void Pump()
|
||||||
|
{
|
||||||
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
|
{
|
||||||
|
_connecting = null;
|
||||||
|
if (finished.IsFaulted)
|
||||||
|
{
|
||||||
|
_statusKey = "net.failed";
|
||||||
|
Log.Error(
|
||||||
|
$"Connect to {_server} failed: "
|
||||||
|
+ finished.Exception?.GetBaseException().Message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
_statusKey = "net.connected";
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connection is not null)
|
||||||
|
{
|
||||||
|
_replication.Pump(_connection);
|
||||||
|
if (!_connection.IsOpen)
|
||||||
|
{
|
||||||
|
_statusKey = "net.lost";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hud.Text = _content.Languages.Format(
|
||||||
|
"net.hud",
|
||||||
|
_content.Languages.Get(_statusKey),
|
||||||
|
_replication.EntityCount
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
_connection?.Close();
|
||||||
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Окно настройки нового мира: имя, размер (пресеты из <see cref="WorldPresetDef"/> Core-мода),
|
||||||
|
/// сид (с кнопкой «случайно») и сглаживание рельефа. «Создать» запускает <see cref="WorldScene"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewWorldScene : Scene
|
||||||
|
{
|
||||||
|
private const int MinSmoothing = 1;
|
||||||
|
private const int MaxSmoothing = 8;
|
||||||
|
|
||||||
|
private WorldPresetDef _preset = null!;
|
||||||
|
private int _smoothing = 4;
|
||||||
|
private TextBox _name = null!;
|
||||||
|
private TextBox _seed = null!;
|
||||||
|
private readonly List<Action> _refreshers = [];
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
var content = Context.Services.Get<GameContent>();
|
||||||
|
var lang = content.Languages;
|
||||||
|
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 column = Ui.Column(10);
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Размер — сегменты пресетов.
|
||||||
|
var sizeRow = Ui.Row(6);
|
||||||
|
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) };
|
||||||
|
button.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_preset = value;
|
||||||
|
Refresh();
|
||||||
|
};
|
||||||
|
sizeRow.Widgets.Add(button);
|
||||||
|
_refreshers.Add(() =>
|
||||||
|
button.TextColor = ReferenceEquals(_preset, value) ? Ui.Accent : Ui.Muted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
column.Widgets.Add(sizeRow);
|
||||||
|
|
||||||
|
// Сид + случайно.
|
||||||
|
_seed = new TextBox { Text = Random.Shared.Next().ToString(), Width = 200 };
|
||||||
|
var seedRow = Ui.Row(6);
|
||||||
|
seedRow.Widgets.Add(new Label { Text = lang.Get("newworld.seed"), TextColor = Ui.Muted });
|
||||||
|
seedRow.Widgets.Add(_seed);
|
||||||
|
seedRow.Widgets.Add(
|
||||||
|
Ui.Button(
|
||||||
|
lang.Get("newworld.random"),
|
||||||
|
() => _seed.Text = Random.Shared.Next().ToString(),
|
||||||
|
width: 120
|
||||||
|
)
|
||||||
|
);
|
||||||
|
column.Widgets.Add(seedRow);
|
||||||
|
|
||||||
|
// Сглаживание −/+.
|
||||||
|
var smoothRow = Ui.Row(6);
|
||||||
|
var smoothLabel = new Label { TextColor = Ui.Muted };
|
||||||
|
_refreshers.Add(() => smoothLabel.Text = $"{lang.Get("newworld.smoothing")}: {_smoothing}");
|
||||||
|
var minus = new TextButton { Text = "−" };
|
||||||
|
minus.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_smoothing = Math.Max(MinSmoothing, _smoothing - 1);
|
||||||
|
Refresh();
|
||||||
|
};
|
||||||
|
var plus = new TextButton { Text = "+" };
|
||||||
|
plus.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
_smoothing = Math.Min(MaxSmoothing, _smoothing + 1);
|
||||||
|
Refresh();
|
||||||
|
};
|
||||||
|
smoothRow.Widgets.Add(smoothLabel);
|
||||||
|
smoothRow.Widgets.Add(minus);
|
||||||
|
smoothRow.Widgets.Add(plus);
|
||||||
|
column.Widgets.Add(smoothRow);
|
||||||
|
|
||||||
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
|
var buttons = Ui.Row(10);
|
||||||
|
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.create"), Create, width: 150));
|
||||||
|
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
|
||||||
|
column.Widgets.Add(buttons);
|
||||||
|
|
||||||
|
desktop.Root = Ui.Screen(column);
|
||||||
|
Refresh();
|
||||||
|
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new CallbackSystem(() =>
|
||||||
|
{
|
||||||
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
Back();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Widget LabeledRow(string label, Widget control)
|
||||||
|
{
|
||||||
|
var row = Ui.Row(6);
|
||||||
|
row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted });
|
||||||
|
row.Widgets.Add(control);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Refresh()
|
||||||
|
{
|
||||||
|
foreach (var refresh in _refreshers)
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Create()
|
||||||
|
{
|
||||||
|
if (Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
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 config = new WorldConfig
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Width = _preset.Width,
|
||||||
|
Height = _preset.Height,
|
||||||
|
Population = _preset.Population,
|
||||||
|
Seed = seed,
|
||||||
|
SmoothPasses = _smoothing,
|
||||||
|
};
|
||||||
|
Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Back()
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
using System;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Audio;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Внутриигровое меню-пауза: затемняющий оверлей поверх мира (мир остаётся загруженным).
|
||||||
|
/// Открытие ставит игру на паузу через <see cref="GameSpeed"/>, закрытие — снимает.
|
||||||
|
/// Кнопки: Продолжить / Настройки (общая <see cref="SettingsPanel"/>) / Сохранить /
|
||||||
|
/// Главное меню / Выход. <see cref="Root"/> добавляется в корневую панель сцены.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PauseMenu
|
||||||
|
{
|
||||||
|
private readonly EngineContext _context;
|
||||||
|
private readonly GameContent _content;
|
||||||
|
private readonly GameSpeed _speed;
|
||||||
|
private readonly GameHost _host;
|
||||||
|
private readonly AudioManager _audio;
|
||||||
|
private readonly Func<string> _onSave;
|
||||||
|
private readonly Action _onMainMenu;
|
||||||
|
private readonly Action _onQuit;
|
||||||
|
private readonly Panel _overlay;
|
||||||
|
private GameSettings _settings = GameSettingsStore.Load();
|
||||||
|
private bool _inSettings;
|
||||||
|
private string? _savedToast;
|
||||||
|
|
||||||
|
public PauseMenu(
|
||||||
|
EngineContext context,
|
||||||
|
GameContent content,
|
||||||
|
GameSpeed speed,
|
||||||
|
Func<string> onSave,
|
||||||
|
Action onMainMenu,
|
||||||
|
Action onQuit
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_content = content;
|
||||||
|
_speed = speed;
|
||||||
|
_host = (GameHost)context.Services.Get<Game>();
|
||||||
|
_audio = context.Services.Get<AudioManager>();
|
||||||
|
_onSave = onSave;
|
||||||
|
_onMainMenu = onMainMenu;
|
||||||
|
_onQuit = onQuit;
|
||||||
|
|
||||||
|
_overlay = new Panel
|
||||||
|
{
|
||||||
|
Visible = false,
|
||||||
|
Background = new SolidBrush(new Color(0, 0, 0, 200)),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Виджет оверлея для добавления в корень сцены.</summary>
|
||||||
|
public Widget Root => _overlay;
|
||||||
|
|
||||||
|
/// <summary>Открыт ли оверлей паузы.</summary>
|
||||||
|
public bool IsOpen { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реакция на Esc: закрыть подменю настроек → вернуться к кнопкам; иначе открыть/закрыть
|
||||||
|
/// меню паузы.
|
||||||
|
/// </summary>
|
||||||
|
public void Toggle()
|
||||||
|
{
|
||||||
|
if (!IsOpen)
|
||||||
|
{
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
else if (_inSettings)
|
||||||
|
{
|
||||||
|
_inSettings = false;
|
||||||
|
ShowButtons();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Open()
|
||||||
|
{
|
||||||
|
IsOpen = true;
|
||||||
|
_inSettings = false;
|
||||||
|
_savedToast = null;
|
||||||
|
_speed.Pause();
|
||||||
|
ShowButtons();
|
||||||
|
_overlay.Visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
IsOpen = false;
|
||||||
|
_inSettings = false;
|
||||||
|
_overlay.Visible = false;
|
||||||
|
_speed.Resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowButtons()
|
||||||
|
{
|
||||||
|
var lang = _content.Languages;
|
||||||
|
var column = Ui.Column(10);
|
||||||
|
column.Widgets.Add(Ui.Title(lang.Get("pause.title")));
|
||||||
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
|
column.Widgets.Add(Ui.Button(lang.Get("pause.resume"), Close));
|
||||||
|
column.Widgets.Add(
|
||||||
|
Ui.Button(
|
||||||
|
lang.Get("pause.settings"),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_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.quit"), _onQuit));
|
||||||
|
|
||||||
|
if (_savedToast is not null)
|
||||||
|
{
|
||||||
|
column.Widgets.Add(
|
||||||
|
new Label { Text = lang.Format("pause.saved", _savedToast), TextColor = Ui.Muted }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetContent(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowSettings() =>
|
||||||
|
SetContent(
|
||||||
|
SettingsPanel.Build(
|
||||||
|
_settings,
|
||||||
|
_content.Languages,
|
||||||
|
onApply: () =>
|
||||||
|
{
|
||||||
|
GameSettingsStore.Save(_settings);
|
||||||
|
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||||
|
ShowSettings(); // язык мог смениться
|
||||||
|
},
|
||||||
|
onBack: () =>
|
||||||
|
{
|
||||||
|
_inSettings = false;
|
||||||
|
ShowButtons();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
_savedToast = _onSave();
|
||||||
|
ShowButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetContent(Widget content)
|
||||||
|
{
|
||||||
|
_overlay.Widgets.Clear();
|
||||||
|
_overlay.Widgets.Add(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Audio;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Экран настроек игры (язык, экран, громкость, разрешение). Использует общий
|
||||||
|
/// <see cref="SettingsPanel"/>. «Применить» сохраняет в settings.json и применяет к движку
|
||||||
|
/// (смена языка перестраивает подписи на лету); «Назад»/Esc — в главное меню.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SettingsScene : Scene
|
||||||
|
{
|
||||||
|
private GameContent _content = null!;
|
||||||
|
private AudioManager _audio = null!;
|
||||||
|
private GameHost _host = null!;
|
||||||
|
private Desktop _desktop = null!;
|
||||||
|
private GameSettings _settings = GameSettingsStore.Load();
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
_content = Context.Services.Get<GameContent>();
|
||||||
|
_audio = Context.Services.Get<AudioManager>();
|
||||||
|
_host = (GameHost)Context.Services.Get<Game>();
|
||||||
|
var input = this.UseInput();
|
||||||
|
|
||||||
|
_desktop = this.UseUI();
|
||||||
|
Rebuild();
|
||||||
|
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new CallbackSystem(() =>
|
||||||
|
{
|
||||||
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
Back();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rebuild() =>
|
||||||
|
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back));
|
||||||
|
|
||||||
|
private void Apply()
|
||||||
|
{
|
||||||
|
GameSettingsStore.Save(_settings);
|
||||||
|
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||||
|
Rebuild(); // язык мог смениться — перестраиваем подписи
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Back()
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
using Friflo.Engine.ECS;
|
|
||||||
using LittleSim.Content;
|
|
||||||
using LittleSim.Sim;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Assets;
|
|
||||||
using MrGameEng.Collisions;
|
|
||||||
using MrGameEng.Core;
|
|
||||||
using MrGameEng.DevConsole;
|
|
||||||
using MrGameEng.Graphics;
|
|
||||||
using MrGameEng.Input;
|
|
||||||
using MrGameEng.Tilemaps;
|
|
||||||
using MrGameEng.UI;
|
|
||||||
using Myra.Graphics2D.UI;
|
|
||||||
|
|
||||||
namespace LittleSim.Scenes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Демо-сцена тайлового террейна: карта-Tilemap по дефам рельефа (поверхность из атласа
|
|
||||||
/// или тонированный тайл), растительность по скаттеру дефов (деревья — с коллайдером
|
|
||||||
/// ствола), животные — дефы вида "animal" с расталкиванием. Команды: "regen", "world".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TerrainScene : Scene
|
|
||||||
{
|
|
||||||
private const int TerrainWidth = 44;
|
|
||||||
private const int TerrainHeight = 28;
|
|
||||||
private const int CellSize = 24;
|
|
||||||
private const int Population = 14;
|
|
||||||
|
|
||||||
private static readonly RectF TerrainBounds = new(
|
|
||||||
0f,
|
|
||||||
0f,
|
|
||||||
TerrainWidth * CellSize,
|
|
||||||
TerrainHeight * CellSize
|
|
||||||
);
|
|
||||||
|
|
||||||
private const uint AnimalLayer = 0b01;
|
|
||||||
private const uint ObstacleLayer = 0b10;
|
|
||||||
|
|
||||||
private readonly int _seed;
|
|
||||||
|
|
||||||
public TerrainScene(int seed) => _seed = seed;
|
|
||||||
|
|
||||||
protected override void OnLoad()
|
|
||||||
{
|
|
||||||
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
|
||||||
var content = Context.Services.Get<GameContent>();
|
|
||||||
var atlases = Context.Services.Get<ModAtlases>();
|
|
||||||
var device = Context.GraphicsDevice;
|
|
||||||
var input = this.UseInput();
|
|
||||||
var renderer = this.UseRenderer2D(
|
|
||||||
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
|
||||||
);
|
|
||||||
GameLayers.EnsureRegistered(renderer);
|
|
||||||
this.UseTilemaps();
|
|
||||||
|
|
||||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
|
||||||
|
|
||||||
// Тайлсет из дефов: у дефа есть поверхность — текстура из атласа, нет — тонированный тайл.
|
|
||||||
var tiles = new TileSet();
|
|
||||||
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))
|
|
||||||
: tiles.Add(white, terrain.Tint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Террейн строится кодом: высоты по сиду → деф клетки → id тайла; поверх — скаттер.
|
|
||||||
var random = new Random(_seed);
|
|
||||||
var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed);
|
|
||||||
var grid = new TileGrid(TerrainWidth, TerrainHeight);
|
|
||||||
var landCells = new List<Point>();
|
|
||||||
for (var x = 0; x < TerrainWidth; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < TerrainHeight; y++)
|
|
||||||
{
|
|
||||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
|
||||||
grid[x, y] = tileByDef[terrain];
|
|
||||||
var cell = new Point(x, y);
|
|
||||||
if (terrain.IsLand)
|
|
||||||
{
|
|
||||||
landCells.Add(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScatterSpawner.Spawn(
|
|
||||||
this,
|
|
||||||
content,
|
|
||||||
atlases,
|
|
||||||
device,
|
|
||||||
terrain,
|
|
||||||
cell,
|
|
||||||
CellSize,
|
|
||||||
random,
|
|
||||||
obstacleLayer: ObstacleLayer,
|
|
||||||
collidesWith: AnimalLayer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
|
||||||
|
|
||||||
// Животные: дефы вида "animal", не наслаиваются (коллайдеры + расталкивание).
|
|
||||||
var animals = content.Defs.All<PawnDef>().Where(p => p.Kind == PawnDef.KindAnimal).ToList();
|
|
||||||
for (var i = 0; i < Population && landCells.Count > 0 && animals.Count > 0; i++)
|
|
||||||
{
|
|
||||||
var cell = landCells[random.Next(landCells.Count)];
|
|
||||||
var animal = animals[random.Next(animals.Count)];
|
|
||||||
var body = atlases.GetRegion(device, animal.Texture);
|
|
||||||
var sprite = new Sprite(body, GameLayers.Beings);
|
|
||||||
sprite.CenterOrigin();
|
|
||||||
var collider = Collider.Circle(animal.SizeCells * CellSize * 0.32f);
|
|
||||||
collider.Layer = AnimalLayer;
|
|
||||||
collider.CollidesWith = AnimalLayer | ObstacleLayer;
|
|
||||||
Store.CreateEntity(
|
|
||||||
new Transform2D(
|
|
||||||
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
|
|
||||||
scale: new Vector2(CellSize * animal.SizeCells / body.Width)
|
|
||||||
),
|
|
||||||
sprite,
|
|
||||||
new Wander(),
|
|
||||||
collider
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var camera = Store.CreateEntity(
|
|
||||||
new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds)
|
|
||||||
);
|
|
||||||
|
|
||||||
var desktop = this.UseUI();
|
|
||||||
var hudLabel = new Label { Left = 10, Top = 8 };
|
|
||||||
desktop.Root = hudLabel;
|
|
||||||
|
|
||||||
var console = this.UseDevConsole();
|
|
||||||
ContentCommands.Register(console, content, atlases);
|
|
||||||
console.Register(
|
|
||||||
"regen",
|
|
||||||
"regen [seed] — regenerate the terrain",
|
|
||||||
(c, args) =>
|
|
||||||
{
|
|
||||||
if (Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
|
||||||
c.WriteLine($"regenerating terrain, seed {seed}");
|
|
||||||
Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.Register(
|
|
||||||
"world",
|
|
||||||
"world [seed] — switch to the world scene",
|
|
||||||
(c, args) =>
|
|
||||||
{
|
|
||||||
if (Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var seed = args.Length > 0 ? int.Parse(args[0]) : _seed;
|
|
||||||
Context.Scenes.Switch(new WorldScene(seed), Transition.Fade(0.6f));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds));
|
|
||||||
var collisions = this.UseCollisions(cellSize: CellSize * 2f); // после движения
|
|
||||||
UpdateSystems.Add(new SeparationSystem(collisions));
|
|
||||||
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
|
|
||||||
UpdateSystems.Add(
|
|
||||||
new HudSystem(Context, content.Languages, hudLabel, "hud.terrain", _seed, Population)
|
|
||||||
);
|
|
||||||
|
|
||||||
Log.Info(
|
|
||||||
$"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells, {Population} animals"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+539
-120
@@ -1,124 +1,464 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Friflo.Engine.ECS;
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.App;
|
||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
using LittleSim.Sim;
|
using LittleSim.Sim;
|
||||||
|
using LittleSim.UI;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
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.Formulas;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Host;
|
||||||
using MrGameEng.Input;
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Inspector;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
using MrGameEng.Tilemaps;
|
||||||
using MrGameEng.UI;
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D;
|
||||||
using Myra.Graphics2D.UI;
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
namespace LittleSim.Scenes;
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Мир LittleSim: рельеф, растения и жители целиком описаны дефами Core-мода
|
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
|
||||||
/// (Mods/Core/Defs), текстуры — атласами из дерева текстур модов, строки HUD —
|
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
|
||||||
/// локализацией. Камера бога, HUD и консоль ("regen", "terrain", "mods", "lang", …).
|
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
|
||||||
|
/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/
|
||||||
|
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
|
||||||
|
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorldScene : Scene
|
public sealed class WorldScene : Scene
|
||||||
{
|
{
|
||||||
public const int WorldWidth = 120;
|
|
||||||
public const int WorldHeight = 70;
|
|
||||||
public const int CellSize = 16;
|
public const int CellSize = 16;
|
||||||
private const int Population = 80;
|
|
||||||
|
|
||||||
private static readonly RectF WorldBounds = new(
|
/// <summary>
|
||||||
0f,
|
/// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые
|
||||||
0f,
|
/// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с.
|
||||||
WorldWidth * CellSize,
|
/// </summary>
|
||||||
WorldHeight * CellSize
|
public const float SecondsPerDay = 480f;
|
||||||
);
|
|
||||||
|
|
||||||
private readonly int _seed;
|
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
|
||||||
|
public const int DensityCap = 4;
|
||||||
|
|
||||||
public WorldScene(int seed) => _seed = seed;
|
private readonly WorldConfig _config;
|
||||||
|
private readonly WorldSave? _save;
|
||||||
|
private readonly RectF _bounds;
|
||||||
|
|
||||||
|
private GameSpeed _speed = null!;
|
||||||
|
private PauseMenu _pause = null!;
|
||||||
|
private readonly List<Action> _speedRefreshers = [];
|
||||||
|
|
||||||
|
private PlantSet _plants = null!;
|
||||||
|
private float[] _cellFertility = [];
|
||||||
|
private bool[] _cellLand = [];
|
||||||
|
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
|
||||||
|
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
|
||||||
|
|
||||||
|
/// <summary>Новый мир из конфига.</summary>
|
||||||
|
public WorldScene(WorldConfig config)
|
||||||
|
: this(config, null) { }
|
||||||
|
|
||||||
|
/// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
|
||||||
|
public WorldScene(WorldConfig config, WorldSave? save)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_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 assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
||||||
var content = Context.Services.Get<GameContent>();
|
var content = Context.Services.Get<GameContent>();
|
||||||
var atlases = Context.Services.Get<ModAtlases>();
|
var atlases = Context.Services.Get<ModAtlases>();
|
||||||
var device = Context.GraphicsDevice;
|
var device = Context.GetGraphicsDevice();
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
|
_speed = Context.Services.Get<GameSpeed>();
|
||||||
|
_speed.SetStep(0); // новый мир/загрузка стартуют на x1
|
||||||
|
|
||||||
var renderer = this.UseRenderer2D(
|
var renderer = this.UseRenderer2D(
|
||||||
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
||||||
);
|
);
|
||||||
GameLayers.EnsureRegistered(renderer);
|
GameLayers.EnsureRegistered(renderer);
|
||||||
|
this.UseTilemaps();
|
||||||
|
|
||||||
|
var calendar = Context.UseCalendar(SecondsPerDay);
|
||||||
|
var climate = Context.UseClimate(ClimateSettings.Default);
|
||||||
|
var dayNight = new DayNight(calendar, DayNightSettings.Default);
|
||||||
|
|
||||||
|
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
||||||
|
_plants = new PlantSet(content, atlases, device);
|
||||||
|
var loadingPlants = _save?.Plants is { Count: > 0 };
|
||||||
|
BuildTerrain(
|
||||||
|
content,
|
||||||
|
atlases,
|
||||||
|
device,
|
||||||
|
assets,
|
||||||
|
loadingPlants ? null : new Random(_config.Seed)
|
||||||
|
);
|
||||||
|
if (loadingPlants)
|
||||||
|
{
|
||||||
|
RestorePlants(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
|
||||||
|
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
|
||||||
|
var lighting = this.UseLighting(
|
||||||
|
renderer,
|
||||||
|
dayNight,
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
CellSize,
|
||||||
|
Vector2.Zero,
|
||||||
|
BuildOccluders
|
||||||
|
);
|
||||||
|
|
||||||
|
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
||||||
|
|
||||||
|
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
||||||
|
var desktop = this.UseUI();
|
||||||
|
var hudLabel = new Label { Left = 10, Top = 8 };
|
||||||
|
var speedBar = BuildSpeedBar(content);
|
||||||
|
_pause = new PauseMenu(
|
||||||
|
Context,
|
||||||
|
content,
|
||||||
|
_speed,
|
||||||
|
onSave: SaveWorld,
|
||||||
|
onMainMenu: () => Switch(new MainMenuScene()),
|
||||||
|
onQuit: () => Context.Services.Get<Game>().Exit()
|
||||||
|
);
|
||||||
|
desktop.Root = Ui.Screen(hudLabel, speedBar, _pause.Root);
|
||||||
|
|
||||||
|
this.UseInspector(renderer);
|
||||||
|
var console = this.UseDevConsole();
|
||||||
|
RegisterCommands(console, content, atlases);
|
||||||
|
console.Register(
|
||||||
|
"light",
|
||||||
|
"light [radius] — place a point light at the cursor (night shadows demo)",
|
||||||
|
(c, args) =>
|
||||||
|
{
|
||||||
|
var radius =
|
||||||
|
args.Length > 0
|
||||||
|
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
: 120f;
|
||||||
|
var mouse = Mouse.GetState();
|
||||||
|
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||||||
|
Store.CreateEntity(
|
||||||
|
Transform2D.At(world),
|
||||||
|
new PointLight
|
||||||
|
{
|
||||||
|
Radius = radius,
|
||||||
|
Color = Color.White,
|
||||||
|
Intensity = 0.9f,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
|
||||||
|
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new PlantLifecycleSystem(
|
||||||
|
Store,
|
||||||
|
_plants,
|
||||||
|
calendar,
|
||||||
|
Context.Clock,
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
CellSize,
|
||||||
|
_cellFertility,
|
||||||
|
_cellLand,
|
||||||
|
DensityCap,
|
||||||
|
_config.Seed
|
||||||
|
)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new HudSystem(
|
||||||
|
Context,
|
||||||
|
content.Languages,
|
||||||
|
hudLabel,
|
||||||
|
"hud.world",
|
||||||
|
_config.Seed,
|
||||||
|
calendar,
|
||||||
|
climate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
|
||||||
|
|
||||||
|
Log.Info(
|
||||||
|
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
|
||||||
|
+ (_save is null ? " (new)" : " (loaded)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
|
||||||
|
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
|
||||||
|
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
|
||||||
|
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
|
||||||
|
/// </summary>
|
||||||
|
private void BuildTerrain(
|
||||||
|
GameContent content,
|
||||||
|
ModAtlases atlases,
|
||||||
|
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||||
|
AssetManager assets,
|
||||||
|
Random? scatterRandom
|
||||||
|
)
|
||||||
|
{
|
||||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||||
|
|
||||||
// Рельеф: одна текстура на все клетки → один draw call на слой;
|
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
|
||||||
// тип клетки и растительность приходят из TerrainDef'ов.
|
var tiles = new TileSet();
|
||||||
var random = new Random(_seed);
|
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
||||||
var heights = WorldGenerator.GenerateHeights(WorldWidth, WorldHeight, _seed);
|
foreach (var terrain in content.Terrains.All)
|
||||||
var landCells = new List<Point>();
|
|
||||||
for (var x = 0; x < WorldWidth; x++)
|
|
||||||
{
|
{
|
||||||
for (var y = 0; y < WorldHeight; y++)
|
tileByDef[terrain] = terrain.Surface is { } surface
|
||||||
|
? tiles.Add(atlases.GetRegion(device, surface))
|
||||||
|
: tiles.Add(white, terrain.Tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
|
||||||
|
var heights = WorldGenerator.Generate(
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
_config.Seed,
|
||||||
|
_config.SmoothPasses
|
||||||
|
);
|
||||||
|
var grid = new TileGrid(_config.Width, _config.Height);
|
||||||
|
var cells = _config.Width * _config.Height;
|
||||||
|
_cellFertility = new float[cells];
|
||||||
|
_cellLand = new bool[cells];
|
||||||
|
_cellOccluderBase = new bool[cells];
|
||||||
|
_cellOccluder = new bool[cells];
|
||||||
|
for (var x = 0; x < _config.Width; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < _config.Height; y++)
|
||||||
{
|
{
|
||||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
var terrain = content.Terrains.Classify(heights[x, y]);
|
||||||
Store.CreateEntity(
|
grid[x, y] = tileByDef[terrain];
|
||||||
Transform2D.At(new Vector2(x * CellSize, y * CellSize)),
|
var cell = y * _config.Width + x;
|
||||||
new Sprite(white) { Color = terrain.Tint }
|
_cellFertility[cell] = terrain.Fertility;
|
||||||
);
|
_cellLand[cell] = terrain.IsLand;
|
||||||
|
_cellOccluderBase[cell] = terrain.BlocksLight;
|
||||||
var cell = new Point(x, y);
|
if (scatterRandom is not null)
|
||||||
if (terrain.IsLand)
|
|
||||||
{
|
{
|
||||||
landCells.Add(cell);
|
ScatterSpawner.Spawn(
|
||||||
|
this,
|
||||||
|
content,
|
||||||
|
_plants,
|
||||||
|
terrain,
|
||||||
|
new Point(x, y),
|
||||||
|
CellSize,
|
||||||
|
scatterRandom
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScatterSpawner.Spawn(
|
|
||||||
this,
|
|
||||||
content,
|
|
||||||
atlases,
|
|
||||||
device,
|
|
||||||
terrain,
|
|
||||||
cell,
|
|
||||||
CellSize,
|
|
||||||
random
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Жители: дефы вида "being", появляются на суше, бродят (Y-sort: кто ниже — ближе).
|
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||||
var beings = content.Defs.All<PawnDef>().Where(p => p.Kind == PawnDef.KindBeing).ToList();
|
}
|
||||||
for (var i = 0; i < Population && landCells.Count > 0 && beings.Count > 0; i++)
|
|
||||||
|
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
||||||
|
{
|
||||||
|
var bar = Ui.Row(6);
|
||||||
|
bar.HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
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();
|
||||||
|
bar.Widgets.Add(pause);
|
||||||
|
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
||||||
|
|
||||||
|
for (var i = 0; i < _speed.Steps.Count; i++)
|
||||||
{
|
{
|
||||||
var cell = landCells[random.Next(landCells.Count)];
|
var index = i;
|
||||||
var being = beings[random.Next(beings.Count)];
|
var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
|
||||||
var body = atlases.GetRegion(device, being.Texture);
|
button.Click += (_, _) => _speed.SetStep(index);
|
||||||
var sprite = new Sprite(body, GameLayers.Beings);
|
bar.Widgets.Add(button);
|
||||||
sprite.CenterOrigin();
|
_speedRefreshers.Add(() =>
|
||||||
Store.CreateEntity(
|
button.TextColor =
|
||||||
new Transform2D(
|
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
|
||||||
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
|
|
||||||
scale: new Vector2(CellSize * being.SizeCells / body.Width)
|
|
||||||
),
|
|
||||||
sprite,
|
|
||||||
new Wander(),
|
|
||||||
// Начальная энергия и фаза решения разбросаны сидом — жители не отдыхают синхронно.
|
|
||||||
new PawnNeeds { Energy = 0.5f + random.NextSingle() * 0.5f },
|
|
||||||
new PawnBrain { Action = PawnAction.Wander, DecideIn = random.NextSingle() * 0.75f }
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var camera = Store.CreateEntity(
|
void Refresh()
|
||||||
new Camera(WorldBounds.Center, zoom: 1f, bounds: WorldBounds)
|
{
|
||||||
);
|
foreach (var refresh in _speedRefreshers)
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HUD и консоль.
|
_speed.Changed += Refresh;
|
||||||
var desktop = this.UseUI();
|
RegisterUnload(() => _speed.Changed -= Refresh);
|
||||||
var hudLabel = new Label { Left = 10, Top = 8 };
|
Refresh();
|
||||||
desktop.Root = hudLabel;
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
var console = this.UseDevConsole();
|
private void Hotkeys(InputManager input)
|
||||||
|
{
|
||||||
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
_pause.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pause.IsOpen)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.Space))
|
||||||
|
{
|
||||||
|
_speed.TogglePause();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.D1))
|
||||||
|
{
|
||||||
|
_speed.SetStep(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
|
||||||
|
{
|
||||||
|
_speed.SetStep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
|
||||||
|
{
|
||||||
|
_speed.SetStep(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SaveWorld()
|
||||||
|
{
|
||||||
|
var save = new WorldSave
|
||||||
|
{
|
||||||
|
Name = _config.Name,
|
||||||
|
Width = _config.Width,
|
||||||
|
Height = _config.Height,
|
||||||
|
Seed = _config.Seed,
|
||||||
|
SmoothPasses = _config.SmoothPasses,
|
||||||
|
Population = _config.Population,
|
||||||
|
SavedUtc = DateTime.UtcNow,
|
||||||
|
ElapsedSeconds = Context.Clock.TotalTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
|
||||||
|
Store
|
||||||
|
.Query<PlantGrowth, PlantOrganism, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(
|
||||||
|
ref PlantGrowth grow,
|
||||||
|
ref PlantOrganism org,
|
||||||
|
ref Transform2D transform,
|
||||||
|
Entity _
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
save.Plants.Add(
|
||||||
|
new PlantSave
|
||||||
|
{
|
||||||
|
Species = _plants[grow.Species].Def.DefName,
|
||||||
|
X = transform.Position.X,
|
||||||
|
Y = transform.Position.Y,
|
||||||
|
AgeDays = grow.AgeDays,
|
||||||
|
Stage = grow.Stage,
|
||||||
|
CellFertility = grow.CellFertility,
|
||||||
|
Genome = org.Genome.ToDictionary(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
new SaveStore().Write(save);
|
||||||
|
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
|
||||||
|
return _config.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestorePlants(GameContent content)
|
||||||
|
{
|
||||||
|
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
|
||||||
|
foreach (var plant in _save!.Plants)
|
||||||
|
{
|
||||||
|
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
|
||||||
|
{
|
||||||
|
continue; // вид пропал (мод убрали) — пропускаем растение
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _plants.IndexOf(def);
|
||||||
|
var genome = plant.Genome is { Count: > 0 }
|
||||||
|
? new Genome(plant.Genome)
|
||||||
|
: _plants[index].Template.Generate(fallback);
|
||||||
|
PlantFactory.Create(
|
||||||
|
Store,
|
||||||
|
_plants,
|
||||||
|
index,
|
||||||
|
new Vector2(plant.X, plant.Y),
|
||||||
|
plant.AgeDays,
|
||||||
|
genome,
|
||||||
|
plant.CellFertility,
|
||||||
|
CellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
|
||||||
|
private bool[] BuildOccluders()
|
||||||
|
{
|
||||||
|
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
|
||||||
|
Store
|
||||||
|
.Query<PlantGrowth, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
|
||||||
|
{
|
||||||
|
var species = _plants[grow.Species];
|
||||||
|
if (
|
||||||
|
species.Def.TrunkRadiusCells <= 0f
|
||||||
|
|| grow.Stage < species.Stages.Length - 1
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return; // затеняют только зрелые деревья (со стволом)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cx = Math.Clamp(
|
||||||
|
(int)(transform.Position.X / CellSize),
|
||||||
|
0,
|
||||||
|
_config.Width - 1
|
||||||
|
);
|
||||||
|
var cy = Math.Clamp(
|
||||||
|
(int)(transform.Position.Y / CellSize),
|
||||||
|
0,
|
||||||
|
_config.Height - 1
|
||||||
|
);
|
||||||
|
_cellOccluder[cy * _config.Width + cx] = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return _cellOccluder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Switch(Scene scene)
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
_speed.Resume();
|
||||||
|
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
|
||||||
|
{
|
||||||
ContentCommands.Register(console, content, atlases);
|
ContentCommands.Register(console, content, atlases);
|
||||||
console.Register(
|
console.Register(
|
||||||
"regen",
|
"regen",
|
||||||
"regen [seed] — regenerate the world",
|
"regen [seed] — regenerate the world with a new seed",
|
||||||
(c, args) =>
|
(c, args) =>
|
||||||
{
|
{
|
||||||
if (Context.Scenes.IsTransitioning)
|
if (Context.Scenes.IsTransitioning)
|
||||||
@@ -128,71 +468,150 @@ public sealed class WorldScene : Scene
|
|||||||
|
|
||||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||||
c.WriteLine($"regenerating world, seed {seed}");
|
c.WriteLine($"regenerating world, seed {seed}");
|
||||||
Context.Scenes.Switch(new WorldScene(seed), Transition.Fade(0.6f));
|
Context.Scenes.Switch(
|
||||||
|
new WorldScene(_config with { Seed = seed }),
|
||||||
|
Transitions.Fade(0.6f)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.Register(
|
console.Register(
|
||||||
"terrain",
|
"formula",
|
||||||
"terrain [seed] — switch to the tile terrain scene",
|
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
|
||||||
(c, args) =>
|
(c, args) =>
|
||||||
{
|
{
|
||||||
if (Context.Scenes.IsTransitioning)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
|
c.WriteLine(
|
||||||
|
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seed = args.Length > 0 ? int.Parse(args[0]) : _seed;
|
var expression = string.Join(' ', args);
|
||||||
c.WriteLine($"switching to terrain scene, seed {seed}");
|
try
|
||||||
Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
var decisions = new PawnDecisionSystem();
|
|
||||||
UpdateSystems.Add(decisions);
|
|
||||||
UpdateSystems.Add(new WanderSystem(_seed, WorldBounds));
|
|
||||||
UpdateSystems.Add(new PawnNeedsSystem());
|
|
||||||
UpdateSystems.Add(new PawnAppearanceSystem());
|
|
||||||
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
|
|
||||||
UpdateSystems.Add(
|
|
||||||
new HudSystem(Context, content.Languages, hudLabel, "hud.world", _seed, Population)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.Register(
|
|
||||||
"ai",
|
|
||||||
"ai [energy] — utility scores at the given energy (0..1), plus the rest/wander split",
|
|
||||||
(c, args) =>
|
|
||||||
{
|
|
||||||
var energy =
|
|
||||||
args.Length > 0 && float.TryParse(args[0], out var e)
|
|
||||||
? Math.Clamp(e, 0f, 1f)
|
|
||||||
: 0.3f;
|
|
||||||
c.WriteLine($"utility scores at energy {energy:0.00}:");
|
|
||||||
foreach (var (name, score) in decisions.Inspect(energy))
|
|
||||||
{
|
{
|
||||||
c.WriteLine($" {name, -8} {score:0.000}");
|
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
|
||||||
|
}
|
||||||
|
catch (FormulaException e)
|
||||||
|
{
|
||||||
|
c.WriteLine($"error: {e.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var resting = 0;
|
|
||||||
var total = 0;
|
|
||||||
Store
|
|
||||||
.Query<PawnBrain>()
|
|
||||||
.ForEachEntity(
|
|
||||||
(ref PawnBrain brain, Entity _) =>
|
|
||||||
{
|
|
||||||
total++;
|
|
||||||
if (brain.Action == PawnAction.Rest)
|
|
||||||
{
|
|
||||||
resting++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
c.WriteLine($"population: {resting} resting / {total - resting} wandering");
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
console.Register(
|
||||||
Log.Info(
|
"gene",
|
||||||
$"World generated: seed {_seed}, {WorldWidth}x{WorldHeight} cells, population {Population}"
|
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
|
||||||
|
(c, args) => RunGeneDemo(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"plant",
|
||||||
|
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
||||||
|
(c, args) => RunPlantDemo(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"menu",
|
||||||
|
"menu — return to the main menu",
|
||||||
|
(_, _) => Switch(new MainMenuScene())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
|
||||||
|
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
|
||||||
|
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var genes = content.Defs.All<GeneDef>();
|
||||||
|
if (genes.Count == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("no Gene defs loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||||||
|
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||||
|
var random = new Random(seed);
|
||||||
|
|
||||||
|
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
|
||||||
|
var parentA = Genome.Generate(genes, random);
|
||||||
|
foreach (var gene in genes)
|
||||||
|
{
|
||||||
|
var allele = parentA[gene.DefName];
|
||||||
|
console.WriteLine(
|
||||||
|
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.WriteLine("traits:");
|
||||||
|
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
|
||||||
|
{
|
||||||
|
console.WriteLine($" {trait} = {value:0.###}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentB = Genome.Generate(genes, random);
|
||||||
|
var child = Genome.Breed(parentA, parentB, registry, random);
|
||||||
|
var childTraits = Phenotype.Compute(child, registry);
|
||||||
|
console.WriteLine(
|
||||||
|
$"bred child: {child.Alleles.Count} genes, "
|
||||||
|
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
|
||||||
|
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
|
||||||
|
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
|
||||||
|
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
|
||||||
|
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var species = content.Defs.NamesOf("Plant");
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
|
||||||
|
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
|
||||||
|
{
|
||||||
|
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _plants.IndexOf(def);
|
||||||
|
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||||
|
var genome = _plants[index].Template.Generate(new Random(seed));
|
||||||
|
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
|
||||||
|
|
||||||
|
console.WriteLine(
|
||||||
|
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
||||||
|
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (def.HarvestProduct is { } harvest)
|
||||||
|
{
|
||||||
|
console.WriteLine(
|
||||||
|
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||||||
|
{
|
||||||
|
var season = content.Languages.Get(
|
||||||
|
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.WriteLine(" barren (no fruit)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProductLabel(GameContent content, string productDefName) =>
|
||||||
|
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||||
|
? content.Languages.Get(product.Label)
|
||||||
|
: productDefName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Scenes;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Единая точка создания сущности-растения: спрайт стадии по возрасту, признаки из генома формулами
|
||||||
|
/// генов, тинт по морфе и компоненты роста/организма. Переиспользуется начальным скаттером,
|
||||||
|
/// размножением и загрузкой сейва — чтобы все растения собирались одинаково.
|
||||||
|
/// </summary>
|
||||||
|
public static class PlantFactory
|
||||||
|
{
|
||||||
|
// Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым.
|
||||||
|
private static readonly Color VariantTint = new(214, 150, 176);
|
||||||
|
|
||||||
|
// Якоря оттенка листвы: тёплый жёлто-зелёный → холодный сине-зелёный (по гену leafHue).
|
||||||
|
private static readonly Color LeafWarm = new(170, 200, 90);
|
||||||
|
private static readonly Color LeafCool = new(90, 170, 150);
|
||||||
|
|
||||||
|
/// <summary>Создаёт растение вида <paramref name="species"/> с заданным возрастом и геномом.</summary>
|
||||||
|
public static Entity Create(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
int species,
|
||||||
|
Vector2 position,
|
||||||
|
float ageDays,
|
||||||
|
Genome genome,
|
||||||
|
float cellFertility,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var sp = plants[species];
|
||||||
|
var stage = sp.StageAt(ageDays);
|
||||||
|
var resolved = sp.Stages[stage];
|
||||||
|
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, plants.GeneRegistry));
|
||||||
|
|
||||||
|
var sprite = new Sprite(resolved.Region, GameLayers.Beings);
|
||||||
|
sprite.CenterOrigin();
|
||||||
|
sprite.Color = Tint(traits);
|
||||||
|
|
||||||
|
return store.CreateEntity(
|
||||||
|
new Transform2D(
|
||||||
|
position,
|
||||||
|
scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width)
|
||||||
|
),
|
||||||
|
sprite,
|
||||||
|
new PlantGrowth
|
||||||
|
{
|
||||||
|
Species = species,
|
||||||
|
Stage = stage,
|
||||||
|
AgeDays = ageDays,
|
||||||
|
CellFertility = cellFertility,
|
||||||
|
},
|
||||||
|
new PlantOrganism { Genome = genome, Traits = traits },
|
||||||
|
new Fruiting()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Цвет растения из генов: лёгкий оттенок листвы (ген leafHue) поверх текстуры; рецессивная морфа
|
||||||
|
// дополнительно сдвигает тинт в свою сторону.
|
||||||
|
private static Color Tint(in PlantPhenotype traits)
|
||||||
|
{
|
||||||
|
var leaf = Color.Lerp(LeafWarm, LeafCool, Math.Clamp(traits.LeafHue, 0f, 1f));
|
||||||
|
var tint = Color.Lerp(Color.White, leaf, 0.35f);
|
||||||
|
return traits.IsVariant ? Color.Lerp(tint, VariantTint, 0.6f) : tint;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Плодоношение (фаза G4): у зрелого растения с признаком <see cref="PlantPhenotype.FruitYield"/> > 0
|
||||||
|
/// в его генетический сезон (<see cref="PlantPhenotype.FruitSeason"/>) копятся зрелые плоды до потолка
|
||||||
|
/// <see cref="PlantPhenotype.FruitYield"/>; вне сезона плоды опадают. Полностью управляется генами через
|
||||||
|
/// признаки — система лишь применяет их к компоненту <see cref="Fruiting"/>. Незрелые/бесплодные
|
||||||
|
/// растения остаются с нулём.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlantFruitingSystem(PlantSet plants, Calendar calendar, Climate climate)
|
||||||
|
: QuerySystem<PlantGrowth, PlantOrganism, Fruiting>
|
||||||
|
{
|
||||||
|
// За сколько игровых дней набирается/опадает полный урожай.
|
||||||
|
private const float RipenDays = 10f;
|
||||||
|
private const float DropDays = 20f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
|
||||||
|
if (deltaDays <= 0f)
|
||||||
|
{
|
||||||
|
return; // пауза
|
||||||
|
}
|
||||||
|
|
||||||
|
var season = (int)climate.Season;
|
||||||
|
|
||||||
|
foreach (var (growths, organisms, fruits, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var orgs = organisms.Span;
|
||||||
|
var f = fruits.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
ref var traits = ref orgs[i].Traits;
|
||||||
|
var mature = g[i].Stage >= plants[g[i].Species].Stages.Length - 1;
|
||||||
|
if (!mature || traits.FruitYield < 1f)
|
||||||
|
{
|
||||||
|
f[i].RipeFruit = 0f; // незрелое или бесплодное — плодов нет
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var ripe = ref f[i].RipeFruit;
|
||||||
|
if (season == traits.FruitSeason)
|
||||||
|
{
|
||||||
|
ripe = MathF.Min(
|
||||||
|
traits.FruitYield,
|
||||||
|
ripe + deltaDays * (traits.FruitYield / RipenDays)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ripe = MathF.Max(0f, ripe - deltaDays * (traits.FruitYield / DropDays));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние роста растения: индекс вида в <see cref="Content.PlantSet"/>, текущая стадия и
|
||||||
|
/// накопленный возраст в игровых днях. Плоские данные — переход между стадиями и смену спрайта
|
||||||
|
/// выполняет <see cref="PlantGrowthSystem"/>, читая пороги стадий из таблицы вида.
|
||||||
|
/// </summary>
|
||||||
|
public struct PlantGrowth : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Индекс вида растения в <see cref="Content.PlantSet"/>.</summary>
|
||||||
|
public int Species;
|
||||||
|
|
||||||
|
/// <summary>Текущая стадия роста (индекс в стадиях вида).</summary>
|
||||||
|
public int Stage;
|
||||||
|
|
||||||
|
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||||
|
public float AgeDays;
|
||||||
|
|
||||||
|
/// <summary>Плодородность клетки, на которой растёт растение — вход пригодности(плодородность).</summary>
|
||||||
|
public float CellFertility;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов
|
||||||
|
/// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) ·
|
||||||
|
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из
|
||||||
|
/// признаков (<see cref="PlantPhenotype"/>), вычисленных формулами генов. Свет локальный (лайтмап),
|
||||||
|
/// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному
|
||||||
|
/// возрасту как прежде. Терминальную стадию пропускает.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlantGrowthSystem(
|
||||||
|
PlantSet plants,
|
||||||
|
Calendar calendar,
|
||||||
|
Climate climate,
|
||||||
|
Lighting lighting,
|
||||||
|
int cellSize
|
||||||
|
) : QuerySystem<PlantGrowth, PlantOrganism, Sprite, Transform2D>
|
||||||
|
{
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
|
||||||
|
if (deltaDays <= 0f)
|
||||||
|
{
|
||||||
|
return; // пауза — рост стоит
|
||||||
|
}
|
||||||
|
|
||||||
|
// Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее).
|
||||||
|
var temperature = climate.Temperature;
|
||||||
|
|
||||||
|
foreach (var (growths, organisms, sprites, transforms, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var orgs = organisms.Span;
|
||||||
|
var s = sprites.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
ref var grow = ref g[i];
|
||||||
|
var stages = plants[grow.Species].Stages;
|
||||||
|
if (grow.Stage >= stages.Length - 1)
|
||||||
|
{
|
||||||
|
grow.AgeDays += deltaDays; // зрелое: старение в реальном времени → смерть по старости
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var traits = ref orgs[i].Traits;
|
||||||
|
var light = lighting.SampleAt(t[i].Position);
|
||||||
|
var rate =
|
||||||
|
traits.Vigor
|
||||||
|
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
|
||||||
|
* Suitability.Gaussian(
|
||||||
|
temperature,
|
||||||
|
traits.OptimalTemperature,
|
||||||
|
traits.TemperatureTolerance
|
||||||
|
)
|
||||||
|
* Suitability.Gaussian(
|
||||||
|
grow.CellFertility,
|
||||||
|
traits.OptimalFertility,
|
||||||
|
traits.FertilityTolerance
|
||||||
|
);
|
||||||
|
|
||||||
|
grow.AgeDays += deltaDays * rate;
|
||||||
|
var changed = false;
|
||||||
|
while (
|
||||||
|
grow.Stage < stages.Length - 1
|
||||||
|
&& grow.AgeDays >= stages[grow.Stage + 1].EnterDay
|
||||||
|
)
|
||||||
|
{
|
||||||
|
grow.Stage++;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
Apply(ref s[i], ref t[i], stages[grow.Stage]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Apply(ref Sprite sprite, ref Transform2D transform, in PlantSet.Stage stage)
|
||||||
|
{
|
||||||
|
sprite.Region = stage.Region;
|
||||||
|
sprite.Origin = new Vector2(stage.Region.Width / 2f, stage.Region.Height / 2f);
|
||||||
|
transform.Scale = new Vector2(cellSize * stage.SizeCells / stage.Region.Width);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается
|
||||||
|
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку
|
||||||
|
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с
|
||||||
|
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления
|
||||||
|
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в
|
||||||
|
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения
|
||||||
|
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlantLifecycleSystem : BaseSystem
|
||||||
|
{
|
||||||
|
private const float IntervalDays = 0.25f;
|
||||||
|
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly Calendar _calendar;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly int _width;
|
||||||
|
private readonly int _height;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly float[] _cellFertility;
|
||||||
|
private readonly bool[] _cellLand;
|
||||||
|
private readonly int _densityCap;
|
||||||
|
private readonly Random _rng;
|
||||||
|
private readonly ArchetypeQuery<PlantGrowth, PlantOrganism, Transform2D> _query;
|
||||||
|
|
||||||
|
private readonly int[] _count;
|
||||||
|
private readonly CellRep[] _rep;
|
||||||
|
private readonly List<Entity> _deaths = [];
|
||||||
|
private readonly List<Candidate> _candidates = [];
|
||||||
|
private readonly List<Birth> _births = [];
|
||||||
|
private float _accumulator;
|
||||||
|
|
||||||
|
public PlantLifecycleSystem(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
Calendar calendar,
|
||||||
|
GameClock clock,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int cellSize,
|
||||||
|
float[] cellFertility,
|
||||||
|
bool[] cellLand,
|
||||||
|
int densityCap,
|
||||||
|
int seed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_plants = plants;
|
||||||
|
_calendar = calendar;
|
||||||
|
_clock = clock;
|
||||||
|
_width = width;
|
||||||
|
_height = height;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_cellFertility = cellFertility;
|
||||||
|
_cellLand = cellLand;
|
||||||
|
_densityCap = densityCap;
|
||||||
|
_rng = new Random(seed);
|
||||||
|
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D>();
|
||||||
|
_count = new int[width * height];
|
||||||
|
_rep = new CellRep[width * height];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
_accumulator += _clock.DeltaTime / _calendar.SecondsPerDay; // прошло игровых дней (с учётом паузы/скорости)
|
||||||
|
if (_accumulator < IntervalDays)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsed = _accumulator;
|
||||||
|
_accumulator = 0f;
|
||||||
|
|
||||||
|
BuildGridAndCollect();
|
||||||
|
Reproduce(elapsed);
|
||||||
|
Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов.
|
||||||
|
private void BuildGridAndCollect()
|
||||||
|
{
|
||||||
|
Array.Clear(_count);
|
||||||
|
for (var c = 0; c < _rep.Length; c++)
|
||||||
|
{
|
||||||
|
_rep[c].Species = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deaths.Clear();
|
||||||
|
_candidates.Clear();
|
||||||
|
_births.Clear();
|
||||||
|
|
||||||
|
foreach (var (growths, organisms, transforms, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var orgs = organisms.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
ref var grow = ref g[i];
|
||||||
|
ref var org = ref orgs[i];
|
||||||
|
if (grow.AgeDays > org.Traits.Lifespan)
|
||||||
|
{
|
||||||
|
_deaths.Add(entities.EntityAt(i)); // умер от старости
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = t[i].Position;
|
||||||
|
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
||||||
|
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
||||||
|
var cell = cy * _width + cx;
|
||||||
|
_count[cell]++;
|
||||||
|
|
||||||
|
var mature = grow.Stage >= _plants[grow.Species].Stages.Length - 1;
|
||||||
|
_rep[cell] = new CellRep
|
||||||
|
{
|
||||||
|
Species = grow.Species,
|
||||||
|
Mature = mature,
|
||||||
|
Genome = org.Genome,
|
||||||
|
MutationRate = org.Traits.MutationRate,
|
||||||
|
};
|
||||||
|
if (mature)
|
||||||
|
{
|
||||||
|
_candidates.Add(
|
||||||
|
new Candidate
|
||||||
|
{
|
||||||
|
Cell = cell,
|
||||||
|
Species = grow.Species,
|
||||||
|
Genome = org.Genome,
|
||||||
|
Traits = org.Traits,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проход 2: зрелые сеют потомка (перекрёстно или само-) в подходящую клетку радиуса.
|
||||||
|
private void Reproduce(float elapsed)
|
||||||
|
{
|
||||||
|
foreach (var cand in _candidates)
|
||||||
|
{
|
||||||
|
var interval = MathF.Max(0.5f, cand.Traits.ReproduceInterval);
|
||||||
|
if (_rng.NextSingle() >= elapsed / interval)
|
||||||
|
{
|
||||||
|
continue; // в этот тик не сеет
|
||||||
|
}
|
||||||
|
|
||||||
|
var range = Math.Clamp((int)MathF.Round(cand.Traits.DispersalRange), 1, 6);
|
||||||
|
var cx = cand.Cell % _width;
|
||||||
|
var cy = cand.Cell / _width;
|
||||||
|
|
||||||
|
if (!TryFindPartner(cx, cy, range, cand.Species, out var partner))
|
||||||
|
{
|
||||||
|
if (_rng.NextSingle() >= cand.Traits.SelfPollination)
|
||||||
|
{
|
||||||
|
continue; // нет партнёра и самоопыление не удалось
|
||||||
|
}
|
||||||
|
|
||||||
|
// самоопыление — партнёр это сам кандидат
|
||||||
|
partner = new Partner
|
||||||
|
{
|
||||||
|
Genome = cand.Genome,
|
||||||
|
MutationRate = cand.Traits.MutationRate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryPickTarget(cx, cy, range, out var tcell))
|
||||||
|
{
|
||||||
|
continue; // некуда сеять (нет суши/свободного места рядом)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Темп мутаций потомка — средний эволюционирующий признак родителей (ген управляет мутацией).
|
||||||
|
var mutationChance = (cand.Traits.MutationRate + partner.MutationRate) * 0.5f;
|
||||||
|
var child = Genome.Breed(
|
||||||
|
cand.Genome,
|
||||||
|
partner.Genome,
|
||||||
|
_plants.GeneRegistry,
|
||||||
|
_rng,
|
||||||
|
mutationChance
|
||||||
|
);
|
||||||
|
var tx = tcell % _width;
|
||||||
|
var ty = tcell / _width;
|
||||||
|
var position = new Vector2(
|
||||||
|
(tx + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize,
|
||||||
|
(ty + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize
|
||||||
|
);
|
||||||
|
_births.Add(
|
||||||
|
new Birth
|
||||||
|
{
|
||||||
|
Species = cand.Species,
|
||||||
|
Position = position,
|
||||||
|
Genome = child,
|
||||||
|
Fertility = _cellFertility[tcell],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
_count[tcell]++; // резервируем место, чтобы не переполнить клетку за тик
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFindPartner(int cx, int cy, int range, int species, out Partner partner)
|
||||||
|
{
|
||||||
|
for (var dy = -range; dy <= range; dy++)
|
||||||
|
{
|
||||||
|
for (var dx = -range; dx <= range; dx++)
|
||||||
|
{
|
||||||
|
if (dx == 0 && dy == 0)
|
||||||
|
{
|
||||||
|
continue; // партнёр — другая клетка (приоритет перекрёстного)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nx = cx + dx;
|
||||||
|
var ny = cy + dy;
|
||||||
|
if (nx < 0 || nx >= _width || ny < 0 || ny >= _height)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var rep = ref _rep[ny * _width + nx];
|
||||||
|
if (rep.Species == species && rep.Mature)
|
||||||
|
{
|
||||||
|
partner = new Partner { Genome = rep.Genome, MutationRate = rep.MutationRate };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partner = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryPickTarget(int cx, int cy, int range, out int cell)
|
||||||
|
{
|
||||||
|
for (var attempt = 0; attempt < 6; attempt++)
|
||||||
|
{
|
||||||
|
var tx = cx + _rng.Next(-range, range + 1);
|
||||||
|
var ty = cy + _rng.Next(-range, range + 1);
|
||||||
|
if (tx < 0 || tx >= _width || ty < 0 || ty >= _height)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = ty * _width + tx;
|
||||||
|
if (_cellLand[candidate] && _count[candidate] < _densityCap)
|
||||||
|
{
|
||||||
|
cell = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cell = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Apply()
|
||||||
|
{
|
||||||
|
foreach (var entity in _deaths)
|
||||||
|
{
|
||||||
|
entity.DeleteEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var birth in _births)
|
||||||
|
{
|
||||||
|
PlantFactory.Create(
|
||||||
|
_store,
|
||||||
|
_plants,
|
||||||
|
birth.Species,
|
||||||
|
birth.Position,
|
||||||
|
ageDays: 0f,
|
||||||
|
birth.Genome,
|
||||||
|
birth.Fertility,
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CellRep
|
||||||
|
{
|
||||||
|
public int Species;
|
||||||
|
public bool Mature;
|
||||||
|
public Genome Genome;
|
||||||
|
public float MutationRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Candidate
|
||||||
|
{
|
||||||
|
public int Cell;
|
||||||
|
public int Species;
|
||||||
|
public Genome Genome;
|
||||||
|
public PlantPhenotype Traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Partner
|
||||||
|
{
|
||||||
|
public Genome Genome;
|
||||||
|
public float MutationRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Birth
|
||||||
|
{
|
||||||
|
public int Species;
|
||||||
|
public Vector2 Position;
|
||||||
|
public Genome Genome;
|
||||||
|
public float Fertility;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>genes.json</c>):
|
||||||
|
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
|
||||||
|
/// Имена полей соответствуют именам признаков в эффектах генов.
|
||||||
|
/// </summary>
|
||||||
|
public struct PlantPhenotype
|
||||||
|
{
|
||||||
|
public float OptimalLight;
|
||||||
|
public float LightTolerance;
|
||||||
|
public float OptimalTemperature;
|
||||||
|
public float TemperatureTolerance;
|
||||||
|
public float OptimalFertility;
|
||||||
|
public float FertilityTolerance;
|
||||||
|
public float Vigor;
|
||||||
|
public float Lifespan;
|
||||||
|
public float DispersalRange;
|
||||||
|
public float ReproduceInterval;
|
||||||
|
public float SelfPollination;
|
||||||
|
public float MutationRate;
|
||||||
|
|
||||||
|
// --- Контент (G4): плодоношение, добыча, цвет ---
|
||||||
|
public float FruitYield;
|
||||||
|
public int FruitSeason;
|
||||||
|
public float HarvestAmount;
|
||||||
|
public float LeafHue;
|
||||||
|
|
||||||
|
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
||||||
|
public bool IsVariant;
|
||||||
|
|
||||||
|
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||||
|
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
||||||
|
{
|
||||||
|
float T(string name) => traits.GetValueOrDefault(name);
|
||||||
|
return new PlantPhenotype
|
||||||
|
{
|
||||||
|
OptimalLight = T("optimalLight"),
|
||||||
|
LightTolerance = T("lightTolerance"),
|
||||||
|
OptimalTemperature = T("optimalTemperature"),
|
||||||
|
TemperatureTolerance = T("temperatureTolerance"),
|
||||||
|
OptimalFertility = T("optimalFertility"),
|
||||||
|
FertilityTolerance = T("fertilityTolerance"),
|
||||||
|
Vigor = T("vigor"),
|
||||||
|
Lifespan = T("lifespan"),
|
||||||
|
DispersalRange = T("dispersalRange"),
|
||||||
|
ReproduceInterval = T("reproduceInterval"),
|
||||||
|
SelfPollination = T("selfPollination"),
|
||||||
|
MutationRate = T("mutationRate"),
|
||||||
|
FruitYield = T("fruitYield"),
|
||||||
|
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
|
||||||
|
HarvestAmount = T("harvestAmount"),
|
||||||
|
LeafHue = T("leafHue"),
|
||||||
|
IsVariant = T("variant") >= 0.5f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Компонент-организм растения: его управляемый <see cref="Genome"/> (для размножения, сейва и
|
||||||
|
/// пересчёта) и выраженный фенотип <see cref="Traits"/>, по которому работают системы. Заменяет
|
||||||
|
/// прежний фиксированный <c>PlantGenome</c> — теперь геном переменного состава и признаки из формул
|
||||||
|
/// генов (фаза G3). Геном — ссылочный объект, поэтому компонент несёт ссылку, а не копию.
|
||||||
|
/// </summary>
|
||||||
|
public struct PlantOrganism : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Геном особи (аллели по генам); общий источник истины для признаков и размножения.</summary>
|
||||||
|
public Genome Genome;
|
||||||
|
|
||||||
|
/// <summary>Выраженные признаки — то, что читают системы роста/жизненного цикла.</summary>
|
||||||
|
public PlantPhenotype Traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его
|
||||||
|
/// сезон плодоношения (признак <see cref="PlantPhenotype.FruitSeason"/>) до потолка
|
||||||
|
/// <see cref="PlantPhenotype.FruitYield"/>, вне сезона опадает. Управляется <c>PlantFruitingSystem</c>.
|
||||||
|
/// </summary>
|
||||||
|
public struct Fruiting : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
||||||
|
public float RipeFruit;
|
||||||
|
}
|
||||||
@@ -14,6 +14,12 @@ public struct Wander : IComponent
|
|||||||
public float ChangeIn;
|
public float ChangeIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Имя дефа жителя — нужно сохранению, чтобы при загрузке пересобрать спрайт из атласа.</summary>
|
||||||
|
public struct PawnId : IComponent
|
||||||
|
{
|
||||||
|
public string DefName;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Блуждание жителей. Один Random на систему, сид от мира — при одинаковом сиде
|
/// Блуждание жителей. Один Random на систему, сид от мира — при одинаковом сиде
|
||||||
/// и порядке обновления симуляция воспроизводима. Отдыхающие жители (см.
|
/// и порядке обновления симуляция воспроизводима. Отдыхающие жители (см.
|
||||||
@@ -110,16 +116,22 @@ public sealed class SeparationSystem(MrGameEng.Collisions.CollisionWorld world)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Камера бога: WASD/стрелки — панорамирование, колесо — зум. Молчит, пока открыта консоль.</summary>
|
/// <summary>
|
||||||
|
/// Камера бога: WASD/стрелки — панорамирование, колесо — зум к курсору (мировая точка под мышью
|
||||||
|
/// остаётся под мышью). Молчит, пока открыта консоль или активен <paramref name="blocked"/>
|
||||||
|
/// (например, открыто меню-пауза).
|
||||||
|
/// </summary>
|
||||||
public sealed class GodCameraSystem(
|
public sealed class GodCameraSystem(
|
||||||
Entity cameraEntity,
|
Entity cameraEntity,
|
||||||
InputManager input,
|
InputManager input,
|
||||||
MrGameEng.DevConsole.DevConsole console
|
Renderer2D renderer,
|
||||||
|
MrGameEng.DevConsole.DevConsole console,
|
||||||
|
Func<bool>? blocked = null
|
||||||
) : BaseSystem
|
) : BaseSystem
|
||||||
{
|
{
|
||||||
protected override void OnUpdateGroup()
|
protected override void OnUpdateGroup()
|
||||||
{
|
{
|
||||||
if (console.IsOpen)
|
if (console.IsOpen || blocked?.Invoke() == true)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -136,7 +148,20 @@ public sealed class GodCameraSystem(
|
|||||||
camera.Position += pan * (500f / camera.Zoom) * Tick.deltaTime;
|
camera.Position += pan * (500f / camera.Zoom) * Tick.deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
camera.Zoom = Math.Clamp(camera.Zoom * (1f + input.WheelDelta * 0.001f), 0.4f, 8f);
|
if (input.WheelDelta != 0)
|
||||||
|
{
|
||||||
|
var oldZoom = camera.Zoom;
|
||||||
|
var newZoom = Math.Clamp(oldZoom * (1f + input.WheelDelta * 0.001f), 0.4f, 8f);
|
||||||
|
if (newZoom != oldZoom)
|
||||||
|
{
|
||||||
|
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
|
||||||
|
var anchor = renderer.ScreenToWorld(
|
||||||
|
new Vector2(input.MousePosition.X, input.MousePosition.Y)
|
||||||
|
);
|
||||||
|
camera.Zoom = newZoom;
|
||||||
|
camera.Position = anchor - (anchor - camera.Position) * (oldZoom / newZoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float Axis(
|
private static float Axis(
|
||||||
@@ -151,8 +176,8 @@ public sealed class GodCameraSystem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, населением и FPS)
|
/// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, датой/временем календаря и
|
||||||
/// и подсказка управления. Обновляется 4 раза в секунду; смена языка подхватывается сама.
|
/// FPS) и подсказка управления. Обновляется 4 раза в секунду; смена языка подхватывается сама.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class HudSystem(
|
public sealed class HudSystem(
|
||||||
MrGameEng.Core.EngineContext context,
|
MrGameEng.Core.EngineContext context,
|
||||||
@@ -160,7 +185,8 @@ public sealed class HudSystem(
|
|||||||
Myra.Graphics2D.UI.Label label,
|
Myra.Graphics2D.UI.Label label,
|
||||||
string titleKey,
|
string titleKey,
|
||||||
int seed,
|
int seed,
|
||||||
int population
|
MrGameEng.Core.Calendar? calendar = null,
|
||||||
|
MrGameEng.Core.Climate? climate = null
|
||||||
) : BaseSystem
|
) : BaseSystem
|
||||||
{
|
{
|
||||||
private float _accumulated;
|
private float _accumulated;
|
||||||
@@ -178,9 +204,18 @@ public sealed class HudSystem(
|
|||||||
var fps = (int)MathF.Round(_frames / _accumulated);
|
var fps = (int)MathF.Round(_frames / _accumulated);
|
||||||
_accumulated = 0f;
|
_accumulated = 0f;
|
||||||
_frames = 0;
|
_frames = 0;
|
||||||
|
var when = calendar is null
|
||||||
|
? ""
|
||||||
|
: languages.Format("hud.datetime", calendar.Day, calendar.Hour, calendar.Minute);
|
||||||
|
if (climate is not null)
|
||||||
|
{
|
||||||
|
var season = languages.Get("season." + climate.Season.ToString().ToLowerInvariant());
|
||||||
|
when +=
|
||||||
|
" | "
|
||||||
|
+ languages.Format("hud.climate", season, (int)MathF.Round(climate.Temperature));
|
||||||
|
}
|
||||||
|
|
||||||
label.Text =
|
label.Text =
|
||||||
languages.Format(titleKey, seed, population, fps)
|
languages.Format(titleKey, seed, when, fps) + "\n" + languages.Get("hud.controls");
|
||||||
+ "\n"
|
|
||||||
+ languages.Get("hud.controls");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,33 @@
|
|||||||
|
using MrGameEng.WorldGen;
|
||||||
|
|
||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Детерминированная генерация рельефа: случайное поле, сглаженное несколькими проходами.
|
/// Игровой адаптер процедурной генерации: превращает параметры мира в настройки движкового
|
||||||
/// Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой симуляции.
|
/// <see cref="HeightmapGenerator"/> (шум Перлина → fBm → island-маска) и отдаёт нормализованную
|
||||||
/// Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
|
/// карту высот. Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой
|
||||||
|
/// симуляции. Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WorldGenerator
|
public static class WorldGenerator
|
||||||
{
|
{
|
||||||
public static float[,] GenerateHeights(int width, int height, int seed)
|
/// <summary>
|
||||||
|
/// Строит нормализованную карту высот <paramref name="width"/>×<paramref name="height"/> из
|
||||||
|
/// <paramref name="seed"/>. <paramref name="detail"/> (1..8, из меню «масштаб деталей»/сейва)
|
||||||
|
/// задаёт размер «континентов»: больше — крупнее и плавнее суша (ниже частота шума).
|
||||||
|
/// </summary>
|
||||||
|
public static Heightmap Generate(int width, int height, int seed, int detail = 4)
|
||||||
{
|
{
|
||||||
var random = new Random(seed);
|
var scale = Math.Clamp(detail, 1, 8);
|
||||||
var heights = new float[width, height];
|
var settings = new HeightmapSettings
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
{
|
||||||
for (var y = 0; y < height; y++)
|
Seed = seed,
|
||||||
{
|
Frequency = 6f / scale, // больше «масштаба деталей» → ниже частота → крупнее континенты
|
||||||
heights[x, y] = random.NextSingle();
|
Octaves = 5,
|
||||||
}
|
Persistence = 0.5f,
|
||||||
}
|
Lacunarity = 2f,
|
||||||
|
IslandStrength = 0.55f, // края мира уходят под воду — мир выглядит как континент
|
||||||
// Сглаживание усреднением окрестности 3×3 — дёшево и даёт связные "континенты".
|
IslandPower = 3f,
|
||||||
for (var pass = 0; pass < 4; pass++)
|
};
|
||||||
{
|
return new HeightmapGenerator(settings).Generate(width, height);
|
||||||
var smoothed = new float[width, height];
|
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
var sum = 0f;
|
|
||||||
var count = 0;
|
|
||||||
for (var dx = -1; dx <= 1; dx++)
|
|
||||||
{
|
|
||||||
for (var dy = -1; dy <= 1; dy++)
|
|
||||||
{
|
|
||||||
var nx = x + dx;
|
|
||||||
var ny = y + dy;
|
|
||||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
|
|
||||||
{
|
|
||||||
sum += heights[nx, ny];
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
smoothed[x, y] = sum / count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
heights = smoothed;
|
|
||||||
}
|
|
||||||
|
|
||||||
Normalize(heights, width, height);
|
|
||||||
return heights;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Normalize(float[,] heights, int width, int height)
|
|
||||||
{
|
|
||||||
var min = float.MaxValue;
|
|
||||||
var max = float.MinValue;
|
|
||||||
foreach (var value in heights)
|
|
||||||
{
|
|
||||||
min = Math.Min(min, value);
|
|
||||||
max = Math.Max(max, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var range = Math.Max(0.0001f, max - min);
|
|
||||||
for (var x = 0; x < width; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
heights[x, y] = (heights[x, y] - min) / range;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Общие фабрики виджетов Myra для экранов меню — единый стиль и отсутствие дублирования
|
||||||
|
/// разметки между сценами. Все подписи приходят готовыми строками (локализованными вызывающим).
|
||||||
|
/// </summary>
|
||||||
|
internal static class Ui
|
||||||
|
{
|
||||||
|
/// <summary>Акцентный цвет заголовков и активных элементов.</summary>
|
||||||
|
public static readonly Color Accent = new(120, 200, 255);
|
||||||
|
|
||||||
|
/// <summary>Приглушённый цвет вторичного текста.</summary>
|
||||||
|
public static readonly Color Muted = new(150, 160, 175);
|
||||||
|
|
||||||
|
private const int ButtonWidth = 280;
|
||||||
|
|
||||||
|
/// <summary>Крупный заголовок по центру.</summary>
|
||||||
|
public static Label Title(string text) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
TextColor = Accent,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Подпись вторичным цветом по центру.</summary>
|
||||||
|
public static Label Subtitle(string text) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
TextColor = Muted,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Кнопка фиксированной ширины с обработчиком клика.</summary>
|
||||||
|
public static TextButton Button(string text, Action onClick, int width = ButtonWidth)
|
||||||
|
{
|
||||||
|
var button = new TextButton
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
Width = width,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
button.Click += (_, _) => onClick();
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Вертикальная колонка по центру экрана.</summary>
|
||||||
|
public static VerticalStackPanel Column(int spacing = 10) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Spacing = spacing,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Горизонтальный ряд.</summary>
|
||||||
|
public static HorizontalStackPanel Row(int spacing = 8) =>
|
||||||
|
new() { Spacing = spacing, HorizontalAlignment = HorizontalAlignment.Center };
|
||||||
|
|
||||||
|
/// <summary>Оборачивает содержимое в панель на весь экран (абсолютное позиционирование детей).</summary>
|
||||||
|
public static Panel Screen(params Widget[] children)
|
||||||
|
{
|
||||||
|
var panel = new Panel();
|
||||||
|
foreach (var child in children)
|
||||||
|
{
|
||||||
|
panel.Widgets.Add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Минимальная система, вызывающая колбэк каждый кадр обновления. Сцены меню используют её
|
||||||
|
/// для опроса ввода (Esc/горячие клавиши) без отдельного класса-системы на каждую сцену.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CallbackSystem(Action update) : BaseSystem
|
||||||
|
{
|
||||||
|
protected override void OnUpdateGroup() => update();
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using LittleSim.App;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Конструктор панели настроек (язык, полный экран, VSync, разрешение, громкость).
|
||||||
|
/// Возвращает виджет Myra, работающий с переданной копией <see cref="GameSettings"/>; общий
|
||||||
|
/// для сцены настроек и оверлея паузы. Вместо неоднозначных комбо/чекбоксов — кнопки-сегменты
|
||||||
|
/// и тумблеры (состояние показывает цвет текста), что переносимо между версиями Myra.
|
||||||
|
/// </summary>
|
||||||
|
internal static class SettingsPanel
|
||||||
|
{
|
||||||
|
private static readonly (int W, int H)[] Resolutions =
|
||||||
|
{
|
||||||
|
(1280, 720),
|
||||||
|
(1600, 900),
|
||||||
|
(1920, 1080),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Строит панель. <paramref name="onApply"/>/<paramref name="onBack"/> — кнопки внизу.</summary>
|
||||||
|
public static Widget Build(
|
||||||
|
GameSettings settings,
|
||||||
|
LanguageManager lang,
|
||||||
|
Action onApply,
|
||||||
|
Action onBack
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var refreshers = new List<Action>();
|
||||||
|
var column = Ui.Column(10);
|
||||||
|
|
||||||
|
column.Widgets.Add(Ui.Title(lang.Get("settings.title")));
|
||||||
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
|
|
||||||
|
// Язык — по одному сегменту на доступный язык.
|
||||||
|
column.Widgets.Add(
|
||||||
|
Segment(
|
||||||
|
lang.Get("settings.language"),
|
||||||
|
lang.AvailableLanguages,
|
||||||
|
code => code.ToUpperInvariant(),
|
||||||
|
() => settings.Language,
|
||||||
|
code => settings.Language = code,
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Полный экран и VSync — тумблеры (цвет = состояние).
|
||||||
|
column.Widgets.Add(
|
||||||
|
Toggle(
|
||||||
|
lang.Get("settings.fullscreen"),
|
||||||
|
() => settings.Fullscreen,
|
||||||
|
v => settings.Fullscreen = v,
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
column.Widgets.Add(
|
||||||
|
Toggle(
|
||||||
|
lang.Get("settings.vsync"),
|
||||||
|
() => settings.VSync,
|
||||||
|
v => settings.VSync = v,
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Разрешение — сегменты пресетов.
|
||||||
|
var resolutions = new List<(int, int)>(Resolutions);
|
||||||
|
column.Widgets.Add(
|
||||||
|
Segment(
|
||||||
|
lang.Get("settings.resolution"),
|
||||||
|
resolutions,
|
||||||
|
r => $"{r.Item1}×{r.Item2}",
|
||||||
|
() => (settings.Width, settings.Height),
|
||||||
|
r =>
|
||||||
|
{
|
||||||
|
settings.Width = r.Item1;
|
||||||
|
settings.Height = r.Item2;
|
||||||
|
},
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Громкость — кнопки −/+ с подписью процента.
|
||||||
|
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
|
||||||
|
|
||||||
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
|
var buttons = Ui.Row(10);
|
||||||
|
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));
|
||||||
|
buttons.Widgets.Add(Ui.Button(lang.Get("settings.back"), onBack, width: 150));
|
||||||
|
column.Widgets.Add(buttons);
|
||||||
|
|
||||||
|
foreach (var refresh in refreshers)
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Widget Segment<T>(
|
||||||
|
string label,
|
||||||
|
IReadOnlyList<T> options,
|
||||||
|
Func<T, string> text,
|
||||||
|
Func<T> get,
|
||||||
|
Action<T> set,
|
||||||
|
List<Action> refreshers
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var row = Ui.Row(6);
|
||||||
|
row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted });
|
||||||
|
foreach (var option in options)
|
||||||
|
{
|
||||||
|
var value = option;
|
||||||
|
var button = new TextButton { Text = text(value) };
|
||||||
|
button.Click += (_, _) => set(value);
|
||||||
|
row.Widgets.Add(button);
|
||||||
|
refreshers.Add(() =>
|
||||||
|
button.TextColor = EqualityComparer<T>.Default.Equals(get(), value)
|
||||||
|
? Ui.Accent
|
||||||
|
: Ui.Muted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Widget Toggle(
|
||||||
|
string label,
|
||||||
|
Func<bool> get,
|
||||||
|
Action<bool> set,
|
||||||
|
List<Action> refreshers
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var button = new TextButton();
|
||||||
|
button.Click += (_, _) => set(!get());
|
||||||
|
refreshers.Add(() =>
|
||||||
|
{
|
||||||
|
button.Text = (get() ? "● " : "○ ") + label;
|
||||||
|
button.TextColor = get() ? Ui.Accent : Ui.Muted;
|
||||||
|
});
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Widget Volume(string label, GameSettings settings, List<Action> refreshers)
|
||||||
|
{
|
||||||
|
var row = Ui.Row(6);
|
||||||
|
var caption = new Label { TextColor = Ui.Muted };
|
||||||
|
refreshers.Add(() => caption.Text = $"{label}: {(int)MathF.Round(settings.Volume * 100)}%");
|
||||||
|
|
||||||
|
void Adjust(float delta)
|
||||||
|
{
|
||||||
|
settings.Volume = Math.Clamp(settings.Volume + delta, 0f, 1f);
|
||||||
|
foreach (var refresh in refreshers)
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var minus = new TextButton { Text = "−" };
|
||||||
|
minus.Click += (_, _) => Adjust(-0.1f);
|
||||||
|
var plus = new TextButton { Text = "+" };
|
||||||
|
plus.Click += (_, _) => Adjust(+0.1f);
|
||||||
|
|
||||||
|
row.Widgets.Add(caption);
|
||||||
|
row.Widgets.Add(minus);
|
||||||
|
row.Widgets.Add(plus);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кнопка с текстовой подписью. В Myra 1.6 нет отдельного TextButton: <see cref="Button"/> несёт
|
||||||
|
/// произвольный <see cref="Button.Content"/>, поэтому оборачиваем <see cref="Label"/> и проксируем
|
||||||
|
/// к нему <see cref="Text"/> и <see cref="TextColor"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TextButton : Button
|
||||||
|
{
|
||||||
|
private readonly Label _label = new();
|
||||||
|
|
||||||
|
/// <summary>Создаёт кнопку с пустой подписью.</summary>
|
||||||
|
public TextButton() => Content = _label;
|
||||||
|
|
||||||
|
/// <summary>Текст подписи.</summary>
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get => _label.Text;
|
||||||
|
set => _label.Text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Цвет подписи.</summary>
|
||||||
|
public Color TextColor
|
||||||
|
{
|
||||||
|
get => _label.TextColor;
|
||||||
|
set => _label.TextColor = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user