Compare commits

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 04:23:51 +03:00
Leonid PershinandClaude Opus 4.8 d52eff07f8 Remove docs/гены.md (gene-system design doc)
The gene system it described (phases G1–G5) is fully implemented; the
design doc is no longer needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 04:09:45 +03:00
Leonid PershinandClaude Opus 4.8 37d8c976a3 G5: bump engine (regex tooling), showcase grouping/patch/validators
Engine pointer -> d044caf (formula gene grouping, content patches, def
field validation). Game-side demonstration of all three:

- genes.json: GeneHardiness whose effect is gsum('Gene.*Tolerance') — a
  derived trait summing all tolerance genes via regex grouping.
- patches.json: a content patch giving every Bush* plant a wood harvest
  product, applied to Core's own defs at load.
- GameContent registers load-time validators: Gene/Product defNames must
  carry their type prefix.
- Program: a --check-content headless mode that loads all mod defs (running
  patches + validators) and computes a sample genome's traits (compiling
  every gene formula, grouping included) — a CI-friendly content lint.

Verified: --check-content reports 6 def types, 18 genes, 8 plants, 18
traits with hardiness computed from the gene group. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 04:00:26 +03:00
Leonid PershinandClaude Opus 4.8 7fc4bcd301 G4: gene-driven content — fruiting, harvest products, leaf color
New plant content, all driven by genes through the trait layer (no engine
change — the genetics machinery already supports it).

- ProductDef + products.json (wood, grass, berries, acorn) — what plants
  yield; localized labels (ru/en).
- New numeric genes (genes.json): GeneFruitYield, GeneFruitSeason,
  GeneHarvestAmount, GeneLeafHue, each declaring its trait via formula.
  GenomeDef carries the per-species bases (PlantSet maps them into the
  species template); PlantDef gains harvestProduct / fruitProduct.
- PlantPhenotype gains FruitYield/FruitSeason/HarvestAmount/LeafHue.
  PlantFactory tints the sprite from the leaf-hue gene (combined with the
  morph variant). Plants carry a Fruiting component; PlantFruitingSystem
  ripens fruit on mature plants during their gene-chosen season and drops
  it off-season.
- plants.json: trees give wood + acorns (autumn), bushes berries (summer),
  grass gives grass — amounts/season/hue from genes.
- 'plant <species> [seed]' console command samples a species genome and
  prints its gene-driven traits and products.

Build clean; def JSON validated; boot smoke loads the new content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:50:03 +03:00
Leonid PershinandClaude Opus 4.8 9c2c2d7fd0 G3: migrate plants onto genes/traits (drop fixed PlantGenome)
Plants now carry a managed Genome and read formula-computed traits
instead of the hardcoded PlantGenome struct. Engine pointer -> ead2251
(GenomeTemplate + Breed mutationChance override).

- PlantOrganism { Genome; PlantPhenotype Traits } replaces PlantGenome.
  Traits (optima/tolerances, vigor, lifespan, dispersal, reproduce
  interval, self-pollination, mutation rate, morph variant) come from
  Phenotype.Compute over the gene effect formulas in genes.json.
- PlantSet builds a per-species GenomeTemplate by mapping each species'
  existing plants.json genome numbers onto the shared GeneDefs, and
  exposes the gene registry. Species values unchanged — only reinterpreted
  through genes now.
- Growth and lifecycle systems read PlantOrganism.Traits; breeding goes
  through Genome.Breed with the registry and a mutation chance averaged
  from the parents' evolvable mutationRate trait. Scatter generates from
  the species template.
- Save stores the genome as a geneId->Allele map; load rebuilds it
  (empty/legacy saves regenerate from the template — dev saves disposable).

Behaviour matches phases A-D, now fully data-driven through genes and
formulas. Full suite green; boot smoke test loads genes + plants cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:59:46 +03:00
Leonid PershinandClaude Opus 4.8 91a00b4305 G2: bump engine (gene foundation), load genes + gene demo command
Engine pointer -> bc18db5: brings in the organism-agnostic gene
foundation (GeneDef, managed Genome, Phenotype trait computation) plus a
parallel MrGameEng.Net library the game does not use.

Game wiring: register the "Gene" def type and ship Mods/Core/Defs/
genes.json — a full gene set covering the plant phenotype dimensions
(environment optima/tolerances, vigor, lifecycle, reproduction) plus a
discrete morph gene, each declaring its trait effects as formulas. A
dev-console 'gene [seed]' command generates a genome from these defs,
prints its alleles, expressed values and computed traits, then breeds a
child — demonstrating the whole pipeline end to end. These genes seed
the G3 migration of plants onto traits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:43:50 +03:00
Leonid PershinandClaude Opus 4.8 32bae03869 G1: bump engine (formula engine + Host split), wire formula demo
Engine pointer -> 10898b0, which brings in two engine changes:
- the gene-system formula engine (Core): a data-driven expression
  evaluator that compiles a def string once and evaluates it
  allocation-free against a variable context;
- a parallel refactor splitting the platform out of Core into a new
  MrGameEng.Host library.

Game migration for the Host split: reference MrGameEng.Host (+ add it
and its test project to the solution), import MrGameEng.Host where
GameHost/GameHostOptions/Input are used, switch Transition.Fade ->
Transitions.Fade, and read the device via context.GetGraphicsDevice()
now that EngineContext is platform-free.

