diff --git a/LittleSim.sln b/LittleSim.sln index 0e3fc5c..2d2ea9b 100644 --- a/LittleSim.sln +++ b/LittleSim.sln @@ -41,17 +41,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}" 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 -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 -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 -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 -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 -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 Global 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|x86.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x64.ActiveCfg = Debug|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x64.Build.0 = Debug|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x86.ActiveCfg = Debug|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Debug|x86.Build.0 = Debug|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|Any CPU.Build.0 = Release|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x64.ActiveCfg = Release|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x64.Build.0 = Release|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x86.ActiveCfg = Release|Any CPU - {D4222C26-40D8-4465-9B26-CD6BD0722EC0}.Release|x86.Build.0 = Release|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x64.ActiveCfg = Debug|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x64.Build.0 = Debug|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x86.ActiveCfg = Debug|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Debug|x86.Build.0 = Debug|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Release|Any CPU.Build.0 = Release|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x64.ActiveCfg = Release|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x64.Build.0 = Release|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x86.ActiveCfg = Release|Any CPU - {41F0249B-106E-4D56-B022-73D2C2D74807}.Release|x86.Build.0 = Release|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x64.ActiveCfg = Debug|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x64.Build.0 = Debug|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x86.ActiveCfg = Debug|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Debug|x86.Build.0 = Debug|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|Any CPU.Build.0 = Release|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x64.ActiveCfg = Release|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x64.Build.0 = Release|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x86.ActiveCfg = Release|Any CPU - {BEFB468B-6784-468E-9B60-EB44AB078D15}.Release|x86.Build.0 = Release|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x64.ActiveCfg = Debug|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x64.Build.0 = Debug|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x86.ActiveCfg = Debug|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Debug|x86.Build.0 = Debug|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|Any CPU.Build.0 = Release|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x64.ActiveCfg = Release|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x64.Build.0 = Release|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x86.ActiveCfg = Release|Any CPU - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E}.Release|x86.Build.0 = Release|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x64.ActiveCfg = Debug|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x64.Build.0 = Debug|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x86.ActiveCfg = Debug|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Debug|x86.Build.0 = Debug|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|Any CPU.Build.0 = Release|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x64.ActiveCfg = Release|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x64.Build.0 = Release|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.ActiveCfg = Release|Any CPU - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.Build.0 = Release|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x64.ActiveCfg = Debug|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x64.Build.0 = Debug|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x86.ActiveCfg = Debug|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x86.Build.0 = Debug|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|Any CPU.Build.0 = Release|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x64.ActiveCfg = Release|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x64.Build.0 = Release|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x86.ActiveCfg = Release|Any CPU - {270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x86.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.Build.0 = Release|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.ActiveCfg = Debug|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.Build.0 = Debug|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.ActiveCfg = Debug|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.Build.0 = Debug|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.Build.0 = Release|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.ActiveCfg = Release|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.Build.0 = Release|Any CPU + {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.ActiveCfg = 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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -337,11 +337,11 @@ Global {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {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} - {D4222C26-40D8-4465-9B26-CD6BD0722EC0} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} - {41F0249B-106E-4D56-B022-73D2C2D74807} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} - {BEFB468B-6784-468E-9B60-EB44AB078D15} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {61C84FE7-1E0A-4CF2-A63F-39B23F936A7E} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} - {03EC6C39-E1DE-4C66-835F-4B05B5C40DB0} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} - {270A9E52-E0E3-4885-B662-79E35AF6F7B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A} = {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 EndGlobal diff --git a/Mods/Core/Defs/genes.json b/Mods/Core/Defs/genes.json new file mode 100644 index 0000000..247801b --- /dev/null +++ b/Mods/Core/Defs/genes.json @@ -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" } } + ] +} diff --git a/Mods/Core/Defs/patches.json b/Mods/Core/Defs/patches.json new file mode 100644 index 0000000..81677c8 --- /dev/null +++ b/Mods/Core/Defs/patches.json @@ -0,0 +1,8 @@ +{ + "type": "Patch", + // Контент-патчи (фаза G5): применяются ко всем дефам типа defType, чьё имя подходит под regex match, + // проставляя поля set. Демонстрация: даём кустам древесину при сборе (веточки). + "patches": [ + { "defType": "Plant", "match": "Bush.*", "set": { "harvestProduct": "ProductWood" } } + ] +} diff --git a/Mods/Core/Defs/plants.json b/Mods/Core/Defs/plants.json index 1a83da0..d39427c 100644 --- a/Mods/Core/Defs/plants.json +++ b/Mods/Core/Defs/plants.json @@ -2,8 +2,12 @@ "type": "Plant", "defs": [ { "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, "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": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" }, { "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": "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, "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": [ { "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, + "fruitProduct": "ProductBerry", "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": [ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" }, { "sizeCells": 1.4, "label": "plant.stage.mature" } diff --git a/Mods/Core/Defs/products.json b/Mods/Core/Defs/products.json new file mode 100644 index 0000000..f43d352 --- /dev/null +++ b/Mods/Core/Defs/products.json @@ -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" } + ] +} diff --git a/Mods/Core/Defs/terrain.json b/Mods/Core/Defs/terrain.json index f2caabf..a53f681 100644 --- a/Mods/Core/Defs/terrain.json +++ b/Mods/Core/Defs/terrain.json @@ -17,6 +17,6 @@ { "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] } ] }, { "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 } ] } diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json index a0f9cc7..e57fb66 100644 --- a/Mods/Core/Languages/en/ui.json +++ b/Mods/Core/Languages/en/ui.json @@ -67,6 +67,10 @@ "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.bear": "bear", "pawn.deer": "deer", diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json index 80d0929..b7982f4 100644 --- a/Mods/Core/Languages/ru/ui.json +++ b/Mods/Core/Languages/ru/ui.json @@ -67,6 +67,10 @@ "plant.stage.sprout": "всходы", "plant.stage.sapling": "саженец", "plant.stage.mature": "взрослое", + "product.wood": "древесина", + "product.grass": "трава", + "product.berry": "ягоды", + "product.acorn": "жёлудь", "pawn.being": "житель", "pawn.bear": "медведь", "pawn.deer": "олень", diff --git a/Mods/Core/Textures/things/pawn/animal/guineapig/AEXP_GuineaPig_SkinSet.xml b/Mods/Core/Textures/things/pawn/animal/guineapig/AEXP_GuineaPig_SkinSet.xml index c6fc478..2b4521b 100644 --- a/Mods/Core/Textures/things/pawn/animal/guineapig/AEXP_GuineaPig_SkinSet.xml +++ b/Mods/Core/Textures/things/pawn/animal/guineapig/AEXP_GuineaPig_SkinSet.xml @@ -1,7 +1,6 @@ - true @@ -9,24 +8,21 @@ 0 GuineaPig - + 0.4 GuineaPig1 - + 0.4 GuineaPig2 - + 0.4 GuineaPig3 - - - + - diff --git a/Mods/Core/Textures/things/pawn/animal/horse/AEXP_Horse_SkinSet.xml b/Mods/Core/Textures/things/pawn/animal/horse/AEXP_Horse_SkinSet.xml index 8616b84..d00f5f3 100644 --- a/Mods/Core/Textures/things/pawn/animal/horse/AEXP_Horse_SkinSet.xml +++ b/Mods/Core/Textures/things/pawn/animal/horse/AEXP_Horse_SkinSet.xml @@ -1,7 +1,6 @@ - true @@ -9,38 +8,36 @@ 0.12 Horse - + 0.25 Horse1 - + 0.4 Horse2 - + 0.4 Horse3 - + 0.4 Horse4 - + 0.35 Horse5 - + 0.25 Horse6 - - + - diff --git a/engine b/engine index 3438ed7..d044caf 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit 3438ed77f66d0a88d5dda640bfbd7a60b4a2de2f +Subproject commit d044cafad93177e0fbffb79872bde12afcf03f9c diff --git a/src/LittleSim/App/WorldSave.cs b/src/LittleSim/App/WorldSave.cs index 2b4fac9..871c3f3 100644 --- a/src/LittleSim/App/WorldSave.cs +++ b/src/LittleSim/App/WorldSave.cs @@ -1,7 +1,33 @@ using System.Text.Json; +using MrGameEng.Genetics; namespace LittleSim.App; +/// Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном. +public sealed class PlantSave +{ + /// Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры. + public string Species { get; set; } = ""; + + /// Позиция X в мировых координатах. + public float X { get; set; } + + /// Позиция Y в мировых координатах. + public float Y { get; set; } + + /// Накопленный возраст в игровых днях. + public float AgeDays { get; set; } + + /// Текущая стадия роста. + public int Stage { get; set; } + + /// Плодородность клетки. + public float CellFertility { get; set; } + + /// Геном особи: аллели по id генов (переменного состава). + public Dictionary Genome { get; set; } = new(); +} + /// Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по ). public sealed class PawnSave { @@ -70,6 +96,9 @@ public sealed class WorldSave /// Снимок жителей. public List Pawns { get; set; } = []; + /// Снимок всех растений (геномы, позиции, возраст, стадия). + public List Plants { get; set; } = []; + /// Конфиг мира для пересоздания сцены. public WorldConfig ToConfig() => new() @@ -90,6 +119,7 @@ public sealed class SaveStore { WriteIndented = true, PropertyNameCaseInsensitive = true, + IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах }; private readonly string _directory; diff --git a/src/LittleSim/Content/GameContent.cs b/src/LittleSim/Content/GameContent.cs index abb0694..07fdee2 100644 --- a/src/LittleSim/Content/GameContent.cs +++ b/src/LittleSim/Content/GameContent.cs @@ -1,5 +1,6 @@ using MrGameEng.Atlases; using MrGameEng.Core; +using MrGameEng.Genetics; using MrGameEng.Mods; namespace LittleSim.Content; @@ -58,9 +59,19 @@ public sealed class GameContent var defs = new DefDatabase(); defs.RegisterType("Terrain"); + defs.RegisterType("Gene"); + defs.RegisterType("Product"); defs.RegisterType("Plant"); defs.RegisterType("Pawn"); defs.RegisterType("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); var languages = new LanguageManager(defaultLanguage: "ru"); diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index a33e1d7..781b6a0 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -27,6 +27,9 @@ public sealed class TerrainDef : Def /// Суша: здесь появляются жители, животные и растения. public bool IsLand { get; init; } + /// Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней. + public bool BlocksLight { get; init; } + /// Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл. public string? Surface { get; init; } @@ -63,6 +66,17 @@ public sealed class PlantGrowthStage public string? Label { get; init; } } +/// +/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении +/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество +/// определяют гены. — ключ локализации (product.*). +/// +public sealed class ProductDef : Def +{ + /// Категория продукта (например "material" или "food") — для будущей экономики/инвентаря. + public string Kind { get; init; } = ""; +} + /// /// Базовый геном вида (вложенный объект ): оптимумы и толерантности по /// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами @@ -91,8 +105,38 @@ public sealed class GenomeDef /// Базовая бодрость роста (множитель скорости в идеальных условиях). public float Vigor { get; init; } = 1f; + /// Продолжительность жизни (игровых дней) до смерти от старости. + public float Lifespan { get; init; } = 80f; + + /// Дальность расселения семян (в клетках). + public float DispersalRange { get; init; } = 2f; + + /// Средний интервал между попытками дать семя (игровых дней). + public float ReproduceInterval { get; init; } = 8f; + + /// Способность к самоопылению (0..1): шанс дать семя без партнёра. + public float SelfPollination { get; init; } = 0.4f; + + /// Темп мутаций (0..1): вероятность сдвига аллеля у потомка. + public float MutationRate { get; init; } = 0.05f; + + /// Доля рецессивного аллеля морфы в стартовой популяции (0..1). + public float VariantChance { get; init; } = 0.15f; + /// Доля разброса аллелей вокруг базы при генерации особи. public float Spread { get; init; } = 0.08f; + + /// Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит). + public float FruitYield { get; init; } + + /// Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима). + public float FruitSeason { get; init; } = 1f; + + /// Сколько продукта даёт сбор растения (масштаб ). + public float HarvestAmount { get; init; } = 1f; + + /// Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета. + public float LeafHue { get; init; } = 0.33f; } /// Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном. @@ -112,6 +156,12 @@ public sealed class PlantDef : Def /// Базовый геном вида: оптимумы/толерантности по среде и бодрость роста. public GenomeDef Genome { get; init; } = new(); + + /// Имя , выдаваемого при сборе растения (дерево/трава); null — несборное. + public string? HarvestProduct { get; init; } + + /// Имя , выдаваемого плодами (ягоды/жёлудь); null — не плодоносит. + public string? FruitProduct { get; init; } } /// diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs index 74872cb..b92622e 100644 --- a/src/LittleSim/Content/PlantSet.cs +++ b/src/LittleSim/Content/PlantSet.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework.Graphics; +using MrGameEng.Genetics; using MrGameEng.Graphics; namespace LittleSim.Content; @@ -23,6 +24,9 @@ public sealed class PlantSet /// Стадии роста по порядку; последняя — терминальная. public required Stage[] Stages { get; init; } + /// Набор генов вида: базовые значения для генерации особи (геном из общих ). + public required GenomeTemplate Template { get; init; } + /// День, с которого растение достигает последней (зрелой) стадии. public float MaturityDays => Stages[^1].EnterDay; @@ -42,15 +46,28 @@ public sealed class PlantSet private readonly Species[] _species; private readonly Dictionary _index = new(); + /// Общий реестр генов (по id) для скрещивания и вычисления признаков растений. + public IReadOnlyDictionary GeneRegistry { get; } + /// Строит таблицу из всех (не-abstract) мода. public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device) { + var genes = content + .Defs.All() + .ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal); + GeneRegistry = genes; + var defs = content.Defs.All().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) }; + _species[i] = new Species + { + Def = def, + Stages = BuildStages(def, atlases, device), + Template = BuildTemplate(def.Genome, genes), + }; _index[def] = i; } } @@ -64,6 +81,50 @@ public sealed class PlantSet /// Индекс вида по дефу. public int IndexOf(PlantDef def) => _index[def]; + // Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр + // аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида. + private static GenomeTemplate BuildTemplate( + GenomeDef g, + IReadOnlyDictionary 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) { // Без явных стадий растение существует как одна терминальная стадия из базовой текстуры. diff --git a/src/LittleSim/Program.cs b/src/LittleSim/Program.cs index f6b7ae6..2e8d078 100644 --- a/src/LittleSim/Program.cs +++ b/src/LittleSim/Program.cs @@ -1,8 +1,35 @@ +using LittleSim.Content; using LittleSim.Net; using LittleSim.Scenes; using Microsoft.Xna.Framework; +using MrGameEng.Genetics; using MrGameEng.Host; +// Контент-линт без окна/GPU: грузит моды/дефы (патчи и валидаторы тоже), компилирует формулы генов +// (включая группировку gsum) и выходит. Удобно для CI. +if (args.Contains("--check-content")) +{ + try + { + var content = GameContent.Load(); + var genes = content.Defs.All(); + 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; diff --git a/src/LittleSim/Scenes/BootScene.cs b/src/LittleSim/Scenes/BootScene.cs index 5a37af5..92f8d28 100644 --- a/src/LittleSim/Scenes/BootScene.cs +++ b/src/LittleSim/Scenes/BootScene.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using LittleSim.App; using LittleSim.Content; diff --git a/src/LittleSim/Scenes/ContentHelpers.cs b/src/LittleSim/Scenes/ContentHelpers.cs index 3f43486..07f90f0 100644 --- a/src/LittleSim/Scenes/ContentHelpers.cs +++ b/src/LittleSim/Scenes/ContentHelpers.cs @@ -36,29 +36,19 @@ internal static class ScatterSpawner var def = content.Defs.Get(entry.Options[random.Next(entry.Options.Count)]); var index = plants.IndexOf(def); - var species = plants[index]; var px = (cell.X + 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 stage = species.StageAt(age); - var resolved = species.Stages[stage]; + var age = random.NextSingle() * plants[index].MaturityDays; - var sprite = new Sprite(resolved.Region, GameLayers.Beings); - sprite.CenterOrigin(); - scene.Store.CreateEntity( - new Transform2D( - new Vector2(px, py), - scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width) - ), - sprite, - new PlantGrowth - { - Species = index, - Stage = stage, - AgeDays = age, - CellFertility = terrain.Fertility, - }, - PlantGenome.FromDef(def.Genome, random) + PlantFactory.Create( + scene.Store, + plants, + index, + new Vector2(px, py), + age, + plants[index].Template.Generate(random), + terrain.Fertility, + cellSize ); } } diff --git a/src/LittleSim/Scenes/CreditsScene.cs b/src/LittleSim/Scenes/CreditsScene.cs index e09dfe0..b4e63e5 100644 --- a/src/LittleSim/Scenes/CreditsScene.cs +++ b/src/LittleSim/Scenes/CreditsScene.cs @@ -1,47 +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; - -/// Экран «Авторы»: демонстрационный, пока один автор. Назад — кнопкой или Esc. -public sealed class CreditsScene : Scene -{ - protected override void OnLoad() - { - var lang = Context.Services.Get().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)); - } - } -} +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; + +/// Экран «Авторы»: демонстрационный, пока один автор. Назад — кнопкой или Esc. +public sealed class CreditsScene : Scene +{ + protected override void OnLoad() + { + var lang = Context.Services.Get().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)); + } + } +} diff --git a/src/LittleSim/Scenes/LoadGameScene.cs b/src/LittleSim/Scenes/LoadGameScene.cs index 2a49ae5..be85763 100644 --- a/src/LittleSim/Scenes/LoadGameScene.cs +++ b/src/LittleSim/Scenes/LoadGameScene.cs @@ -1,111 +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; - -/// -/// Экран загрузки: список сохранений из (имя, размер, дата) с -/// кнопками «Загрузить» и «Удалить». Загрузка пересоздаёт мир из полного состояния симуляции. -/// -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(); - 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)); - } - } -} +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; + +/// +/// Экран загрузки: список сохранений из (имя, размер, дата) с +/// кнопками «Загрузить» и «Удалить». Загрузка пересоздаёт мир из полного состояния симуляции. +/// +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(); + 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)); + } + } +} diff --git a/src/LittleSim/Scenes/MainMenuScene.cs b/src/LittleSim/Scenes/MainMenuScene.cs index 429873e..64dbc57 100644 --- a/src/LittleSim/Scenes/MainMenuScene.cs +++ b/src/LittleSim/Scenes/MainMenuScene.cs @@ -1,44 +1,44 @@ -using LittleSim.Content; -using LittleSim.UI; -using Microsoft.Xna.Framework; -using MrGameEng.Core; -using MrGameEng.Host; -using MrGameEng.UI; - -namespace LittleSim.Scenes; - -/// -/// Главное меню: заголовок и кнопки Новый мир / Загрузка / Настройки / Авторы / Выход. -/// Все подписи — из локализации Core-мода. Чистый UI-экран (Myra), без мира и рендерера. -/// -public sealed class MainMenuScene : Scene -{ - protected override void OnLoad() - { - var content = Context.Services.Get(); - 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().Exit()) - ); - - desktop.Root = Ui.Screen(column); - } - - private void Go(Scene scene) - { - if (!Context.Scenes.IsTransitioning) - { - Context.Scenes.Switch(scene, Transitions.Fade(0.4f)); - } - } -} +using LittleSim.Content; +using LittleSim.UI; +using Microsoft.Xna.Framework; +using MrGameEng.Core; +using MrGameEng.Host; +using MrGameEng.UI; + +namespace LittleSim.Scenes; + +/// +/// Главное меню: заголовок и кнопки Новый мир / Загрузка / Настройки / Авторы / Выход. +/// Все подписи — из локализации Core-мода. Чистый UI-экран (Myra), без мира и рендерера. +/// +public sealed class MainMenuScene : Scene +{ + protected override void OnLoad() + { + var content = Context.Services.Get(); + 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().Exit()) + ); + + desktop.Root = Ui.Screen(column); + } + + private void Go(Scene scene) + { + if (!Context.Scenes.IsTransitioning) + { + Context.Scenes.Switch(scene, Transitions.Fade(0.4f)); + } + } +} diff --git a/src/LittleSim/Scenes/NewWorldScene.cs b/src/LittleSim/Scenes/NewWorldScene.cs index eafdb76..a379f2a 100644 --- a/src/LittleSim/Scenes/NewWorldScene.cs +++ b/src/LittleSim/Scenes/NewWorldScene.cs @@ -1,167 +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; - -/// -/// Окно настройки нового мира: имя, размер (пресеты из Core-мода), -/// сид (с кнопкой «случайно») и сглаживание рельефа. «Создать» запускает . -/// -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 _refreshers = []; - - protected override void OnLoad() - { - var content = Context.Services.Get(); - var lang = content.Languages; - var input = this.UseInput(); - var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов - - var presets = content.Defs.All().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)); - } - } -} +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; + +/// +/// Окно настройки нового мира: имя, размер (пресеты из Core-мода), +/// сид (с кнопкой «случайно») и сглаживание рельефа. «Создать» запускает . +/// +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 _refreshers = []; + + protected override void OnLoad() + { + var content = Context.Services.Get(); + var lang = content.Languages; + var input = this.UseInput(); + var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов + + var presets = content.Defs.All().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)); + } + } +} diff --git a/src/LittleSim/Scenes/PauseMenu.cs b/src/LittleSim/Scenes/PauseMenu.cs index a3aced3..6071706 100644 --- a/src/LittleSim/Scenes/PauseMenu.cs +++ b/src/LittleSim/Scenes/PauseMenu.cs @@ -1,168 +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; - -/// -/// Внутриигровое меню-пауза: затемняющий оверлей поверх мира (мир остаётся загруженным). -/// Открытие ставит игру на паузу через , закрытие — снимает. -/// Кнопки: Продолжить / Настройки (общая ) / Сохранить / -/// Главное меню / Выход. добавляется в корневую панель сцены. -/// -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 _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 onSave, - Action onMainMenu, - Action onQuit - ) - { - _context = context; - _content = content; - _speed = speed; - _host = (GameHost)context.Services.Get(); - _audio = context.Services.Get(); - _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, - }; - } - - /// Виджет оверлея для добавления в корень сцены. - public Widget Root => _overlay; - - /// Открыт ли оверлей паузы. - public bool IsOpen { get; private set; } - - /// - /// Реакция на Esc: закрыть подменю настроек → вернуться к кнопкам; иначе открыть/закрыть - /// меню паузы. - /// - 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); - } -} +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; + +/// +/// Внутриигровое меню-пауза: затемняющий оверлей поверх мира (мир остаётся загруженным). +/// Открытие ставит игру на паузу через , закрытие — снимает. +/// Кнопки: Продолжить / Настройки (общая ) / Сохранить / +/// Главное меню / Выход. добавляется в корневую панель сцены. +/// +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 _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 onSave, + Action onMainMenu, + Action onQuit + ) + { + _context = context; + _content = content; + _speed = speed; + _host = (GameHost)context.Services.Get(); + _audio = context.Services.Get(); + _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, + }; + } + + /// Виджет оверлея для добавления в корень сцены. + public Widget Root => _overlay; + + /// Открыт ли оверлей паузы. + public bool IsOpen { get; private set; } + + /// + /// Реакция на Esc: закрыть подменю настроек → вернуться к кнопкам; иначе открыть/закрыть + /// меню паузы. + /// + 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); + } +} diff --git a/src/LittleSim/Scenes/SettingsScene.cs b/src/LittleSim/Scenes/SettingsScene.cs index 59583cb..ce1e79f 100644 --- a/src/LittleSim/Scenes/SettingsScene.cs +++ b/src/LittleSim/Scenes/SettingsScene.cs @@ -1,66 +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; - -/// -/// Экран настроек игры (язык, экран, громкость, разрешение). Использует общий -/// . «Применить» сохраняет в settings.json и применяет к движку -/// (смена языка перестраивает подписи на лету); «Назад»/Esc — в главное меню. -/// -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(); - _audio = Context.Services.Get(); - _host = (GameHost)Context.Services.Get(); - 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)); - } - } -} +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; + +/// +/// Экран настроек игры (язык, экран, громкость, разрешение). Использует общий +/// . «Применить» сохраняет в settings.json и применяет к движку +/// (смена языка перестраивает подписи на лету); «Назад»/Esc — в главное меню. +/// +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(); + _audio = Context.Services.Get(); + _host = (GameHost)Context.Services.Get(); + 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)); + } + } +} diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index f8f7684..eba5384 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -1,312 +1,617 @@ -using System; -using System.Collections.Generic; -using Friflo.Engine.ECS; -using LittleSim.App; -using LittleSim.Content; -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.Inspector; -using MrGameEng.Lighting; -using MrGameEng.Tilemaps; -using MrGameEng.UI; -using Myra.Graphics2D; -using Myra.Graphics2D.UI; - -namespace LittleSim.Scenes; - -/// -/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из -/// (размер/сид/масштаб деталей) процедурной генерацией движка -/// (). Каждый тип клетки рисуется текстурой-поверхностью из атласа -/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие -/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн. -/// -public sealed class WorldScene : Scene -{ - public const int CellSize = 16; - - /// - /// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые - /// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с. - /// - public const float SecondsPerDay = 480f; - - private readonly WorldConfig _config; - private readonly WorldSave? _save; - private readonly RectF _bounds; - - private GameSpeed _speed = null!; - private PauseMenu _pause = null!; - private readonly List _speedRefreshers = []; - - /// Новый мир из конфига. - public WorldScene(WorldConfig config) - : this(config, null) { } - - /// Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига). - 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() - { - var assets = Context.Services.GetOrDefault() ?? Context.UseAssets(); - var content = Context.Services.Get(); - var atlases = Context.Services.Get(); - var device = Context.GetGraphicsDevice(); - var input = this.UseInput(); - _speed = Context.Services.Get(); - _speed.SetStep(0); // новый мир/загрузка стартуют на x1 - - var renderer = this.UseRenderer2D( - new Renderer2DOptions { VirtualResolution = new Point(1280, 720) } - ); - GameLayers.EnsureRegistered(renderer); - this.UseTilemaps(); - - var calendar = Context.UseCalendar(SecondsPerDay); - var climate = Context.UseClimate(ClimateSettings.Default); - var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер - - // Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed). - var plants = new PlantSet(content, atlases, device); - var random = new Random(_config.Seed); - BuildTerrain(content, atlases, device, assets, plants, random); - - var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds)); - - // 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().Exit() - ); - desktop.Root = Ui.Screen(hudLabel, speedBar, _pause.Root); - - this.UseInspector(renderer); - var console = this.UseDevConsole(); - RegisterCommands(console, content, atlases); - - UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, climate, dayNight, CellSize)); - 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)") - ); - } - - /// - /// Строит одну сущность-: тайлсет из дефов рельефа (поверхность из - /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а - /// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости). - /// - private void BuildTerrain( - GameContent content, - ModAtlases atlases, - Microsoft.Xna.Framework.Graphics.GraphicsDevice device, - AssetManager assets, - PlantSet plants, - Random random - ) - { - var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); - - // Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода). - var tiles = new TileSet(); - var tileByDef = new Dictionary(); - foreach (var terrain in content.Terrains.All) - { - 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); - for (var x = 0; x < _config.Width; x++) - { - for (var y = 0; y < _config.Height; y++) - { - var terrain = content.Terrains.Classify(heights[x, y]); - grid[x, y] = tileByDef[terrain]; - ScatterSpawner.Spawn( - this, - content, - plants, - terrain, - new Point(x, y), - CellSize, - random - ); - } - } - - Store.CreateEntity(new Tilemap(grid, tiles, CellSize)); - } - - 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 index = i; - var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" }; - button.Click += (_, _) => _speed.SetStep(index); - bar.Widgets.Add(button); - _speedRefreshers.Add(() => - button.TextColor = - !_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted - ); - } - - void Refresh() - { - foreach (var refresh in _speedRefreshers) - { - refresh(); - } - } - - _speed.Changed += Refresh; - RegisterUnload(() => _speed.Changed -= Refresh); - Refresh(); - return bar; - } - - 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, - }; - - new SaveStore().Write(save); - Log.Info($"World '{_config.Name}' saved"); - return _config.Name; - } - - 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); - console.Register( - "regen", - "regen [seed] — regenerate the world with a new seed", - (c, args) => - { - if (Context.Scenes.IsTransitioning) - { - return; - } - - var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next(); - c.WriteLine($"regenerating world, seed {seed}"); - Context.Scenes.Switch( - new WorldScene(_config with { Seed = seed }), - Transitions.Fade(0.6f) - ); - } - ); - console.Register( - "menu", - "menu — return to the main menu", - (_, _) => Switch(new MainMenuScene()) - ); - } -} +using System; +using System.Collections.Generic; +using Friflo.Engine.ECS; +using LittleSim.App; +using LittleSim.Content; +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.Formulas; +using MrGameEng.Genetics; +using MrGameEng.Graphics; +using MrGameEng.Host; +using MrGameEng.Input; +using MrGameEng.Inspector; +using MrGameEng.Lighting; +using MrGameEng.Tilemaps; +using MrGameEng.UI; +using Myra.Graphics2D; +using Myra.Graphics2D.UI; + +namespace LittleSim.Scenes; + +/// +/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из +/// (размер/сид/масштаб деталей) процедурной генерацией движка +/// (). Каждый тип клетки рисуется текстурой-поверхностью из атласа +/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/ +/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости +/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1). +/// +public sealed class WorldScene : Scene +{ + public const int CellSize = 16; + + /// + /// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые + /// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с. + /// + public const float SecondsPerDay = 480f; + + /// Максимум растений на клетку — потолок плотности для размножения. + public const int DensityCap = 4; + + private readonly WorldConfig _config; + private readonly WorldSave? _save; + private readonly RectF _bounds; + + private GameSpeed _speed = null!; + private PauseMenu _pause = null!; + private readonly List _speedRefreshers = []; + + private PlantSet _plants = null!; + private float[] _cellFertility = []; + private bool[] _cellLand = []; + private bool[] _cellOccluderBase = []; // горы (статично из террейна) + private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом) + + /// Новый мир из конфига. + public WorldScene(WorldConfig config) + : this(config, null) { } + + /// Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига). + 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() + { + var assets = Context.Services.GetOrDefault() ?? Context.UseAssets(); + var content = Context.Services.Get(); + var atlases = Context.Services.Get(); + var device = Context.GetGraphicsDevice(); + var input = this.UseInput(); + _speed = Context.Services.Get(); + _speed.SetStep(0); // новый мир/загрузка стартуют на x1 + + var renderer = this.UseRenderer2D( + new Renderer2DOptions { VirtualResolution = new Point(1280, 720) } + ); + 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().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)") + ); + } + + /// + /// Строит сущность- (тайлсет из дефов рельефа + сетка по карте высот) и + /// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если + /// задан (новый мир) — рассыпает растительность по дефам; + /// при загрузке передаётся null (растения восстанавливаются из сейва отдельно). + /// + 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 tiles = new TileSet(); + var tileByDef = new Dictionary(); + foreach (var terrain in content.Terrains.All) + { + 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]); + grid[x, y] = tileByDef[terrain]; + var cell = y * _config.Width + x; + _cellFertility[cell] = terrain.Fertility; + _cellLand[cell] = terrain.IsLand; + _cellOccluderBase[cell] = terrain.BlocksLight; + if (scatterRandom is not null) + { + ScatterSpawner.Spawn( + this, + content, + _plants, + terrain, + new Point(x, y), + CellSize, + scatterRandom + ); + } + } + } + + Store.CreateEntity(new Tilemap(grid, tiles, CellSize)); + } + + 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 index = i; + var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" }; + button.Click += (_, _) => _speed.SetStep(index); + bar.Widgets.Add(button); + _speedRefreshers.Add(() => + button.TextColor = + !_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted + ); + } + + void Refresh() + { + foreach (var refresh in _speedRefreshers) + { + refresh(); + } + } + + _speed.Changed += Refresh; + RegisterUnload(() => _speed.Changed -= Refresh); + Refresh(); + return bar; + } + + 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() + .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(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() + .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); + console.Register( + "regen", + "regen [seed] — regenerate the world with a new seed", + (c, args) => + { + if (Context.Scenes.IsTransitioning) + { + return; + } + + var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next(); + c.WriteLine($"regenerating world, seed {seed}"); + Context.Scenes.Switch( + new WorldScene(_config with { Seed = seed }), + Transitions.Fade(0.6f) + ); + } + ); + console.Register( + "formula", + "formula — compile and evaluate an expression (gene-formula engine demo)", + (c, args) => + { + if (args.Length == 0) + { + c.WriteLine( + "usage: formula 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 [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(); + 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 [seed] e.g. plant TreeOakA"); + console.WriteLine($"species: {string.Join(", ", species)}"); + return; + } + + if (!content.Defs.TryGet(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(productDefName, out var product) + ? content.Languages.Get(product.Label) + : productDefName; +} diff --git a/src/LittleSim/Sim/PlantFactory.cs b/src/LittleSim/Sim/PlantFactory.cs new file mode 100644 index 0000000..412247a --- /dev/null +++ b/src/LittleSim/Sim/PlantFactory.cs @@ -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; + +/// +/// Единая точка создания сущности-растения: спрайт стадии по возрасту, признаки из генома формулами +/// генов, тинт по морфе и компоненты роста/организма. Переиспользуется начальным скаттером, +/// размножением и загрузкой сейва — чтобы все растения собирались одинаково. +/// +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); + + /// Создаёт растение вида с заданным возрастом и геномом. + 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; + } +} diff --git a/src/LittleSim/Sim/PlantFruitingSystem.cs b/src/LittleSim/Sim/PlantFruitingSystem.cs new file mode 100644 index 0000000..8579d00 --- /dev/null +++ b/src/LittleSim/Sim/PlantFruitingSystem.cs @@ -0,0 +1,62 @@ +using Friflo.Engine.ECS.Systems; +using LittleSim.Content; +using MrGameEng.AI; +using MrGameEng.Core; + +namespace LittleSim.Sim; + +/// +/// Плодоношение (фаза G4): у зрелого растения с признаком > 0 +/// в его генетический сезон () копятся зрелые плоды до потолка +/// ; вне сезона плоды опадают. Полностью управляется генами через +/// признаки — система лишь применяет их к компоненту . Незрелые/бесплодные +/// растения остаются с нулём. +/// +public sealed class PlantFruitingSystem(PlantSet plants, Calendar calendar, Climate climate) + : QuerySystem +{ + // За сколько игровых дней набирается/опадает полный урожай. + 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)); + } + } + } + } +} diff --git a/src/LittleSim/Sim/PlantGenome.cs b/src/LittleSim/Sim/PlantGenome.cs deleted file mode 100644 index a7971db..0000000 --- a/src/LittleSim/Sim/PlantGenome.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Friflo.Engine.ECS; -using LittleSim.Content; - -namespace LittleSim.Sim; - -/// -/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная — -/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением). -/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста. -/// -public struct PlantGenome : IComponent -{ - /// Аллели оптимума освещённости (0..1). - public float OptimalLightA; - public float OptimalLightB; - - /// Аллели толерантности по свету. - public float LightToleranceA; - public float LightToleranceB; - - /// Аллели оптимума температуры (°C). - public float OptimalTemperatureA; - public float OptimalTemperatureB; - - /// Аллели толерантности по температуре (°C). - public float TemperatureToleranceA; - public float TemperatureToleranceB; - - /// Аллели оптимума плодородности. - public float OptimalFertilityA; - public float OptimalFertilityB; - - /// Аллели толерантности по плодородности. - public float FertilityToleranceA; - public float FertilityToleranceB; - - /// Аллели бодрости роста. - public float VigorA; - public float VigorB; - - /// Фенотип: оптимальная освещённость. - public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f; - - /// Фенотип: толерантность по свету. - public readonly float LightTolerance => (LightToleranceA + LightToleranceB) * 0.5f; - - /// Фенотип: оптимальная температура. - public readonly float OptimalTemperature => (OptimalTemperatureA + OptimalTemperatureB) * 0.5f; - - /// Фенотип: толерантность по температуре. - public readonly float TemperatureTolerance => - (TemperatureToleranceA + TemperatureToleranceB) * 0.5f; - - /// Фенотип: оптимальная плодородность. - public readonly float OptimalFertility => (OptimalFertilityA + OptimalFertilityB) * 0.5f; - - /// Фенотип: толерантность по плодородности. - public readonly float FertilityTolerance => (FertilityToleranceA + FertilityToleranceB) * 0.5f; - - /// Фенотип: бодрость роста. - public readonly float Vigor => (VigorA + VigorB) * 0.5f; - - /// - /// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс - /// (). Сидируется переданным — детерминизм. - /// - 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), - }; - } -} diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index aafdc72..e7f200c 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -11,17 +11,18 @@ namespace LittleSim.Sim; /// /// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов /// растения. Скорость = бодрость · пригодность(свет) · пригодность(температура) · -/// пригодность(плодородность), каждая пригодность — гауссов «колокол» вокруг оптимума из генов -/// (см. ). Свет/температура в фазе B глобальные (день/ночь и сезон); стадии -/// и спрайт переключаются по накопленному возрасту как прежде. Терминальную стадию пропускает. +/// пригодность(плодородность), каждая пригодность — гауссов «колокол» вокруг оптимума из +/// признаков (), вычисленных формулами генов. Свет локальный (лайтмап), +/// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному +/// возрасту как прежде. Терминальную стадию пропускает. /// public sealed class PlantGrowthSystem( PlantSet plants, Calendar calendar, Climate climate, - DayNight dayNight, + Lighting lighting, int cellSize -) : QuerySystem +) : QuerySystem { protected override void OnUpdate() { @@ -31,14 +32,13 @@ public sealed class PlantGrowthSystem( return; // пауза — рост стоит } - // В фазе B свет и температура глобальные — считаем раз за кадр (фаза D даст локальный свет). - var light = dayNight.Intensity; + // Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее). 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 dna = genomes.Span; + var orgs = organisms.Span; var s = sprites.Span; var t = transforms.Span; for (var i = 0; i < g.Length; i++) @@ -47,22 +47,24 @@ public sealed class PlantGrowthSystem( var stages = plants[grow.Species].Stages; 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 = - gene.Vigor - * Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance) + traits.Vigor + * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) * Suitability.Gaussian( temperature, - gene.OptimalTemperature, - gene.TemperatureTolerance + traits.OptimalTemperature, + traits.TemperatureTolerance ) * Suitability.Gaussian( grow.CellFertility, - gene.OptimalFertility, - gene.FertilityTolerance + traits.OptimalFertility, + traits.FertilityTolerance ); grow.AgeDays += deltaDays * rate; diff --git a/src/LittleSim/Sim/PlantLifecycleSystem.cs b/src/LittleSim/Sim/PlantLifecycleSystem.cs new file mode 100644 index 0000000..1f6ff2c --- /dev/null +++ b/src/LittleSim/Sim/PlantLifecycleSystem.cs @@ -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; + +/// +/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается +/// не каждый кадр, а раз в игрового дня. За тик: строит по-клеточную сетку +/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с +/// вероятностью elapsed/ReproduceInterval сеют потомка — приоритет перекрёстного опыления +/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в +/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения +/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно. +/// +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 _query; + + private readonly int[] _count; + private readonly CellRep[] _rep; + private readonly List _deaths = []; + private readonly List _candidates = []; + private readonly List _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(); + _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; + } +} diff --git a/src/LittleSim/Sim/PlantOrganism.cs b/src/LittleSim/Sim/PlantOrganism.cs new file mode 100644 index 0000000..8376415 --- /dev/null +++ b/src/LittleSim/Sim/PlantOrganism.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using Friflo.Engine.ECS; +using MrGameEng.Genetics; + +namespace LittleSim.Sim; + +/// +/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. genes.json): +/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую. +/// Имена полей соответствуют именам признаков в эффектах генов. +/// +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; + + /// Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта. + public bool IsVariant; + + /// Собирает фенотип из карты признаков, посчитанной . + public static PlantPhenotype FromTraits(IReadOnlyDictionary 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, + }; + } +} + +/// +/// Компонент-организм растения: его управляемый (для размножения, сейва и +/// пересчёта) и выраженный фенотип , по которому работают системы. Заменяет +/// прежний фиксированный PlantGenome — теперь геном переменного состава и признаки из формул +/// генов (фаза G3). Геном — ссылочный объект, поэтому компонент несёт ссылку, а не копию. +/// +public struct PlantOrganism : IComponent +{ + /// Геном особи (аллели по генам); общий источник истины для признаков и размножения. + public Genome Genome; + + /// Выраженные признаки — то, что читают системы роста/жизненного цикла. + public PlantPhenotype Traits; +} + +/// +/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его +/// сезон плодоношения (признак ) до потолка +/// , вне сезона опадает. Управляется PlantFruitingSystem. +/// +public struct Fruiting : IComponent +{ + /// Накоплено зрелых плодов (0..FruitYield). + public float RipeFruit; +}