Merge main (gene system G1-G5, phases C-D) into the multiplayer branch

Main independently adopted the engine Core/Host split in G1, so the
scene adaptations conflicted with identical changes here — resolved in
main's favor (its scenes also carry the gene-driven world). Kept from
this branch: BootScene's --connect path into MultiplayerScene, the
buildAtlases switch in GameContent.Load (with the Task.Run lambda fix
its optional parameter requires), net.* localization keys, and the
Net/Server/Web projects in the solution. Program.cs now has both CLI
modes: --check-content (main) and --connect (here). The engine pointer
lands on d044caf, which linearly contains both lines of work.

Verified on the merged tree: 366 engine tests pass, --check-content
reports genes/traits intact, and a live --listen server replicates
moving pawns to the probe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-13 04:23:51 +03:00
co-authored by Claude Fable 5
31 changed files with 2178 additions and 1150 deletions
+84 -84
View File
@@ -41,17 +41,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "engine\src\MrGameEng.Host\MrGameEng.Host.csproj", "{D4222C26-40D8-4465-9B26-CD6BD0722EC0}" 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.Host.Tests", "engine\tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{41F0249B-106E-4D56-B022-73D2C2D74807}" 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}") = "LittleSim.Server", "src\LittleSim.Server\LittleSim.Server.csproj", "{BEFB468B-6784-468E-9B60-EB44AB078D15}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net", "engine\src\MrGameEng.Net\MrGameEng.Net.csproj", "{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net", "engine\src\MrGameEng.Net\MrGameEng.Net.csproj", "{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net.Tests", "engine\tests\MrGameEng.Net.Tests\MrGameEng.Net.Tests.csproj", "{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net.Tests", "engine\tests\MrGameEng.Net.Tests\MrGameEng.Net.Tests.csproj", "{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Server", "src\LittleSim.Server\LittleSim.Server.csproj", "{95630217-333B-4263-84AE-5EE5961FC307}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Web", "src\LittleSim.Web\LittleSim.Web.csproj", "{270A9E52-E0E3-4885-B662-79E35AF6F7B4}" 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
@@ -243,78 +243,78 @@ Global
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x64.ActiveCfg = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.ActiveCfg = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x64.Build.0 = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.Build.0 = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x86.ActiveCfg = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.ActiveCfg = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x86.Build.0 = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.Build.0 = Debug|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|Any CPU.Build.0 = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.Build.0 = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x64.ActiveCfg = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.ActiveCfg = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x64.Build.0 = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.Build.0 = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x86.ActiveCfg = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.ActiveCfg = Release|Any CPU
{D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x86.Build.0 = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.Build.0 = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x64.ActiveCfg = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.ActiveCfg = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x64.Build.0 = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.Build.0 = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x86.ActiveCfg = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.ActiveCfg = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x86.Build.0 = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.Build.0 = Debug|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|Any CPU.Build.0 = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.Build.0 = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x64.ActiveCfg = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.ActiveCfg = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x64.Build.0 = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.Build.0 = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x86.ActiveCfg = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.ActiveCfg = Release|Any CPU
{41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x86.Build.0 = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.Build.0 = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|Any CPU.Build.0 = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x64.ActiveCfg = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.ActiveCfg = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x64.Build.0 = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.Build.0 = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x86.ActiveCfg = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.ActiveCfg = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x86.Build.0 = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.Build.0 = Debug|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|Any CPU.ActiveCfg = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|Any CPU.Build.0 = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.Build.0 = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x64.ActiveCfg = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.ActiveCfg = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x64.Build.0 = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.Build.0 = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x86.ActiveCfg = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.ActiveCfg = Release|Any CPU
{BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x86.Build.0 = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.Build.0 = Release|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|Any CPU.Build.0 = Debug|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x64.ActiveCfg = Debug|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x64.ActiveCfg = Debug|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x64.Build.0 = Debug|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x64.Build.0 = Debug|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x86.ActiveCfg = Debug|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x86.ActiveCfg = Debug|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x86.Build.0 = Debug|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x86.Build.0 = Debug|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|Any CPU.Build.0 = Release|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|Any CPU.Build.0 = Release|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x64.ActiveCfg = Release|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x64.ActiveCfg = Release|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x64.Build.0 = Release|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x64.Build.0 = Release|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x86.ActiveCfg = Release|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x86.ActiveCfg = Release|Any CPU
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x86.Build.0 = Release|Any CPU {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x86.Build.0 = Release|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Debug|Any CPU.Build.0 = Debug|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x64.ActiveCfg = Debug|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Debug|x64.ActiveCfg = Debug|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x64.Build.0 = Debug|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Debug|x64.Build.0 = Debug|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x86.ActiveCfg = Debug|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Debug|x86.ActiveCfg = Debug|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x86.Build.0 = Debug|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Debug|x86.Build.0 = Debug|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Release|Any CPU.ActiveCfg = Release|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|Any CPU.Build.0 = Release|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Release|Any CPU.Build.0 = Release|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x64.ActiveCfg = Release|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Release|x64.ActiveCfg = Release|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x64.Build.0 = Release|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Release|x64.Build.0 = Release|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.ActiveCfg = Release|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Release|x86.ActiveCfg = Release|Any CPU
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.Build.0 = Release|Any CPU {95630217-333B-4263-84AE-5EE5961FC307}.Release|x86.Build.0 = Release|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x64.ActiveCfg = Debug|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x64.ActiveCfg = Debug|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x64.Build.0 = Debug|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x64.Build.0 = Debug|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x86.ActiveCfg = Debug|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x86.ActiveCfg = Debug|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x86.Build.0 = Debug|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x86.Build.0 = Debug|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|Any CPU.Build.0 = Release|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|Any CPU.Build.0 = Release|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x64.ActiveCfg = Release|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x64.ActiveCfg = Release|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x64.Build.0 = Release|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x64.Build.0 = Release|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x86.ActiveCfg = Release|Any CPU {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x86.ActiveCfg = Release|Any CPU
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x86.Build.0 = 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
@@ -337,11 +337,11 @@ Global
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{D4222C26-40D8-4465-9B26-CD6BD0722EC0} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{41F0249B-106E-4D56-B022-73D2C2D74807} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{BEFB468B-6784-468E-9B60-EB44AB078D15} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {841EAE51-E86B-4BFB-A70C-A67E4D492DD7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {95630217-333B-4263-84AE-5EE5961FC307} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{270A9E52-E0E3-4885-B662-79E35AF6F7B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {1FF0DF78-22E4-4169-A9DC-233FFAEA5B14} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+73
View File
@@ -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" } }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"type": "Patch",
// Контент-патчи (фаза G5): применяются ко всем дефам типа defType, чьё имя подходит под regex match,
// проставляя поля set. Демонстрация: даём кустам древесину при сборе (веточки).
"patches": [
{ "defType": "Plant", "match": "Bush.*", "set": { "harvestProduct": "ProductWood" } }
]
}
+15 -3
View File
@@ -2,8 +2,12 @@
"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, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0, "spread": 0.08 }, "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": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" }, { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" }, { "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
@@ -15,16 +19,24 @@
{ "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, "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3, "spread": 0.1 }, "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": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
{ "sizeCells": 1.2, "label": "plant.stage.mature" } { "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, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13,
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0, "spread": 0.1 }, "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": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
{ "sizeCells": 1.4, "label": "plant.stage.mature" } { "sizeCells": 1.4, "label": "plant.stage.mature" }
+11
View File
@@ -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" }
]
}
+1 -1
View File
@@ -17,6 +17,6 @@
{ "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", "fertility": 0.2 } "surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
] ]
} }
+4
View File
@@ -67,6 +67,10 @@
"plant.stage.sprout": "sprout", "plant.stage.sprout": "sprout",
"plant.stage.sapling": "sapling", "plant.stage.sapling": "sapling",
"plant.stage.mature": "mature", "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",
+4
View File
@@ -67,6 +67,10 @@
"plant.stage.sprout": "всходы", "plant.stage.sprout": "всходы",
"plant.stage.sapling": "саженец", "plant.stage.sapling": "саженец",
"plant.stage.mature": "взрослое", "plant.stage.mature": "взрослое",
"product.wood": "древесина",
"product.grass": "трава",
"product.berry": "ягоды",
"product.acorn": "жёлудь",
"pawn.being": "житель", "pawn.being": "житель",
"pawn.bear": "медведь", "pawn.bear": "медведь",
"pawn.deer": "олень", "pawn.deer": "олень",
@@ -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>
@@ -24,9 +23,6 @@
<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>
@@ -39,8 +38,6 @@
<commonality>0.25</commonality> <commonality>0.25</commonality>
<texName>Horse6</texName> <texName>Horse6</texName>
</skin> </skin>
</variants> </variants>
</lifeStage> </lifeStage>
</SkinSet> </SkinSet>
+1 -1
Submodule engine updated: 3438ed77f6...d044cafad9
+30
View File
@@ -1,7 +1,33 @@
using System.Text.Json; using System.Text.Json;
using MrGameEng.Genetics;
namespace LittleSim.App; 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> /// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
public sealed class PawnSave public sealed class PawnSave
{ {
@@ -70,6 +96,9 @@ public sealed class WorldSave
/// <summary>Снимок жителей.</summary> /// <summary>Снимок жителей.</summary>
public List<PawnSave> Pawns { get; set; } = []; public List<PawnSave> Pawns { get; set; } = [];
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
public List<PlantSave> Plants { get; set; } = [];
/// <summary>Конфиг мира для пересоздания сцены.</summary> /// <summary>Конфиг мира для пересоздания сцены.</summary>
public WorldConfig ToConfig() => public WorldConfig ToConfig() =>
new() new()
@@ -90,6 +119,7 @@ public sealed class SaveStore
{ {
WriteIndented = true, WriteIndented = true,
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
}; };
private readonly string _directory; private readonly string _directory;
+11
View File
@@ -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;
@@ -58,9 +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"); 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");
+50
View File
@@ -27,6 +27,9 @@ 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; }
@@ -63,6 +66,17 @@ public sealed class PlantGrowthStage
public string? Label { get; init; } 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> /// <summary>
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по /// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами /// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
@@ -91,8 +105,38 @@ public sealed class GenomeDef
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary> /// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
public float Vigor { get; init; } = 1f; 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> /// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
public float Spread { get; init; } = 0.08f; 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> /// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
@@ -112,6 +156,12 @@ public sealed class PlantDef : Def
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary> /// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
public GenomeDef Genome { get; init; } = new(); 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> /// <summary>
+62 -1
View File
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Genetics;
using MrGameEng.Graphics; using MrGameEng.Graphics;
namespace LittleSim.Content; namespace LittleSim.Content;
@@ -23,6 +24,9 @@ public sealed class PlantSet
/// <summary>Стадии роста по порядку; последняя — терминальная.</summary> /// <summary>Стадии роста по порядку; последняя — терминальная.</summary>
public required Stage[] Stages { get; init; } public required Stage[] Stages { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи (геном из общих <see cref="GeneDef"/>).</summary>
public required GenomeTemplate Template { get; init; }
/// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary> /// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary>
public float MaturityDays => Stages[^1].EnterDay; public float MaturityDays => Stages[^1].EnterDay;
@@ -42,15 +46,28 @@ public sealed class PlantSet
private readonly Species[] _species; private readonly Species[] _species;
private readonly Dictionary<PlantDef, int> _index = new(); private readonly Dictionary<PlantDef, int> _index = new();
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков растений.</summary>
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
/// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary> /// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary>
public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device) 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(); var defs = content.Defs.All<PlantDef>().ToArray();
_species = new Species[defs.Length]; _species = new Species[defs.Length];
for (var i = 0; i < defs.Length; i++) for (var i = 0; i < defs.Length; i++)
{ {
var def = defs[i]; var def = defs[i];
_species[i] = new Species { Def = def, Stages = BuildStages(def, atlases, device) }; _species[i] = new Species
{
Def = def,
Stages = BuildStages(def, atlases, device),
Template = BuildTemplate(def.Genome, genes),
};
_index[def] = i; _index[def] = i;
} }
} }
@@ -64,6 +81,50 @@ public sealed class PlantSet
/// <summary>Индекс вида по дефу.</summary> /// <summary>Индекс вида по дефу.</summary>
public int IndexOf(PlantDef def) => _index[def]; 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) private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device)
{ {
// Без явных стадий растение существует как одна терминальная стадия из базовой текстуры. // Без явных стадий растение существует как одна терминальная стадия из базовой текстуры.
+27
View File
@@ -1,8 +1,35 @@
using LittleSim.Content;
using LittleSim.Net; using LittleSim.Net;
using LittleSim.Scenes; using LittleSim.Scenes;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Genetics;
using MrGameEng.Host; using MrGameEng.Host;
// Контент-линт без окна/GPU: грузит моды/дефы (патчи и валидаторы тоже), компилирует формулы генов
// (включая группировку 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] — после загрузки контента сразу в сетевую сцену // --connect [ws://host:port] — после загрузки контента сразу в сетевую сцену
// (см. MultiplayerScene); без аргументов — обычный запуск в главное меню. // (см. MultiplayerScene); без аргументов — обычный запуск в главное меню.
string? connect = null; string? connect = null;
+1
View File
@@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using LittleSim.App; using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
+10 -20
View File
@@ -36,29 +36,19 @@ internal static class ScatterSpawner
var def = 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 index = plants.IndexOf(def); var index = plants.IndexOf(def);
var species = plants[index];
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize; var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize; var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var age = random.NextSingle() * species.MaturityDays; var age = random.NextSingle() * plants[index].MaturityDays;
var stage = species.StageAt(age);
var resolved = species.Stages[stage];
var sprite = new Sprite(resolved.Region, GameLayers.Beings); PlantFactory.Create(
sprite.CenterOrigin(); scene.Store,
scene.Store.CreateEntity( plants,
new Transform2D( index,
new Vector2(px, py), new Vector2(px, py),
scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width) age,
), plants[index].Template.Generate(random),
sprite, terrain.Fertility,
new PlantGrowth cellSize
{
Species = index,
Stage = stage,
AgeDays = age,
CellFertility = terrain.Fertility,
},
PlantGenome.FromDef(def.Genome, random)
); );
} }
} }
+329 -24
View File
@@ -10,6 +10,8 @@ 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.Host;
using MrGameEng.Input; using MrGameEng.Input;
@@ -26,8 +28,9 @@ namespace LittleSim.Scenes;
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из /// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка /// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа /// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие /// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/
/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн. /// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
/// </summary> /// </summary>
public sealed class WorldScene : Scene public sealed class WorldScene : Scene
{ {
@@ -39,6 +42,9 @@ public sealed class WorldScene : Scene
/// </summary> /// </summary>
public const float SecondsPerDay = 480f; public const float SecondsPerDay = 480f;
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
public const int DensityCap = 4;
private readonly WorldConfig _config; private readonly WorldConfig _config;
private readonly WorldSave? _save; private readonly WorldSave? _save;
private readonly RectF _bounds; private readonly RectF _bounds;
@@ -47,6 +53,12 @@ public sealed class WorldScene : Scene
private PauseMenu _pause = null!; private PauseMenu _pause = null!;
private readonly List<Action> _speedRefreshers = []; private readonly List<Action> _speedRefreshers = [];
private PlantSet _plants = null!;
private float[] _cellFertility = [];
private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
/// <summary>Новый мир из конфига.</summary> /// <summary>Новый мир из конфига.</summary>
public WorldScene(WorldConfig config) public WorldScene(WorldConfig config)
: this(config, null) { } : this(config, null) { }
@@ -77,12 +89,34 @@ public sealed class WorldScene : Scene
var calendar = Context.UseCalendar(SecondsPerDay); var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default); var climate = Context.UseClimate(ClimateSettings.Default);
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер var dayNight = new DayNight(calendar, DayNightSettings.Default);
// Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed). // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
var plants = new PlantSet(content, atlases, device); _plants = new PlantSet(content, atlases, device);
var random = new Random(_config.Seed); var loadingPlants = _save?.Plants is { Count: > 0 };
BuildTerrain(content, atlases, device, assets, plants, random); 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)); var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
@@ -103,8 +137,47 @@ public sealed class WorldScene : Scene
this.UseInspector(renderer); this.UseInspector(renderer);
var console = this.UseDevConsole(); var console = this.UseDevConsole();
RegisterCommands(console, content, atlases); 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, dayNight, CellSize)); 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( UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
); );
@@ -128,17 +201,17 @@ public sealed class WorldScene : Scene
} }
/// <summary> /// <summary>
/// Строит одну сущность-<see cref="Tilemap"/>: тайлсет из дефов рельефа (поверхность из /// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
/// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а /// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
/// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости). /// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
/// </summary> /// </summary>
private void BuildTerrain( private void BuildTerrain(
GameContent content, GameContent content,
ModAtlases atlases, ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device, Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
AssetManager assets, AssetManager assets,
PlantSet plants, Random? scatterRandom
Random random
) )
{ {
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
@@ -161,21 +234,33 @@ public sealed class WorldScene : Scene
_config.SmoothPasses _config.SmoothPasses
); );
var grid = new TileGrid(_config.Width, _config.Height); 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 x = 0; x < _config.Width; x++)
{ {
for (var y = 0; y < _config.Height; y++) for (var y = 0; y < _config.Height; y++)
{ {
var terrain = content.Terrains.Classify(heights[x, y]); var terrain = content.Terrains.Classify(heights[x, y]);
grid[x, y] = tileByDef[terrain]; grid[x, y] = tileByDef[terrain];
ScatterSpawner.Spawn( var cell = y * _config.Width + x;
this, _cellFertility[cell] = terrain.Fertility;
content, _cellLand[cell] = terrain.IsLand;
plants, _cellOccluderBase[cell] = terrain.BlocksLight;
terrain, if (scatterRandom is not null)
new Point(x, y), {
CellSize, ScatterSpawner.Spawn(
random this,
); content,
_plants,
terrain,
new Point(x, y),
CellSize,
scatterRandom
);
}
} }
} }
@@ -255,7 +340,6 @@ public sealed class WorldScene : Scene
private string SaveWorld() private string SaveWorld()
{ {
// Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
var save = new WorldSave var save = new WorldSave
{ {
Name = _config.Name, Name = _config.Name,
@@ -268,11 +352,98 @@ public sealed class WorldScene : Scene
ElapsedSeconds = Context.Clock.TotalTime, 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); new SaveStore().Write(save);
Log.Info($"World '{_config.Name}' saved"); Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
return _config.Name; 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) private void Switch(Scene scene)
{ {
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
@@ -303,10 +474,144 @@ public sealed class WorldScene : Scene
); );
} }
); );
console.Register(
"formula",
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
(c, args) =>
{
if (args.Length == 0)
{
c.WriteLine(
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
);
return;
}
var expression = string.Join(' ', args);
try
{
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
}
catch (FormulaException e)
{
c.WriteLine($"error: {e.Message}");
}
}
);
console.Register(
"gene",
"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( console.Register(
"menu", "menu",
"menu — return to the main menu", "menu — return to the main menu",
(_, _) => Switch(new MainMenuScene()) (_, _) => 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;
} }
+71
View File
@@ -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;
}
}
+62
View File
@@ -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"/> &gt; 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));
}
}
}
}
}
-90
View File
@@ -1,90 +0,0 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
namespace LittleSim.Sim;
/// <summary>
/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная —
/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением).
/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста.
/// </summary>
public struct PlantGenome : IComponent
{
/// <summary>Аллели оптимума освещённости (0..1).</summary>
public float OptimalLightA;
public float OptimalLightB;
/// <summary>Аллели толерантности по свету.</summary>
public float LightToleranceA;
public float LightToleranceB;
/// <summary>Аллели оптимума температуры (°C).</summary>
public float OptimalTemperatureA;
public float OptimalTemperatureB;
/// <summary>Аллели толерантности по температуре (°C).</summary>
public float TemperatureToleranceA;
public float TemperatureToleranceB;
/// <summary>Аллели оптимума плодородности.</summary>
public float OptimalFertilityA;
public float OptimalFertilityB;
/// <summary>Аллели толерантности по плодородности.</summary>
public float FertilityToleranceA;
public float FertilityToleranceB;
/// <summary>Аллели бодрости роста.</summary>
public float VigorA;
public float VigorB;
/// <summary>Фенотип: оптимальная освещённость.</summary>
public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f;
/// <summary>Фенотип: толерантность по свету.</summary>
public readonly float LightTolerance => (LightToleranceA + LightToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная температура.</summary>
public readonly float OptimalTemperature => (OptimalTemperatureA + OptimalTemperatureB) * 0.5f;
/// <summary>Фенотип: толерантность по температуре.</summary>
public readonly float TemperatureTolerance =>
(TemperatureToleranceA + TemperatureToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная плодородность.</summary>
public readonly float OptimalFertility => (OptimalFertilityA + OptimalFertilityB) * 0.5f;
/// <summary>Фенотип: толерантность по плодородности.</summary>
public readonly float FertilityTolerance => (FertilityToleranceA + FertilityToleranceB) * 0.5f;
/// <summary>Фенотип: бодрость роста.</summary>
public readonly float Vigor => (VigorA + VigorB) * 0.5f;
/// <summary>
/// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс
/// (<see cref="GenomeDef.Spread"/>). Сидируется переданным <paramref name="random"/> — детерминизм.
/// </summary>
public static PlantGenome FromDef(GenomeDef def, Random random)
{
float Allele(float baseValue) =>
baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue;
return new PlantGenome
{
OptimalLightA = Allele(def.OptimalLight),
OptimalLightB = Allele(def.OptimalLight),
LightToleranceA = Allele(def.LightTolerance),
LightToleranceB = Allele(def.LightTolerance),
OptimalTemperatureA = Allele(def.OptimalTemperature),
OptimalTemperatureB = Allele(def.OptimalTemperature),
TemperatureToleranceA = Allele(def.TemperatureTolerance),
TemperatureToleranceB = Allele(def.TemperatureTolerance),
OptimalFertilityA = Allele(def.OptimalFertility),
OptimalFertilityB = Allele(def.OptimalFertility),
FertilityToleranceA = Allele(def.FertilityTolerance),
FertilityToleranceB = Allele(def.FertilityTolerance),
VigorA = Allele(def.Vigor),
VigorB = Allele(def.Vigor),
};
}
}
+19 -17
View File
@@ -11,17 +11,18 @@ namespace LittleSim.Sim;
/// <summary> /// <summary>
/// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов /// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов
/// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) · /// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) ·
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из генов /// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из
/// (см. <see cref="PlantGenome"/>). Свет/температура в фазе B глобальные (день/ночь и сезон); стадии /// признаков (<see cref="PlantPhenotype"/>), вычисленных формулами генов. Свет локальный (лайтмап),
/// и спрайт переключаются по накопленному возрасту как прежде. Терминальную стадию пропускает. /// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному
/// возрасту как прежде. Терминальную стадию пропускает.
/// </summary> /// </summary>
public sealed class PlantGrowthSystem( public sealed class PlantGrowthSystem(
PlantSet plants, PlantSet plants,
Calendar calendar, Calendar calendar,
Climate climate, Climate climate,
DayNight dayNight, Lighting lighting,
int cellSize int cellSize
) : QuerySystem<PlantGrowth, PlantGenome, Sprite, Transform2D> ) : QuerySystem<PlantGrowth, PlantOrganism, Sprite, Transform2D>
{ {
protected override void OnUpdate() protected override void OnUpdate()
{ {
@@ -31,14 +32,13 @@ public sealed class PlantGrowthSystem(
return; // пауза — рост стоит return; // пауза — рост стоит
} }
// В фазе B свет и температура глобальные — считаем раз за кадр (фаза D даст локальный свет). // Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее).
var light = dayNight.Intensity;
var temperature = climate.Temperature; var temperature = climate.Temperature;
foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks) foreach (var (growths, organisms, sprites, transforms, _) in Query.Chunks)
{ {
var g = growths.Span; var g = growths.Span;
var dna = genomes.Span; var orgs = organisms.Span;
var s = sprites.Span; var s = sprites.Span;
var t = transforms.Span; var t = transforms.Span;
for (var i = 0; i < g.Length; i++) for (var i = 0; i < g.Length; i++)
@@ -47,22 +47,24 @@ public sealed class PlantGrowthSystem(
var stages = plants[grow.Species].Stages; var stages = plants[grow.Species].Stages;
if (grow.Stage >= stages.Length - 1) if (grow.Stage >= stages.Length - 1)
{ {
continue; // уже зрелое — рост окончен grow.AgeDays += deltaDays; // зрелое: старение в реальном времени → смерть по старости
continue;
} }
ref var gene = ref dna[i]; ref var traits = ref orgs[i].Traits;
var light = lighting.SampleAt(t[i].Position);
var rate = var rate =
gene.Vigor traits.Vigor
* Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance) * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Gaussian( * Suitability.Gaussian(
temperature, temperature,
gene.OptimalTemperature, traits.OptimalTemperature,
gene.TemperatureTolerance traits.TemperatureTolerance
) )
* Suitability.Gaussian( * Suitability.Gaussian(
grow.CellFertility, grow.CellFertility,
gene.OptimalFertility, traits.OptimalFertility,
gene.FertilityTolerance traits.FertilityTolerance
); );
grow.AgeDays += deltaDays * rate; grow.AgeDays += deltaDays * rate;
+316
View File
@@ -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;
}
}
+87
View File
@@ -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;
}