Showcase the formula engine with a dev-console 'formula <expr>' command
that compiles and evaluates an expression. Also adds docs/гены.md, the
gene-system design doc (phases G1-G5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:30:44 +03:00
Leonid PershinandClaude Opus 4.8 49936a1de3 Phase D: spatial lighting with shadows; local light drives growth
Bump the engine submodule to 79d406a (2D lightmap). The world scene now uses
UseLighting instead of the per-sprite ambient: a lightmap multiplies day/night
× occlusion over the world, so forests cast soft shade and the world darkens
spatially toward night. Occluders are mountains (new TerrainDef.BlocksLight)
plus mature trees, rebuilt from the live population. PlantGrowthSystem samples
the local light per plant (Lighting.SampleAt), so undergrowth under canopy
grows slower. A dev-console 'light' command places a point light at the cursor
to show cast shadows at night. Completes the plant ecosystem (A–D).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:40:31 +03:00
Leonid PershinandClaude Opus 4.8 a04e867e87 Phase C: plant reproduction (Mendel), death and persistence
Plants now live full lives. The genome gains lifespan, dispersal, fecundity,
self-pollination, mutation-rate and a discrete dominant/recessive Morph gene;
PlantGenome.Breed models meiosis (one allele per parent) plus mutation. A
gated PlantLifecycleSystem builds a per-cell grid (density + a species/genome
representative), reaps plants past their lifespan, and lets mature plants seed
offspring — cross-pollinating with a mature same-species neighbour in dispersal
range (falling back to self-pollination by gene) into a nearby land cell under
the density cap. A shared PlantFactory creates every plant (initial scatter,
births, save restore) and tints recessive-morph plants. Growth keeps aging
mature plants so they can die of old age. The whole population (genomes,
positions, age, stage) is saved to WorldSave and restored on load instead of
re-scattering from the seed. plants.json carries the new genes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:21:38 +03:00
31 changed files with 2178 additions and 1150 deletions
+84 -84
View File
@@ -41,17 +41,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin
EndProject
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
+73
View File
@@ -0,0 +1,73 @@
{
"type": "Gene",
// Организм-агностичные гены: каждый ген задаёт, как генерируются/мутируют аллели и как ген
// вкладывается в признаки (effects: имя признака → формула; value — выраженное значение гена).
// Этот набор покрывает измерения генома растения (его используют системы роста/жизненного цикла).
"defs": [
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 },
{ "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight",
"default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"],
"effects": { "optimalLight": "value" } },
{ "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance",
"default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"],
"effects": { "lightTolerance": "value" } },
{ "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature",
"default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"],
"effects": { "optimalTemperature": "value" } },
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
"effects": { "temperatureTolerance": "value" } },
{ "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
"effects": { "optimalFertility": "value" } },
{ "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance",
"default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
"effects": { "fertilityTolerance": "value" } },
{ "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor",
"default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"],
"effects": { "vigor": "value" } },
{ "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan",
"default": 160, "min": 5, "max": 500, "tags": ["lifecycle"],
"effects": { "lifespan": "value" } },
{ "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange",
"default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"],
"effects": { "dispersalRange": "value" } },
{ "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval",
"default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"],
"effects": { "reproduceInterval": "value" } },
{ "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination",
"default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
"effects": { "selfPollination": "value" } },
{ "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate",
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
"effects": { "mutationRate": "value" } },
// --- Контент (фаза G4): плодоношение, добыча, цвет ---
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
"effects": { "fruitYield": "value" } },
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
"effects": { "fruitSeason": "value" } },
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
"effects": { "harvestAmount": "value" } },
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
"effects": { "leafHue": "value" } },
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
"default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"],
"effects": { "hardiness": "gsum('Gene.*Tolerance')" } },
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
"effects": { "variant": "value" } }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"type": "Patch",
// Контент-патчи (фаза G5): применяются ко всем дефам типа defType, чьё имя подходит под regex match,
// проставляя поля set. Демонстрация: даём кустам древесину при сборе (веточки).
"patches": [
{ "defType": "Plant", "match": "Bush.*", "set": { "harvestProduct": "ProductWood" } }
]
}
+15 -3
View File
@@ -2,8 +2,12 @@
"type": "Plant",
"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" }
+11
View File
@@ -0,0 +1,11 @@
{
"type": "Product",
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
// количество задаётся генами (harvestAmount / fruitYield).
"defs": [
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }
]
}
+1 -1
View File
@@ -17,6 +17,6 @@
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
] },
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2 }
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
]
}
+4
View File
@@ -67,6 +67,10 @@
"plant.stage.sprout": "sprout",
"plant.stage.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",
+4
View File
@@ -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": "олень",
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<SkinSet>
<lifeStage>
<appliesToAll>true</appliesToAll>
<variants>
@@ -24,9 +23,6 @@
<commonality>0.4</commonality>
<texName>GuineaPig3</texName>
</skin>
</variants>
</lifeStage>
</SkinSet>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<SkinSet>
<lifeStage>
<appliesToAll>true</appliesToAll>
<variants>
@@ -39,8 +38,6 @@
<commonality>0.25</commonality>
<texName>Horse6</texName>
</skin>
</variants>
</lifeStage>
</SkinSet>
+1 -1
Submodule engine updated: 3438ed77f6...d044cafad9
+30
View File
@@ -1,7 +1,33 @@
using System.Text.Json;
using MrGameEng.Genetics;
namespace LittleSim.App;
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
public sealed class PlantSave
{
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
public string Species { get; set; } = "";
/// <summary>Позиция X в мировых координатах.</summary>
public float X { get; set; }
/// <summary>Позиция Y в мировых координатах.</summary>
public float Y { get; set; }
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays { get; set; }
/// <summary>Текущая стадия роста.</summary>
public int Stage { get; set; }
/// <summary>Плодородность клетки.</summary>
public float CellFertility { get; set; }
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
public Dictionary<string, Allele> Genome { get; set; } = new();
}
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
public sealed class PawnSave
{
@@ -70,6 +96,9 @@ public sealed class WorldSave
/// <summary>Снимок жителей.</summary>
public List<PawnSave> Pawns { get; set; } = [];
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
public List<PlantSave> Plants { get; set; } = [];
/// <summary>Конфиг мира для пересоздания сцены.</summary>
public WorldConfig ToConfig() =>
new()
@@ -90,6 +119,7 @@ public sealed class SaveStore
{
WriteIndented = true,
PropertyNameCaseInsensitive = true,
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
};
private readonly string _directory;
+11
View File
@@ -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<TerrainDef>("Terrain");
defs.RegisterType<GeneDef>("Gene");
defs.RegisterType<ProductDef>("Product");
defs.RegisterType<PlantDef>("Plant");
defs.RegisterType<PawnDef>("Pawn");
defs.RegisterType<WorldPresetDef>("WorldPreset");
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
defs.RegisterValidator(
"Product",
"defName",
"^Product",
"product defName must start with 'Product'"
);
defs.Load(mods);
var languages = new LanguageManager(defaultLanguage: "ru");
+50
View File
@@ -27,6 +27,9 @@ public sealed class TerrainDef : Def
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
public bool IsLand { get; init; }
/// <summary>Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.</summary>
public bool BlocksLight { get; init; }
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
public string? Surface { get; init; }
@@ -63,6 +66,17 @@ public sealed class PlantGrowthStage
public string? Label { get; init; }
}
/// <summary>
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
/// </summary>
public sealed class ProductDef : Def
{
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
public string Kind { get; init; } = "";
}
/// <summary>
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
@@ -91,8 +105,38 @@ public sealed class GenomeDef
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
public float Vigor { get; init; } = 1f;
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
public float Lifespan { get; init; } = 80f;
/// <summary>Дальность расселения семян (в клетках).</summary>
public float DispersalRange { get; init; } = 2f;
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
public float ReproduceInterval { get; init; } = 8f;
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
public float SelfPollination { get; init; } = 0.4f;
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
public float MutationRate { get; init; } = 0.05f;
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
public float VariantChance { get; init; } = 0.15f;
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
public float Spread { get; init; } = 0.08f;
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
public float FruitYield { get; init; }
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
public float FruitSeason { get; init; } = 1f;
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
public float HarvestAmount { get; init; } = 1f;
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
public float LeafHue { get; init; } = 0.33f;
}
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
@@ -112,6 +156,12 @@ public sealed class PlantDef : Def
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
public GenomeDef Genome { get; init; } = new();
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
public string? HarvestProduct { get; init; }
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
public string? FruitProduct { get; init; }
}
/// <summary>
+62 -1
View File
@@ -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
/// <summary>Стадии роста по порядку; последняя — терминальная.</summary>
public required Stage[] Stages { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи (геном из общих <see cref="GeneDef"/>).</summary>
public required GenomeTemplate Template { get; init; }
/// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary>
public float MaturityDays => Stages[^1].EnterDay;
@@ -42,15 +46,28 @@ public sealed class PlantSet
private readonly Species[] _species;
private readonly Dictionary<PlantDef, int> _index = new();
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков растений.</summary>
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
/// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary>
public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device)
{
var genes = content
.Defs.All<GeneDef>()
.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
GeneRegistry = genes;
var defs = content.Defs.All<PlantDef>().ToArray();
_species = new Species[defs.Length];
for (var i = 0; i < defs.Length; i++)
{
var def = defs[i];
_species[i] = new Species { Def = def, Stages = BuildStages(def, atlases, device) };
_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
/// <summary>Индекс вида по дефу.</summary>
public int IndexOf(PlantDef def) => _index[def];
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
private static GenomeTemplate BuildTemplate(
GenomeDef g,
IReadOnlyDictionary<string, GeneDef> genes
)
{
GeneDef Gene(string id) =>
genes.TryGetValue(id, out var def)
? def
: throw new KeyNotFoundException(
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?"
);
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
new(Gene(id), baseValue, g.Spread);
return new GenomeTemplate([
Numeric("GeneOptimalLight", g.OptimalLight),
Numeric("GeneLightTolerance", g.LightTolerance),
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
Numeric("GeneOptimalFertility", g.OptimalFertility),
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
Numeric("GeneVigor", g.Vigor),
Numeric("GeneLifespan", g.Lifespan),
Numeric("GeneDispersalRange", g.DispersalRange),
Numeric("GeneReproduceInterval", g.ReproduceInterval),
Numeric("GeneSelfPollination", g.SelfPollination),
Numeric("GeneMutationRate", g.MutationRate),
// Контент (G4): плодоношение, добыча, цвет.
Numeric("GeneFruitYield", g.FruitYield),
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
Numeric("GeneHarvestAmount", g.HarvestAmount),
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
new GenomeTemplate.Entry(
Gene("GeneMorph"),
0f,
0f,
[1f - g.VariantChance, g.VariantChance]
),
]);
}
private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device)
{
// Без явных стадий растение существует как одна терминальная стадия из базовой текстуры.
+27
View File
@@ -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<GeneDef>();
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
var traits = Phenotype.Compute(Genome.Generate(genes, new Random(1)), registry);
Console.WriteLine(
$"content ok: {content.Defs.TypeKeys.Count} def types, {genes.Count} genes, "
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
);
return;
}
catch (Exception error)
{
Console.Error.WriteLine($"content FAILED: {error.Message}");
Environment.Exit(1);
return;
}
}
// --connect [ws://host:port] — после загрузки контента сразу в сетевую сцену
// (см. MultiplayerScene); без аргументов — обычный запуск в главное меню.
string? connect = null;
+1
View File
@@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using LittleSim.App;
using LittleSim.Content;
+10 -20
View File
@@ -36,29 +36,19 @@ internal static class ScatterSpawner
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
var 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
);
}
}
+329 -24
View File
@@ -10,6 +10,8 @@ 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;
@@ -26,8 +28,9 @@ namespace LittleSim.Scenes;
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие
/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн.
/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
/// </summary>
public sealed class WorldScene : Scene
{
@@ -39,6 +42,9 @@ public sealed class WorldScene : Scene
/// </summary>
public const float SecondsPerDay = 480f;
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
public const int DensityCap = 4;
private readonly WorldConfig _config;
private readonly WorldSave? _save;
private readonly RectF _bounds;
@@ -47,6 +53,12 @@ public sealed class WorldScene : Scene
private PauseMenu _pause = null!;
private readonly List<Action> _speedRefreshers = [];
private PlantSet _plants = null!;
private float[] _cellFertility = [];
private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
/// <summary>Новый мир из конфига.</summary>
public WorldScene(WorldConfig config)
: this(config, null) { }
@@ -77,12 +89,34 @@ public sealed class WorldScene : Scene
var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
var dayNight = new DayNight(calendar, DayNightSettings.Default);
// Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed).
var plants = new PlantSet(content, atlases, device);
var random = new Random(_config.Seed);
BuildTerrain(content, atlases, device, assets, plants, random);
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
_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));
@@ -103,8 +137,47 @@ public sealed class WorldScene : Scene
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, dayNight, CellSize));
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
UpdateSystems.Add(
new PlantLifecycleSystem(
Store,
_plants,
calendar,
Context.Clock,
_config.Width,
_config.Height,
CellSize,
_cellFertility,
_cellLand,
DensityCap,
_config.Seed
)
);
UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
);
@@ -128,17 +201,17 @@ public sealed class WorldScene : Scene
}
/// <summary>
/// Строит одну сущность-<see cref="Tilemap"/>: тайлсет из дефов рельефа (поверхность из
/// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а
/// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости).
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
/// </summary>
private void BuildTerrain(
GameContent content,
ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
AssetManager assets,
PlantSet plants,
Random random
Random? scatterRandom
)
{
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
@@ -161,21 +234,33 @@ public sealed class WorldScene : Scene
_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];
ScatterSpawner.Spawn(
this,
content,
plants,
terrain,
new Point(x, y),
CellSize,
random
);
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
);
}
}
}
@@ -255,7 +340,6 @@ public sealed class WorldScene : Scene
private string SaveWorld()
{
// Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
var save = new WorldSave
{
Name = _config.Name,
@@ -268,11 +352,98 @@ public sealed class WorldScene : Scene
ElapsedSeconds = Context.Clock.TotalTime,
};
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
Store
.Query<PlantGrowth, PlantOrganism, Transform2D>()
.ForEachEntity(
(
ref PlantGrowth grow,
ref PlantOrganism org,
ref Transform2D transform,
Entity _
) =>
{
save.Plants.Add(
new PlantSave
{
Species = _plants[grow.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
AgeDays = grow.AgeDays,
Stage = grow.Stage,
CellFertility = grow.CellFertility,
Genome = org.Genome.ToDictionary(),
}
);
}
);
new SaveStore().Write(save);
Log.Info($"World '{_config.Name}' saved");
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
return _config.Name;
}
private void RestorePlants(GameContent content)
{
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
foreach (var plant in _save!.Plants)
{
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
{
continue; // вид пропал (мод убрали) — пропускаем растение
}
var index = _plants.IndexOf(def);
var genome = plant.Genome is { Count: > 0 }
? new Genome(plant.Genome)
: _plants[index].Template.Generate(fallback);
PlantFactory.Create(
Store,
_plants,
index,
new Vector2(plant.X, plant.Y),
plant.AgeDays,
genome,
plant.CellFertility,
CellSize
);
}
}
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
private bool[] BuildOccluders()
{
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
Store
.Query<PlantGrowth, Transform2D>()
.ForEachEntity(
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
{
var species = _plants[grow.Species];
if (
species.Def.TrunkRadiusCells <= 0f
|| grow.Stage < species.Stages.Length - 1
)
{
return; // затеняют только зрелые деревья (со стволом)
}
var cx = Math.Clamp(
(int)(transform.Position.X / CellSize),
0,
_config.Width - 1
);
var cy = Math.Clamp(
(int)(transform.Position.Y / CellSize),
0,
_config.Height - 1
);
_cellOccluder[cy * _config.Width + cx] = true;
}
);
return _cellOccluder;
}
private void Switch(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
@@ -303,10 +474,144 @@ public sealed class WorldScene : Scene
);
}
);
console.Register(
"formula",
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
(c, args) =>
{
if (args.Length == 0)
{
c.WriteLine(
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
);
return;
}
var expression = string.Join(' ', args);
try
{
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
}
catch (FormulaException e)
{
c.WriteLine($"error: {e.Message}");
}
}
);
console.Register(
"gene",
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
(c, args) => RunGeneDemo(c, content, args)
);
console.Register(
"plant",
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
(c, args) => RunPlantDemo(c, content, args)
);
console.Register(
"menu",
"menu — return to the main menu",
(_, _) => Switch(new MainMenuScene())
);
}
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
{
var genes = content.Defs.All<GeneDef>();
if (genes.Count == 0)
{
console.WriteLine("no Gene defs loaded");
return;
}
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
var random = new Random(seed);
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
var parentA = Genome.Generate(genes, random);
foreach (var gene in genes)
{
var allele = parentA[gene.DefName];
console.WriteLine(
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
);
}
console.WriteLine("traits:");
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
{
console.WriteLine($" {trait} = {value:0.###}");
}
var parentB = Genome.Generate(genes, random);
var child = Genome.Breed(parentA, parentB, registry, random);
var childTraits = Phenotype.Compute(child, registry);
console.WriteLine(
$"bred child: {child.Alleles.Count} genes, "
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
);
}
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Plant");
if (args.Length == 0)
{
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
{
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var index = _plants.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
var genome = _plants[index].Template.Generate(new Random(seed));
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
console.WriteLine(
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
);
console.WriteLine(
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
);
if (def.HarvestProduct is { } harvest)
{
console.WriteLine(
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
);
}
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
{
var season = content.Languages.Get(
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
);
console.WriteLine(
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
);
}
else
{
console.WriteLine(" barren (no fruit)");
}
}
private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
: productDefName;
}
+71
View File
@@ -0,0 +1,71 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
using LittleSim.Scenes;
using Microsoft.Xna.Framework;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Единая точка создания сущности-растения: спрайт стадии по возрасту, признаки из генома формулами
/// генов, тинт по морфе и компоненты роста/организма. Переиспользуется начальным скаттером,
/// размножением и загрузкой сейва — чтобы все растения собирались одинаково.
/// </summary>
public static class PlantFactory
{
// Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым.
private static readonly Color VariantTint = new(214, 150, 176);
// Якоря оттенка листвы: тёплый жёлто-зелёный → холодный сине-зелёный (по гену leafHue).
private static readonly Color LeafWarm = new(170, 200, 90);
private static readonly Color LeafCool = new(90, 170, 150);
/// <summary>Создаёт растение вида <paramref name="species"/> с заданным возрастом и геномом.</summary>
public static Entity Create(
EntityStore store,
PlantSet plants,
int species,
Vector2 position,
float ageDays,
Genome genome,
float cellFertility,
int cellSize
)
{
var sp = plants[species];
var stage = sp.StageAt(ageDays);
var resolved = sp.Stages[stage];
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, plants.GeneRegistry));
var sprite = new Sprite(resolved.Region, GameLayers.Beings);
sprite.CenterOrigin();
sprite.Color = Tint(traits);
return store.CreateEntity(
new Transform2D(
position,
scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width)
),
sprite,
new PlantGrowth
{
Species = species,
Stage = stage,
AgeDays = ageDays,
CellFertility = cellFertility,
},
new PlantOrganism { Genome = genome, Traits = traits },
new Fruiting()
);
}
// Цвет растения из генов: лёгкий оттенок листвы (ген leafHue) поверх текстуры; рецессивная морфа
// дополнительно сдвигает тинт в свою сторону.
private static Color Tint(in PlantPhenotype traits)
{
var leaf = Color.Lerp(LeafWarm, LeafCool, Math.Clamp(traits.LeafHue, 0f, 1f));
var tint = Color.Lerp(Color.White, leaf, 0.35f);
return traits.IsVariant ? Color.Lerp(tint, VariantTint, 0.6f) : tint;
}
}
+62
View File
@@ -0,0 +1,62 @@
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using MrGameEng.AI;
using MrGameEng.Core;
namespace LittleSim.Sim;
/// <summary>
/// Плодоношение (фаза G4): у зрелого растения с признаком <see cref="PlantPhenotype.FruitYield"/> &gt; 0
/// в его генетический сезон (<see cref="PlantPhenotype.FruitSeason"/>) копятся зрелые плоды до потолка
/// <see cref="PlantPhenotype.FruitYield"/>; вне сезона плоды опадают. Полностью управляется генами через
/// признаки — система лишь применяет их к компоненту <see cref="Fruiting"/>. Незрелые/бесплодные
/// растения остаются с нулём.
/// </summary>
public sealed class PlantFruitingSystem(PlantSet plants, Calendar calendar, Climate climate)
: QuerySystem<PlantGrowth, PlantOrganism, Fruiting>
{
// За сколько игровых дней набирается/опадает полный урожай.
private const float RipenDays = 10f;
private const float DropDays = 20f;
protected override void OnUpdate()
{
var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
if (deltaDays <= 0f)
{
return; // пауза
}
var season = (int)climate.Season;
foreach (var (growths, organisms, fruits, _) in Query.Chunks)
{
var g = growths.Span;
var orgs = organisms.Span;
var f = fruits.Span;
for (var i = 0; i < g.Length; i++)
{
ref var traits = ref orgs[i].Traits;
var mature = g[i].Stage >= plants[g[i].Species].Stages.Length - 1;
if (!mature || traits.FruitYield < 1f)
{
f[i].RipeFruit = 0f; // незрелое или бесплодное — плодов нет
continue;
}
ref var ripe = ref f[i].RipeFruit;
if (season == traits.FruitSeason)
{
ripe = MathF.Min(
traits.FruitYield,
ripe + deltaDays * (traits.FruitYield / RipenDays)
);
}
else
{
ripe = MathF.Max(0f, ripe - deltaDays * (traits.FruitYield / DropDays));
}
}
}
}
}
-90
View File
@@ -1,90 +0,0 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
namespace LittleSim.Sim;
/// <summary>
/// Диплоидный геном растения: по паре аллелей на ген. Для числовых генов экспрессия гибридная —
/// фенотип есть среднее двух аллелей (дискретные доминант/рецессивные гены добавятся с размножением).
/// Аллели — наследуемые данные (правятся в инспекторе); фенотип-свойства читает система роста.
/// </summary>
public struct PlantGenome : IComponent
{
/// <summary>Аллели оптимума освещённости (0..1).</summary>
public float OptimalLightA;
public float OptimalLightB;
/// <summary>Аллели толерантности по свету.</summary>
public float LightToleranceA;
public float LightToleranceB;
/// <summary>Аллели оптимума температуры (°C).</summary>
public float OptimalTemperatureA;
public float OptimalTemperatureB;
/// <summary>Аллели толерантности по температуре (°C).</summary>
public float TemperatureToleranceA;
public float TemperatureToleranceB;
/// <summary>Аллели оптимума плодородности.</summary>
public float OptimalFertilityA;
public float OptimalFertilityB;
/// <summary>Аллели толерантности по плодородности.</summary>
public float FertilityToleranceA;
public float FertilityToleranceB;
/// <summary>Аллели бодрости роста.</summary>
public float VigorA;
public float VigorB;
/// <summary>Фенотип: оптимальная освещённость.</summary>
public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f;
/// <summary>Фенотип: толерантность по свету.</summary>
public readonly float LightTolerance => (LightToleranceA + LightToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная температура.</summary>
public readonly float OptimalTemperature => (OptimalTemperatureA + OptimalTemperatureB) * 0.5f;
/// <summary>Фенотип: толерантность по температуре.</summary>
public readonly float TemperatureTolerance =>
(TemperatureToleranceA + TemperatureToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная плодородность.</summary>
public readonly float OptimalFertility => (OptimalFertilityA + OptimalFertilityB) * 0.5f;
/// <summary>Фенотип: толерантность по плодородности.</summary>
public readonly float FertilityTolerance => (FertilityToleranceA + FertilityToleranceB) * 0.5f;
/// <summary>Фенотип: бодрость роста.</summary>
public readonly float Vigor => (VigorA + VigorB) * 0.5f;
/// <summary>
/// Генерирует особь из базового генома вида: каждый аллель — база ± относительный разброс
/// (<see cref="GenomeDef.Spread"/>). Сидируется переданным <paramref name="random"/> — детерминизм.
/// </summary>
public static PlantGenome FromDef(GenomeDef def, Random random)
{
float Allele(float baseValue) =>
baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue;
return new PlantGenome
{
OptimalLightA = Allele(def.OptimalLight),
OptimalLightB = Allele(def.OptimalLight),
LightToleranceA = Allele(def.LightTolerance),
LightToleranceB = Allele(def.LightTolerance),
OptimalTemperatureA = Allele(def.OptimalTemperature),
OptimalTemperatureB = Allele(def.OptimalTemperature),
TemperatureToleranceA = Allele(def.TemperatureTolerance),
TemperatureToleranceB = Allele(def.TemperatureTolerance),
OptimalFertilityA = Allele(def.OptimalFertility),
OptimalFertilityB = Allele(def.OptimalFertility),
FertilityToleranceA = Allele(def.FertilityTolerance),
FertilityToleranceB = Allele(def.FertilityTolerance),
VigorA = Allele(def.Vigor),
VigorB = Allele(def.Vigor),
};
}
}
+19 -17
View File
@@ -11,17 +11,18 @@ namespace LittleSim.Sim;
/// <summary>
/// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов
/// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) ·
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из генов
/// (см. <see cref="PlantGenome"/>). Свет/температура в фазе B глобальные (день/ночь и сезон); стадии
/// и спрайт переключаются по накопленному возрасту как прежде. Терминальную стадию пропускает.
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из
/// признаков (<see cref="PlantPhenotype"/>), вычисленных формулами генов. Свет локальный (лайтмап),
/// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному
/// возрасту как прежде. Терминальную стадию пропускает.
/// </summary>
public sealed class PlantGrowthSystem(
PlantSet plants,
Calendar calendar,
Climate climate,
DayNight dayNight,
Lighting lighting,
int cellSize
) : QuerySystem<PlantGrowth, PlantGenome, Sprite, Transform2D>
) : QuerySystem<PlantGrowth, PlantOrganism, Sprite, Transform2D>
{
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;
+316
View File
@@ -0,0 +1,316 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно.
/// </summary>
public sealed class PlantLifecycleSystem : BaseSystem
{
private const float IntervalDays = 0.25f;
private readonly EntityStore _store;
private readonly PlantSet _plants;
private readonly Calendar _calendar;
private readonly GameClock _clock;
private readonly int _width;
private readonly int _height;
private readonly int _cellSize;
private readonly float[] _cellFertility;
private readonly bool[] _cellLand;
private readonly int _densityCap;
private readonly Random _rng;
private readonly ArchetypeQuery<PlantGrowth, PlantOrganism, Transform2D> _query;
private readonly int[] _count;
private readonly CellRep[] _rep;
private readonly List<Entity> _deaths = [];
private readonly List<Candidate> _candidates = [];
private readonly List<Birth> _births = [];
private float _accumulator;
public PlantLifecycleSystem(
EntityStore store,
PlantSet plants,
Calendar calendar,
GameClock clock,
int width,
int height,
int cellSize,
float[] cellFertility,
bool[] cellLand,
int densityCap,
int seed
)
{
_store = store;
_plants = plants;
_calendar = calendar;
_clock = clock;
_width = width;
_height = height;
_cellSize = cellSize;
_cellFertility = cellFertility;
_cellLand = cellLand;
_densityCap = densityCap;
_rng = new Random(seed);
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D>();
_count = new int[width * height];
_rep = new CellRep[width * height];
}
protected override void OnUpdateGroup()
{
_accumulator += _clock.DeltaTime / _calendar.SecondsPerDay; // прошло игровых дней (с учётом паузы/скорости)
if (_accumulator < IntervalDays)
{
return;
}
var elapsed = _accumulator;
_accumulator = 0f;
BuildGridAndCollect();
Reproduce(elapsed);
Apply();
}
// Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов.
private void BuildGridAndCollect()
{
Array.Clear(_count);
for (var c = 0; c < _rep.Length; c++)
{
_rep[c].Species = -1;
}
_deaths.Clear();
_candidates.Clear();
_births.Clear();
foreach (var (growths, organisms, transforms, entities) in _query.Chunks)
{
var g = growths.Span;
var orgs = organisms.Span;
var t = transforms.Span;
for (var i = 0; i < g.Length; i++)
{
ref var grow = ref g[i];
ref var org = ref orgs[i];
if (grow.AgeDays > org.Traits.Lifespan)
{
_deaths.Add(entities.EntityAt(i)); // умер от старости
continue;
}
var pos = t[i].Position;
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
var cell = cy * _width + cx;
_count[cell]++;
var mature = grow.Stage >= _plants[grow.Species].Stages.Length - 1;
_rep[cell] = new CellRep
{
Species = grow.Species,
Mature = mature,
Genome = org.Genome,
MutationRate = org.Traits.MutationRate,
};
if (mature)
{
_candidates.Add(
new Candidate
{
Cell = cell,
Species = grow.Species,
Genome = org.Genome,
Traits = org.Traits,
}
);
}
}
}
}
// Проход 2: зрелые сеют потомка (перекрёстно или само-) в подходящую клетку радиуса.
private void Reproduce(float elapsed)
{
foreach (var cand in _candidates)
{
var interval = MathF.Max(0.5f, cand.Traits.ReproduceInterval);
if (_rng.NextSingle() >= elapsed / interval)
{
continue; // в этот тик не сеет
}
var range = Math.Clamp((int)MathF.Round(cand.Traits.DispersalRange), 1, 6);
var cx = cand.Cell % _width;
var cy = cand.Cell / _width;
if (!TryFindPartner(cx, cy, range, cand.Species, out var partner))
{
if (_rng.NextSingle() >= cand.Traits.SelfPollination)
{
continue; // нет партнёра и самоопыление не удалось
}
// самоопыление — партнёр это сам кандидат
partner = new Partner
{
Genome = cand.Genome,
MutationRate = cand.Traits.MutationRate,
};
}
if (!TryPickTarget(cx, cy, range, out var tcell))
{
continue; // некуда сеять (нет суши/свободного места рядом)
}
// Темп мутаций потомка — средний эволюционирующий признак родителей (ген управляет мутацией).
var mutationChance = (cand.Traits.MutationRate + partner.MutationRate) * 0.5f;
var child = Genome.Breed(
cand.Genome,
partner.Genome,
_plants.GeneRegistry,
_rng,
mutationChance
);
var tx = tcell % _width;
var ty = tcell / _width;
var position = new Vector2(
(tx + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize,
(ty + 0.3f + _rng.NextSingle() * 0.4f) * _cellSize
);
_births.Add(
new Birth
{
Species = cand.Species,
Position = position,
Genome = child,
Fertility = _cellFertility[tcell],
}
);
_count[tcell]++; // резервируем место, чтобы не переполнить клетку за тик
}
}
private bool TryFindPartner(int cx, int cy, int range, int species, out Partner partner)
{
for (var dy = -range; dy <= range; dy++)
{
for (var dx = -range; dx <= range; dx++)
{
if (dx == 0 && dy == 0)
{
continue; // партнёр — другая клетка (приоритет перекрёстного)
}
var nx = cx + dx;
var ny = cy + dy;
if (nx < 0 || nx >= _width || ny < 0 || ny >= _height)
{
continue;
}
ref var rep = ref _rep[ny * _width + nx];
if (rep.Species == species && rep.Mature)
{
partner = new Partner { Genome = rep.Genome, MutationRate = rep.MutationRate };
return true;
}
}
}
partner = default;
return false;
}
private bool TryPickTarget(int cx, int cy, int range, out int cell)
{
for (var attempt = 0; attempt < 6; attempt++)
{
var tx = cx + _rng.Next(-range, range + 1);
var ty = cy + _rng.Next(-range, range + 1);
if (tx < 0 || tx >= _width || ty < 0 || ty >= _height)
{
continue;
}
var candidate = ty * _width + tx;
if (_cellLand[candidate] && _count[candidate] < _densityCap)
{
cell = candidate;
return true;
}
}
cell = -1;
return false;
}
private void Apply()
{
foreach (var entity in _deaths)
{
entity.DeleteEntity();
}
foreach (var birth in _births)
{
PlantFactory.Create(
_store,
_plants,
birth.Species,
birth.Position,
ageDays: 0f,
birth.Genome,
birth.Fertility,
_cellSize
);
}
}
private struct CellRep
{
public int Species;
public bool Mature;
public Genome Genome;
public float MutationRate;
}
private struct Candidate
{
public int Cell;
public int Species;
public Genome Genome;
public PlantPhenotype Traits;
}
private struct Partner
{
public Genome Genome;
public float MutationRate;
}
private struct Birth
{
public int Species;
public Vector2 Position;
public Genome Genome;
public float Fertility;
}
}
+87
View File
@@ -0,0 +1,87 @@
using System.Collections.Generic;
using Friflo.Engine.ECS;
using MrGameEng.Genetics;
namespace LittleSim.Sim;
/// <summary>
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>genes.json</c>):
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
/// Имена полей соответствуют именам признаков в эффектах генов.
/// </summary>
public struct PlantPhenotype
{
public float OptimalLight;
public float LightTolerance;
public float OptimalTemperature;
public float TemperatureTolerance;
public float OptimalFertility;
public float FertilityTolerance;
public float Vigor;
public float Lifespan;
public float DispersalRange;
public float ReproduceInterval;
public float SelfPollination;
public float MutationRate;
// --- Контент (G4): плодоношение, добыча, цвет ---
public float FruitYield;
public int FruitSeason;
public float HarvestAmount;
public float LeafHue;
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant;
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{
float T(string name) => traits.GetValueOrDefault(name);
return new PlantPhenotype
{
OptimalLight = T("optimalLight"),
LightTolerance = T("lightTolerance"),
OptimalTemperature = T("optimalTemperature"),
TemperatureTolerance = T("temperatureTolerance"),
OptimalFertility = T("optimalFertility"),
FertilityTolerance = T("fertilityTolerance"),
Vigor = T("vigor"),
Lifespan = T("lifespan"),
DispersalRange = T("dispersalRange"),
ReproduceInterval = T("reproduceInterval"),
SelfPollination = T("selfPollination"),
MutationRate = T("mutationRate"),
FruitYield = T("fruitYield"),
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
HarvestAmount = T("harvestAmount"),
LeafHue = T("leafHue"),
IsVariant = T("variant") >= 0.5f,
};
}
}
/// <summary>
/// Компонент-организм растения: его управляемый <see cref="Genome"/> (для размножения, сейва и
/// пересчёта) и выраженный фенотип <see cref="Traits"/>, по которому работают системы. Заменяет
/// прежний фиксированный <c>PlantGenome</c> — теперь геном переменного состава и признаки из формул
/// генов (фаза G3). Геном — ссылочный объект, поэтому компонент несёт ссылку, а не копию.
/// </summary>
public struct PlantOrganism : IComponent
{
/// <summary>Геном особи (аллели по генам); общий источник истины для признаков и размножения.</summary>
public Genome Genome;
/// <summary>Выраженные признаки — то, что читают системы роста/жизненного цикла.</summary>
public PlantPhenotype Traits;
}
/// <summary>
/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его
/// сезон плодоношения (признак <see cref="PlantPhenotype.FruitSeason"/>) до потолка
/// <see cref="PlantPhenotype.FruitYield"/>, вне сезона опадает. Управляется <c>PlantFruitingSystem</c>.
/// </summary>
public struct Fruiting : IComponent
{
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
public float RipeFruit;
}