Compare 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 Fable 5 580cb6ccc9 Browser multiplayer client: promote the KNI spike to src/LittleSim.Web
LittleSim.Web (Blazor WASM + KNI/WebGL) is a real network client now: it
connects to the dedicated server over WebSocket (ClientWebSocket maps to
the browser socket), applies MrGameEng.Net delta snapshots into its
EntityStore and draws pawns through SpriteBatch — fatigue dims them just
like on desktop. The server address comes from ?server=ws://host:port
in the page URL, defaulting to the page's host on port 9050. The net
contract is mirrored in NetContract.cs (KNI and DesktopGL assemblies
can't mix until the graphics libraries build per platform) with loud
keep-in-sync comments on both sides.

Both clients now smooth replicated positions between 10 Hz snapshots:
NetLerp + NetSmoothingSystem lerp the visual position toward the latest
server position every frame (exponential, ~0.25 s to converge).

Verified against a live LittleSim.Server --listen: the browser client
connects (server log), draws ~3.3k lit pixels of pawns whose layout
changes between samples, and survives 400+ ticks without errors. Found
along the way: requestAnimationFrame freezes in hidden windows — the
game loop only runs while the tab is visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:04:11 +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 Fable 5 e8273f9ffa Multiplayer: networked dedicated server + desktop client over MrGameEng.Net
Engine bump brings MrGameEng.Net (WebSocket transport, RFC 6455 server,
delta component replication) — this commit is its showcase.

LittleSim.Server --listen runs the world online: 24 pawns wander, tire
and rest on the engine's fixed-tick HeadlessHost (the same Wander/
Needs/Decision systems the windowed world uses), a WebSocketServer
accepts clients and a ReplicationServer ships Transform2D + PawnNeeds
deltas at 10 snapshots/s per the shared NetSchema. --probe is the CLI
check: it connects, listens for a second and prints replicated pawns
with positions sampled twice to show the world is alive.

The game gains MultiplayerScene (dotnet run --project src/LittleSim --
--connect [ws://host:port]): simulation stays on the server, the client
applies snapshots into its scene store, decorates spawned entities with
sprites and reuses PawnAppearanceSystem so replicated fatigue darkens
pawns locally. HUD strings go through ru/en localization; the `net`
console command reports connection state and entity count. Esc returns
to the main menu.

Verified end to end: probe sees 24 pawns moving between samples; the
windowed client connects and runs against a live local server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:45:35 +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 Fable 5 ce8f8ba2ed KNI spike: engine core + Friflo render via WebGL in the browser
spikes/KniWeb (outside LittleSim.sln): a kni-blazor-gl template project
(KNI 4.2.9001, net8.0) referencing MrGameEng.Core directly. A mini-host
in the GameHost mold drives EngineContext/GameClock/Scene phases over
KNI's Game; the scene moves 300 Friflo entities in the update phase and
draws them with SpriteBatch (WebGL). Verified in a real browser: sprites
render and animate, browser console is clean.

Decision (docs/web-client.md): path A — KNI — is the primary route for
the web client; the core runs in Blazor WASM unchanged thanks to the
Core/Host split. Known follow-ups: per-platform compilation of the
graphics libraries against nkast.* packages, shader compatibility for
Renderer2D, HTTP-served content instead of the filesystem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:27:40 +03:00
Leonid PershinandClaude Fable 5 8a7e2cce52 Adopt the engine Core/Host split; add LittleSim.Server headless prototype
Engine bump: MrGameEng.Core is now platform-free (Friflo only), the
windowed MonoGame host lives in the new MrGameEng.Host library, and the
core gains HeadlessHost — a fixed-timestep loop without a window or GPU.

Game side: scenes switch to Transitions.Fade from the Host library,
WorldScene reads the graphics device via Context.GetGraphicsDevice(),
GameContent.Load(buildAtlases: false) skips atlas building for headless
runs.

LittleSim.Server is the dedicated-server seed and the showcase for
HeadlessHost: it loads mods/defs without textures and fast-forwards the
world calendar and climate on a fixed tick (~3.6M ticks/s in Debug):

  dotnet run --project src/LittleSim.Server -- --days 10 --tps 60

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:13:40 +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
Leonid PershinandClaude Opus 4.8 bfe1b139f0 Phase B: plant genome drives growth via environment suitability
Bump the engine submodule to d0104df (Suitability.Gaussian). Each plant now
carries a diploid PlantGenome (allele pairs, phenotype = allele average) seeded
from a per-species GenomeDef baseline at spawn. PlantGrowthSystem replaces the
flat fertility multiplier with vigor times a product of Gaussian suitabilities
for light (day/night), temperature (climate season) and the cell's fertility —
so plants grow faster in their preferred light/warmth/soil and crawl outside it.
Genomes are visible and editable in the ECS inspector. plants.json carries the
species genomes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:52:27 +03:00
Leonid PershinandClaude Opus 4.8 aff5a947c5 Phase A: wire climate + day/night into the world; design doc
Bump the engine submodule to a684c23 (Climate + day/night ambient). The
world scene now registers UseClimate and UseDayNight, so it darkens at
night and the HUD shows the season and temperature alongside the date.
Add docs/растения.md capturing the whole plant-ecosystem design (growth as
a suitability product, hybrid Mendelian genome, climate, lighting with
shadows, reproduction, persistence) and the A-D phase plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 16:46:05 +03:00
Leonid PershinandClaude Opus 4.8 147ab203f1 Soil fertility scales calendar-driven plant growth
Bump the engine submodule to f39ee9e (half-texel UV inset that removes the
black tile seams seen while zooming). Add a Fertility multiplier to
TerrainDef (forest soil rich, sand/mountain poor); plants record their
cell's fertility as PlantGrowth.GrowthRate at spawn, and the growth system
multiplies the per-frame calendar day-delta by it — so growth stays tied to
the in-game calendar and runs faster on fertile soil.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:45:00 +03:00
Leonid PershinandClaude Opus 4.8 9f67c478a2 Zoom the god camera toward the cursor
The mouse-wheel zoom now keeps the world point under the cursor fixed
(cursor-anchored zoom) by shifting the camera position after applying the
new zoom, instead of zooming around the camera centre. GodCameraSystem
takes the renderer to map the cursor to world space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:29:49 +03:00
Leonid PershinandClaude Opus 4.8 26e234a35e Show calendar date/time in the world HUD; base speed 3 in-game min/sec
Bump the engine submodule to 4b91b60 (calendar time of day) and display
the current date and time in the world HUD (День N, HH:MM) instead of just
the day number. Set the day length to 480 scaled seconds so the base (x1)
speed runs at 3 in-game minutes per real second.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:10:23 +03:00
Leonid PershinandClaude Opus 4.8 aab59723d3 Bump engine to 44019a7: usable inspector field editing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:05:56 +03:00
Leonid PershinandClaude Opus 4.8 02e342cb73 Refactor plant growth system to enhance data-driven mechanics
Update the PlantGrowth component and PlantGrowthSystem to improve plant growth stages based on the calendar. Introduce a new PlantDef structure with a Stages list for texture, size, and growth duration, allowing for more dynamic plant behavior. Adjust WorldScene to utilize the updated growth logic, ensuring varied initial maturity and seamless integration with the calendar system.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 12:56:43 +03:00
Leonid PershinandClaude Opus 4.8 d0f8fec273 Add the F1 ECS inspector overlay to the world scene
Bump the engine submodule to 09dbdfa (MrGameEng.Inspector) and wire the
new DevTools-style ECS debugger into WorldScene via UseInspector(renderer),
before UseDevConsole so the console still draws on top. The HUD controls
hint now mentions F1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 12:46:15 +03:00
Leonid PershinandClaude Opus 4.8 1030f27c50 Bring plants back as data-driven growth stages over the calendar
Bump the engine submodule to f791ee6 (Calendar) and use it to drive plant
growth. PlantDef gains a Stages list (texture/size/growDays per stage,
inheritable via abstract parents in plants.json); a resolved PlantSet
table turns those into atlas regions and day thresholds at scene load.

New PlantGrowth component plus PlantGrowthSystem advance each plant by
in-game days from the Calendar and swap its sprite/scale at stage borders.
WorldScene scatters plants again with a seed-deterministic, varied initial
maturity, registers the calendar and growth system, and the HUD now shows
the current day. Stage durations and visuals are pure data in plants.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 09:47:18 +03:00
Leonid PershinandClaude Opus 4.8 923644dd19 Rework world scene into a textured tile terrain on engine WorldGen
Bump the engine submodule to 5365945 (MrGameEng.WorldGen) and drive
terrain from it: WorldGenerator is now a thin adapter over the engine's
seed-based Perlin/fBm/island HeightmapGenerator instead of the old
random+smoothing pass.

Strip WorldScene down to terrain + camera: render the world as a single
Tilemap entity using per-biome surface textures from the atlas (water
falls back to a tinted tile), and remove pawns, plants, AI systems,
population and save-restore for now. Delete the TerrainScene demo (its
tile approach now lives in WorldScene) along with its console command
and the hud.terrain/population localization strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 09:24:32 +03:00
Leonid Pershin 9433db6212 Update engine submodule to latest commit ef1111b, reflecting recent changes. 2026-06-12 08:41:46 +03:00
Leonid Pershin dc13b9b8c3 Enhance localization and world generation features
- Added new UI strings for menus, settings, and pause functionality in both English and Russian localization files.
- Introduced a new WorldPresetDef class to define world size and population settings, allowing for dynamic world creation.
- Updated world generation logic to support customizable terrain smoothing and population settings based on user-defined presets.
- Refactored WorldScene to utilize the new WorldConfig structure for improved world initialization and loading.

This commit improves the user experience by providing more options for world creation and enhancing the overall localization of the game.
2026-06-12 08:41:30 +03:00
Leonid Pershin 0d1b21ffeb Update engine submodule to latest commit 3f3c020, reflecting recent changes. 2026-06-12 07:47:19 +03:00
Leonid Pershin dd3e9e400d Update project references in LittleSim.sln and LittleSim.csproj to include new MrGameEng.Content and MrGameEng.Simulation modules, while removing obsolete references. Mark engine submodule as dirty to indicate uncommitted changes. 2026-06-12 07:43:41 +03:00
78 changed files with 7370 additions and 669 deletions
+17 -6
View File
@@ -14,12 +14,18 @@ Game design docs live in `docs/` and are written in **Russian**. Engine rules li
## Layout ## Layout
``` ```
engine/ mrgameeng git submodule (own repo, own CLAUDE.md) engine/ mrgameeng git submodule (own repo, own CLAUDE.md)
src/LittleSim the game (net8.0); references engine projects directly src/LittleSim the game (net8.0); references engine projects directly
Mods/Core the game's own content as a mod: About, Defs, Languages, Textures src/LittleSim.Server dedicated server: the world headless (engine HeadlessHost) +
Cache/ runtime-built atlases (gitignored) WebSocket replication (MrGameEng.Net); also fast-forward and probe modes
docs/ концепт, симуляция, моды, roadmap (Russian) src/LittleSim.Web browser client (Blazor WASM + KNI/WebGL): connects to the dedicated
LittleSim.sln game + engine sources + engine tests — one window for everything server, renders replicated pawns; mirrors the net contract (NetContract.cs)
because KNI and DesktopGL assemblies can't mix — keep in sync with
src/LittleSim/Net/NetSchema.cs. Outside LittleSim.sln's test flow.
Mods/Core the game's own content as a mod: About, Defs, Languages, Textures
Cache/ runtime-built atlases (gitignored)
docs/ концепт, симуляция, моды, roadmap (Russian)
LittleSim.sln game + engine sources + engine tests — one window for everything
``` ```
## Commands ## Commands
@@ -28,6 +34,11 @@ LittleSim.sln game + engine sources + engine tests — one window for everythin
git submodule update --init # after fresh clone git submodule update --init # after fresh clone
dotnet build LittleSim.sln dotnet build LittleSim.sln
dotnet run --project src/LittleSim -c Release # measure perf in Release only dotnet run --project src/LittleSim -c Release # measure perf in Release only
dotnet run --project src/LittleSim.Server -- --days 10 --tps 60 # headless fast-forward
dotnet run --project src/LittleSim.Server -- --listen # multiplayer world (WebSocket)
dotnet run --project src/LittleSim -- --connect # client → ws://localhost:9050
dotnet run --project src/LittleSim.Server -- --probe # CLI check of a running server
dotnet run --project src/LittleSim.Web # browser client (?server=ws://…)
dotnet test LittleSim.sln # runs the engine test suites dotnet test LittleSim.sln # runs the engine test suites
``` ```
+171 -186
View File
@@ -15,16 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core", "engine\sr
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics", "engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj", "{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics", "engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj", "{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Input", "engine\src\MrGameEng.Input\MrGameEng.Input.csproj", "{7D7E001D-72ED-4204-94D5-B8A0F706D544}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio", "engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj", "{A2620CBF-3404-4583-AD55-FCC8A704BD9A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio", "engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj", "{A2620CBF-3404-4583-AD55-FCC8A704BD9A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets", "engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj", "{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI", "engine\src\MrGameEng.UI\MrGameEng.UI.csproj", "{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI", "engine\src\MrGameEng.UI\MrGameEng.UI.csproj", "{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.DevConsole", "engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj", "{0C370DCA-18AB-4A79-9C60-BE00E7A57180}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets.Generator", "engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj", "{BA456A86-0960-45C3-B48E-5882D7A3873F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets.Generator", "engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj", "{BA456A86-0960-45C3-B48E-5882D7A3873F}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CD3B5CE5-C23F-38B4-04AA-A303F94731A5}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CD3B5CE5-C23F-38B4-04AA-A303F94731A5}"
@@ -33,27 +27,31 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Assets.Generator.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core.Tests", "engine\tests\MrGameEng.Core.Tests\MrGameEng.Core.Tests.csproj", "{2994C301-DFD3-48D1-B44C-F01ED28FBE93}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Core.Tests", "engine\tests\MrGameEng.Core.Tests\MrGameEng.Core.Tests.csproj", "{2994C301-DFD3-48D1-B44C-F01ED28FBE93}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.DevConsole.Tests", "engine\tests\MrGameEng.DevConsole.Tests\MrGameEng.DevConsole.Tests.csproj", "{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics.Tests", "engine\tests\MrGameEng.Graphics.Tests\MrGameEng.Graphics.Tests.csproj", "{21B1878F-2532-4D48-95DD-1C7268DCF1D5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Graphics.Tests", "engine\tests\MrGameEng.Graphics.Tests\MrGameEng.Graphics.Tests.csproj", "{21B1878F-2532-4D48-95DD-1C7268DCF1D5}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Input.Tests", "engine\tests\MrGameEng.Input.Tests\MrGameEng.Input.Tests.csproj", "{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Content", "engine\src\MrGameEng.Content\MrGameEng.Content.csproj", "{EC5504F5-483B-4197-AA19-E8FD710583F4}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Pathfinding", "engine\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj", "{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Simulation", "engine\src\MrGameEng.Simulation\MrGameEng.Simulation.csproj", "{9E653B28-0060-46DA-84D0-DA8655B07AF6}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Collisions", "engine\src\MrGameEng.Collisions\MrGameEng.Collisions.csproj", "{9406DB4C-FABF-48FE-BEA7-25AD319D2172}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Content.Tests", "engine\tests\MrGameEng.Content.Tests\MrGameEng.Content.Tests.csproj", "{A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Pathfinding.Tests", "engine\tests\MrGameEng.Pathfinding.Tests\MrGameEng.Pathfinding.Tests.csproj", "{47E5E756-64F9-4B5A-8EC3-B016745166C4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Simulation.Tests", "engine\tests\MrGameEng.Simulation.Tests\MrGameEng.Simulation.Tests.csproj", "{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Collisions.Tests", "engine\tests\MrGameEng.Collisions.Tests\MrGameEng.Collisions.Tests.csproj", "{B9161776-32FE-415E-9401-DC5BB87FC225}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engine\tests\MrGameEng.UI.Tests\MrGameEng.UI.Tests.csproj", "{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Mods", "engine\src\MrGameEng.Mods\MrGameEng.Mods.csproj", "{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Mods.Tests", "engine\tests\MrGameEng.Mods.Tests\MrGameEng.Mods.Tests.csproj", "{07986629-FE7C-42BE-868B-0FCA1915D2A2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "engine\src\MrGameEng.Host\MrGameEng.Host.csproj", "{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.AI", "engine\src\MrGameEng.AI\MrGameEng.AI.csproj", "{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host.Tests", "engine\tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.AI.Tests", "engine\tests\MrGameEng.AI.Tests\MrGameEng.AI.Tests.csproj", "{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net", "engine\src\MrGameEng.Net\MrGameEng.Net.csproj", "{841EAE51-E86B-4BFB-A70C-A67E4D492DD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net.Tests", "engine\tests\MrGameEng.Net.Tests\MrGameEng.Net.Tests.csproj", "{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Server", "src\LittleSim.Server\LittleSim.Server.csproj", "{95630217-333B-4263-84AE-5EE5961FC307}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Web", "src\LittleSim.Web\LittleSim.Web.csproj", "{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -101,18 +99,6 @@ Global
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x64.Build.0 = Release|Any CPU {F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x64.Build.0 = Release|Any CPU
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.ActiveCfg = Release|Any CPU {F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.ActiveCfg = Release|Any CPU
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.Build.0 = Release|Any CPU {F35A5F1A-9A1B-4F57-BC81-BF16BC574DED}.Release|x86.Build.0 = Release|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x64.ActiveCfg = Debug|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x64.Build.0 = Debug|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x86.ActiveCfg = Debug|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Debug|x86.Build.0 = Debug|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|Any CPU.Build.0 = Release|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x64.ActiveCfg = Release|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x64.Build.0 = Release|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x86.ActiveCfg = Release|Any CPU
{7D7E001D-72ED-4204-94D5-B8A0F706D544}.Release|x86.Build.0 = Release|Any CPU
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|x64.ActiveCfg = Debug|Any CPU {A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -125,18 +111,6 @@ Global
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x64.Build.0 = Release|Any CPU {A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x64.Build.0 = Release|Any CPU
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.ActiveCfg = Release|Any CPU {A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.ActiveCfg = Release|Any CPU
{A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.Build.0 = Release|Any CPU {A2620CBF-3404-4583-AD55-FCC8A704BD9A}.Release|x86.Build.0 = Release|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x64.ActiveCfg = Debug|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x64.Build.0 = Debug|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x86.ActiveCfg = Debug|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Debug|x86.Build.0 = Debug|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|Any CPU.Build.0 = Release|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x64.ActiveCfg = Release|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x64.Build.0 = Release|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x86.ActiveCfg = Release|Any CPU
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27}.Release|x86.Build.0 = Release|Any CPU
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|x64.ActiveCfg = Debug|Any CPU {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -149,18 +123,6 @@ Global
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x64.Build.0 = Release|Any CPU {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x64.Build.0 = Release|Any CPU
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.ActiveCfg = Release|Any CPU {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.ActiveCfg = Release|Any CPU
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.Build.0 = Release|Any CPU {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D}.Release|x86.Build.0 = Release|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x64.ActiveCfg = Debug|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x64.Build.0 = Debug|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x86.ActiveCfg = Debug|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Debug|x86.Build.0 = Debug|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|Any CPU.Build.0 = Release|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x64.ActiveCfg = Release|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x64.Build.0 = Release|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x86.ActiveCfg = Release|Any CPU
{0C370DCA-18AB-4A79-9C60-BE00E7A57180}.Release|x86.Build.0 = Release|Any CPU
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|x64.ActiveCfg = Debug|Any CPU {BA456A86-0960-45C3-B48E-5882D7A3873F}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -197,18 +159,6 @@ Global
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x64.Build.0 = Release|Any CPU {2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x64.Build.0 = Release|Any CPU
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.ActiveCfg = Release|Any CPU {2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.ActiveCfg = Release|Any CPU
{2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.Build.0 = Release|Any CPU {2994C301-DFD3-48D1-B44C-F01ED28FBE93}.Release|x86.Build.0 = Release|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x64.ActiveCfg = Debug|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x64.Build.0 = Debug|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x86.ActiveCfg = Debug|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Debug|x86.Build.0 = Debug|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|Any CPU.Build.0 = Release|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x64.ActiveCfg = Release|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x64.Build.0 = Release|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x86.ActiveCfg = Release|Any CPU
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C}.Release|x86.Build.0 = Release|Any CPU
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|x64.ActiveCfg = Debug|Any CPU {21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -221,114 +171,150 @@ Global
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x64.Build.0 = Release|Any CPU {21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x64.Build.0 = Release|Any CPU
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.ActiveCfg = Release|Any CPU {21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.ActiveCfg = Release|Any CPU
{21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.Build.0 = Release|Any CPU {21B1878F-2532-4D48-95DD-1C7268DCF1D5}.Release|x86.Build.0 = Release|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x64.ActiveCfg = Debug|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x64.ActiveCfg = Debug|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x64.Build.0 = Debug|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x64.Build.0 = Debug|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x86.ActiveCfg = Debug|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x86.ActiveCfg = Debug|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Debug|x86.Build.0 = Debug|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Debug|x86.Build.0 = Debug|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|Any CPU.Build.0 = Release|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|Any CPU.Build.0 = Release|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x64.ActiveCfg = Release|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x64.ActiveCfg = Release|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x64.Build.0 = Release|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x64.Build.0 = Release|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x86.ActiveCfg = Release|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x86.ActiveCfg = Release|Any CPU
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4}.Release|x86.Build.0 = Release|Any CPU {EC5504F5-483B-4197-AA19-E8FD710583F4}.Release|x86.Build.0 = Release|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|Any CPU.Build.0 = Debug|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x64.ActiveCfg = Debug|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x64.ActiveCfg = Debug|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x64.Build.0 = Debug|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x64.Build.0 = Debug|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x86.ActiveCfg = Debug|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x86.ActiveCfg = Debug|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Debug|x86.Build.0 = Debug|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Debug|x86.Build.0 = Debug|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|Any CPU.Build.0 = Release|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|Any CPU.Build.0 = Release|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x64.ActiveCfg = Release|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x64.ActiveCfg = Release|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x64.Build.0 = Release|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x64.Build.0 = Release|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x86.ActiveCfg = Release|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x86.ActiveCfg = Release|Any CPU
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30}.Release|x86.Build.0 = Release|Any CPU {9E653B28-0060-46DA-84D0-DA8655B07AF6}.Release|x86.Build.0 = Release|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|Any CPU.Build.0 = Debug|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x64.ActiveCfg = Debug|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x64.ActiveCfg = Debug|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x64.Build.0 = Debug|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x64.Build.0 = Debug|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x86.ActiveCfg = Debug|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x86.ActiveCfg = Debug|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Debug|x86.Build.0 = Debug|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Debug|x86.Build.0 = Debug|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|Any CPU.ActiveCfg = Release|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|Any CPU.Build.0 = Release|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|Any CPU.Build.0 = Release|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x64.ActiveCfg = Release|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x64.ActiveCfg = Release|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x64.Build.0 = Release|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x64.Build.0 = Release|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x86.ActiveCfg = Release|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x86.ActiveCfg = Release|Any CPU
{9406DB4C-FABF-48FE-BEA7-25AD319D2172}.Release|x86.Build.0 = Release|Any CPU {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F}.Release|x86.Build.0 = Release|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x64.ActiveCfg = Debug|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x64.ActiveCfg = Debug|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x64.Build.0 = Debug|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x64.Build.0 = Debug|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x86.ActiveCfg = Debug|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x86.ActiveCfg = Debug|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Debug|x86.Build.0 = Debug|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Debug|x86.Build.0 = Debug|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|Any CPU.Build.0 = Release|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|Any CPU.Build.0 = Release|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x64.ActiveCfg = Release|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x64.ActiveCfg = Release|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x64.Build.0 = Release|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x64.Build.0 = Release|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x86.ActiveCfg = Release|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x86.ActiveCfg = Release|Any CPU
{47E5E756-64F9-4B5A-8EC3-B016745166C4}.Release|x86.Build.0 = Release|Any CPU {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050}.Release|x86.Build.0 = Release|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x64.ActiveCfg = Debug|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x64.ActiveCfg = Debug|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x64.Build.0 = Debug|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x64.Build.0 = Debug|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x86.ActiveCfg = Debug|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x86.ActiveCfg = Debug|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Debug|x86.Build.0 = Debug|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Debug|x86.Build.0 = Debug|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|Any CPU.Build.0 = Release|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|Any CPU.Build.0 = Release|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x64.ActiveCfg = Release|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x64.ActiveCfg = Release|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x64.Build.0 = Release|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x64.Build.0 = Release|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x86.ActiveCfg = Release|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x86.ActiveCfg = Release|Any CPU
{B9161776-32FE-415E-9401-DC5BB87FC225}.Release|x86.Build.0 = Release|Any CPU {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD}.Release|x86.Build.0 = Release|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x64.ActiveCfg = Debug|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x64.ActiveCfg = Debug|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x64.Build.0 = Debug|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x64.Build.0 = Debug|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x86.ActiveCfg = Debug|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x86.ActiveCfg = Debug|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Debug|x86.Build.0 = Debug|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Debug|x86.Build.0 = Debug|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|Any CPU.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|Any CPU.Build.0 = Release|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x64.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.ActiveCfg = Release|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x64.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x86.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82}.Release|x86.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x64.ActiveCfg = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.ActiveCfg = Debug|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x64.Build.0 = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.Build.0 = Debug|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x86.ActiveCfg = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.ActiveCfg = Debug|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Debug|x86.Build.0 = Debug|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.Build.0 = Debug|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|Any CPU.Build.0 = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.Build.0 = Release|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x64.ActiveCfg = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.ActiveCfg = Release|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x64.Build.0 = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.Build.0 = Release|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x86.ActiveCfg = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.ActiveCfg = Release|Any CPU
{07986629-FE7C-42BE-868B-0FCA1915D2A2}.Release|x86.Build.0 = Release|Any CPU {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.Build.0 = Release|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x64.ActiveCfg = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.ActiveCfg = Debug|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x64.Build.0 = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.Build.0 = Debug|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x86.ActiveCfg = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.ActiveCfg = Debug|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Debug|x86.Build.0 = Debug|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.Build.0 = Debug|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|Any CPU.Build.0 = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.Build.0 = Release|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x64.ActiveCfg = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.ActiveCfg = Release|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x64.Build.0 = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.Build.0 = Release|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x86.ActiveCfg = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.ActiveCfg = Release|Any CPU
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5}.Release|x86.Build.0 = Release|Any CPU {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.Build.0 = Release|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|Any CPU.Build.0 = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x64.ActiveCfg = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.ActiveCfg = Debug|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x64.Build.0 = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x64.Build.0 = Debug|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x86.ActiveCfg = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.ActiveCfg = Debug|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Debug|x86.Build.0 = Debug|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Debug|x86.Build.0 = Debug|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|Any CPU.ActiveCfg = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|Any CPU.Build.0 = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|Any CPU.Build.0 = Release|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x64.ActiveCfg = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.ActiveCfg = Release|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x64.Build.0 = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x64.Build.0 = Release|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x86.ActiveCfg = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.ActiveCfg = Release|Any CPU
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85}.Release|x86.Build.0 = Release|Any CPU {841EAE51-E86B-4BFB-A70C-A67E4D492DD7}.Release|x86.Build.0 = Release|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x64.ActiveCfg = Debug|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x64.Build.0 = Debug|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x86.ActiveCfg = Debug|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Debug|x86.Build.0 = Debug|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|Any CPU.Build.0 = Release|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x64.ActiveCfg = Release|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x64.Build.0 = Release|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x86.ActiveCfg = Release|Any CPU
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA}.Release|x86.Build.0 = Release|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x64.ActiveCfg = Debug|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x64.Build.0 = Debug|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x86.ActiveCfg = Debug|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Debug|x86.Build.0 = Debug|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Release|Any CPU.Build.0 = Release|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x64.ActiveCfg = Release|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x64.Build.0 = Release|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x86.ActiveCfg = Release|Any CPU
{95630217-333B-4263-84AE-5EE5961FC307}.Release|x86.Build.0 = Release|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x64.ActiveCfg = Debug|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x64.Build.0 = Debug|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x86.ActiveCfg = Debug|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Debug|x86.Build.0 = Debug|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|Any CPU.Build.0 = Release|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x64.ActiveCfg = Release|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x64.Build.0 = Release|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x86.ActiveCfg = Release|Any CPU
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -338,25 +324,24 @@ Global
{F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} = {D585295F-7994-3649-1065-7AA403C41681} {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} = {D585295F-7994-3649-1065-7AA403C41681}
{FE6C04D3-E619-4C77-A971-1357463676B7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {FE6C04D3-E619-4C77-A971-1357463676B7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{F35A5F1A-9A1B-4F57-BC81-BF16BC574DED} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {F35A5F1A-9A1B-4F57-BC81-BF16BC574DED} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{7D7E001D-72ED-4204-94D5-B8A0F706D544} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{A2620CBF-3404-4583-AD55-FCC8A704BD9A} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {A2620CBF-3404-4583-AD55-FCC8A704BD9A} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{4FCD860B-F8E1-42F5-8A53-EA9AFCCE5A27} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{A90690FB-E3A8-4C92-94E2-33D8E1F27D9D} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {A90690FB-E3A8-4C92-94E2-33D8E1F27D9D} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{0C370DCA-18AB-4A79-9C60-BE00E7A57180} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{BA456A86-0960-45C3-B48E-5882D7A3873F} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {BA456A86-0960-45C3-B48E-5882D7A3873F} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{CD3B5CE5-C23F-38B4-04AA-A303F94731A5} = {D585295F-7994-3649-1065-7AA403C41681} {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} = {D585295F-7994-3649-1065-7AA403C41681}
{83548DF5-75BC-45EF-8911-83BE5173CD2D} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {83548DF5-75BC-45EF-8911-83BE5173CD2D} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{2994C301-DFD3-48D1-B44C-F01ED28FBE93} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {2994C301-DFD3-48D1-B44C-F01ED28FBE93} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{A3C128F7-6CCD-46CD-B935-C30E15EC7E1C} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{21B1878F-2532-4D48-95DD-1C7268DCF1D5} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {21B1878F-2532-4D48-95DD-1C7268DCF1D5} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{E9B94072-798E-4E09-BD9E-1FE6165D7EA4} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {EC5504F5-483B-4197-AA19-E8FD710583F4} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{DC52F36D-2729-42DD-B1ED-B0D0CF95DC30} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {9E653B28-0060-46DA-84D0-DA8655B07AF6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{9406DB4C-FABF-48FE-BEA7-25AD319D2172} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {A09593B2-82BD-468F-AC0C-52B8DEFE8E0F} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{47E5E756-64F9-4B5A-8EC3-B016745166C4} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{B9161776-32FE-415E-9401-DC5BB87FC225} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{F91AA8C8-FD6B-4B92-BB3A-844DCA932D82} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{07986629-FE7C-42BE-868B-0FCA1915D2A2} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{EBD0B6A4-B8F3-45D6-88EF-D5D17DEDEFA5} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{6731DC3C-0807-4791-AF8E-C56F7F7D1F85} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {841EAE51-E86B-4BFB-A70C-A67E4D492DD7} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
{D66A7D37-A1F1-473D-A5FA-9F121CCCC0CA} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{95630217-333B-4263-84AE-5EE5961FC307} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{1FF0DF78-22E4-4169-A9DC-233FFAEA5B14} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+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" } }
]
}
+34 -3
View File
@@ -1,15 +1,46 @@
{ {
"type": "Plant", "type": "Plant",
"defs": [ "defs": [
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28 }, { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
] },
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" }, { "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" }, { "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" }, { "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" }, { "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2 }, { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
"harvestProduct": "ProductGrass",
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
"harvestAmount": 2, "leafHue": 0.36 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
{ "sizeCells": 1.2, "label": "plant.stage.mature" }
] },
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4 }, { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
"fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13,
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
] },
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" }, { "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" }, { "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" } { "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
+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" }
]
}
+4 -4
View File
@@ -4,19 +4,19 @@
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] }, { "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] }, { "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120], { "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120],
"isLand": true, "surface": "terrain/surfaces/sand" }, "isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4 },
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70], { "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70],
"isLand": true, "surface": "terrain/surfaces/mossy", "isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
"scatter": [ "scatter": [
{ "chance": 0.35, "options": ["GrassA"] }, { "chance": 0.35, "options": ["GrassA"] },
{ "chance": 0.07, "options": ["BushA", "BushB", "BushC"] } { "chance": 0.07, "options": ["BushA", "BushB", "BushC"] }
] }, ] },
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50], { "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
"isLand": true, "surface": "terrain/surfaces/soil", "isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
"scatter": [ "scatter": [
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] } { "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
] }, ] },
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128], { "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
"surface": "terrain/surfaces/roughhewnrock" } "surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
] ]
} }
+8
View File
@@ -0,0 +1,8 @@
{
"type": "WorldPreset",
"defs": [
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 80, "height": 50, "population": 40 },
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 120, "height": 70, "population": 80 },
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 180, "height": 110, "population": 140 }
]
}
+67 -7
View File
@@ -1,21 +1,76 @@
{ {
"hud.world": "LittleSim | seed {0} | population {1} | {2} FPS", "hud.world": "LittleSim | seed {0} | {1} | {2} FPS",
"hud.terrain": "LittleSim | terrain | seed {0} | animals: {1}", "hud.datetime": "day {0}, {1:00}:{2:00}",
"hud.controls": "WASD — camera, wheel — zoom, ` — console ('help' lists commands)", "hud.climate": "{0} {1}°C",
"season.spring": "spring",
"season.summer": "summer",
"season.autumn": "autumn",
"season.winter": "winter",
"hud.controls": "WASD — camera, wheel — zoom, ` — console, F1 — inspector",
"hud.paused": "PAUSED",
"menu.title": "LittleSim",
"menu.subtitle": "a god-game: minimal graphics, deep simulation",
"menu.newworld": "New World",
"menu.load": "Load",
"menu.settings": "Settings",
"menu.credits": "Credits",
"menu.quit": "Quit",
"newworld.title": "New World",
"newworld.name": "Name",
"newworld.defaultname": "New World",
"newworld.size": "Size",
"newworld.seed": "Seed",
"newworld.random": "Random",
"newworld.smoothing": "Terrain smoothing",
"newworld.create": "Create",
"newworld.back": "Back",
"load.title": "Load",
"load.empty": "No saves yet",
"load.load": "Load",
"load.delete": "Delete",
"load.back": "Back",
"settings.title": "Settings",
"settings.language": "Language",
"settings.fullscreen": "Fullscreen",
"settings.vsync": "Vertical sync",
"settings.resolution": "Resolution",
"settings.volume": "Volume",
"settings.apply": "Apply",
"settings.back": "Back",
"pause.title": "Paused",
"pause.resume": "Resume",
"pause.settings": "Settings",
"pause.save": "Save",
"pause.mainmenu": "Main menu",
"pause.quit": "Quit",
"pause.saved": "Saved: {0}",
"speed.pause": "⏸",
"credits.title": "Credits",
"credits.author": "mrleo1nid",
"credits.role": "Concept, code, mrgameeng engine",
"credits.back": "Back",
"preset.small": "Small",
"preset.medium": "Medium",
"preset.large": "Large",
"terrain.deepwater": "deep water", "terrain.deepwater": "deep water",
"terrain.water": "water", "terrain.water": "water",
"terrain.sand": "sand", "terrain.sand": "sand",
"terrain.grass": "grass", "terrain.grass": "grass",
"terrain.forest": "forest", "terrain.forest": "forest",
"terrain.mountain": "mountains", "terrain.mountain": "mountains",
"plant.oak": "oak", "plant.oak": "oak",
"plant.birch": "birch", "plant.birch": "birch",
"plant.pine": "pine", "plant.pine": "pine",
"plant.grass": "grass tuft", "plant.grass": "grass tuft",
"plant.bush": "bush", "plant.bush": "bush",
"plant.stage.seedling": "seedling",
"plant.stage.sprout": "sprout",
"plant.stage.sapling": "sapling",
"plant.stage.mature": "mature",
"product.wood": "wood",
"product.grass": "grass",
"product.berry": "berries",
"product.acorn": "acorn",
"pawn.being": "being", "pawn.being": "being",
"pawn.bear": "bear", "pawn.bear": "bear",
"pawn.deer": "deer", "pawn.deer": "deer",
@@ -24,5 +79,10 @@
"pawn.boar": "boar", "pawn.boar": "boar",
"pawn.wolf": "wolf", "pawn.wolf": "wolf",
"pawn.muffalo": "muffalo", "pawn.muffalo": "muffalo",
"pawn.squirrel": "squirrel" "pawn.squirrel": "squirrel",
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
"net.connecting": "connecting…",
"net.connected": "connected",
"net.failed": "connection failed",
"net.lost": "connection lost"
} }
+67 -7
View File
@@ -1,21 +1,76 @@
{ {
"hud.world": "LittleSim | сид {0} | население {1} | {2} FPS", "hud.world": "LittleSim | сид {0} | {1} | {2} FPS",
"hud.terrain": "LittleSim | террейн | сид {0} | животных: {1}", "hud.datetime": "день {0}, {1:00}:{2:00}",
"hud.controls": "WASD — камера, колесо — зум, ` — консоль ('help' — список команд)", "hud.climate": "{0} {1}°C",
"season.spring": "весна",
"season.summer": "лето",
"season.autumn": "осень",
"season.winter": "зима",
"hud.controls": "WASD — камера, колесо — зум, ` — консоль, F1 — инспектор",
"hud.paused": "ПАУЗА",
"menu.title": "LittleSim",
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
"menu.newworld": "Новый мир",
"menu.load": "Загрузка",
"menu.settings": "Настройки",
"menu.credits": "Авторы",
"menu.quit": "Выход",
"newworld.title": "Новый мир",
"newworld.name": "Название",
"newworld.defaultname": "Новый мир",
"newworld.size": "Размер",
"newworld.seed": "Сид",
"newworld.random": "Случайно",
"newworld.smoothing": "Сглаживание рельефа",
"newworld.create": "Создать",
"newworld.back": "Назад",
"load.title": "Загрузка",
"load.empty": "Сохранений пока нет",
"load.load": "Загрузить",
"load.delete": "Удалить",
"load.back": "Назад",
"settings.title": "Настройки",
"settings.language": "Язык",
"settings.fullscreen": "Полный экран",
"settings.vsync": "Вертикальная синхронизация",
"settings.resolution": "Разрешение",
"settings.volume": "Громкость",
"settings.apply": "Применить",
"settings.back": "Назад",
"pause.title": "Пауза",
"pause.resume": "Продолжить",
"pause.settings": "Настройки",
"pause.save": "Сохранить",
"pause.mainmenu": "Главное меню",
"pause.quit": "Выход",
"pause.saved": "Сохранено: {0}",
"speed.pause": "⏸",
"credits.title": "Авторы",
"credits.author": "mrleo1nid",
"credits.role": "Идея, код, движок mrgameeng",
"credits.back": "Назад",
"preset.small": "Маленький",
"preset.medium": "Средний",
"preset.large": "Большой",
"terrain.deepwater": "глубокая вода", "terrain.deepwater": "глубокая вода",
"terrain.water": "вода", "terrain.water": "вода",
"terrain.sand": "песок", "terrain.sand": "песок",
"terrain.grass": "трава", "terrain.grass": "трава",
"terrain.forest": "лес", "terrain.forest": "лес",
"terrain.mountain": "горы", "terrain.mountain": "горы",
"plant.oak": "дуб", "plant.oak": "дуб",
"plant.birch": "берёза", "plant.birch": "берёза",
"plant.pine": "сосна", "plant.pine": "сосна",
"plant.grass": "пучок травы", "plant.grass": "пучок травы",
"plant.bush": "куст", "plant.bush": "куст",
"plant.stage.seedling": "росток",
"plant.stage.sprout": "всходы",
"plant.stage.sapling": "саженец",
"plant.stage.mature": "взрослое",
"product.wood": "древесина",
"product.grass": "трава",
"product.berry": "ягоды",
"product.acorn": "жёлудь",
"pawn.being": "житель", "pawn.being": "житель",
"pawn.bear": "медведь", "pawn.bear": "медведь",
"pawn.deer": "олень", "pawn.deer": "олень",
@@ -24,5 +79,10 @@
"pawn.boar": "кабан", "pawn.boar": "кабан",
"pawn.wolf": "волк", "pawn.wolf": "волк",
"pawn.muffalo": "муффало", "pawn.muffalo": "муффало",
"pawn.squirrel": "белка" "pawn.squirrel": "белка",
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
"net.connecting": "подключение…",
"net.connected": "подключено",
"net.failed": "не удалось подключиться",
"net.lost": "соединение потеряно"
} }
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<SkinSet> <SkinSet>
<lifeStage> <lifeStage>
<appliesToAll>true</appliesToAll> <appliesToAll>true</appliesToAll>
<variants> <variants>
@@ -24,9 +23,6 @@
<commonality>0.4</commonality> <commonality>0.4</commonality>
<texName>GuineaPig3</texName> <texName>GuineaPig3</texName>
</skin> </skin>
</variants> </variants>
</lifeStage> </lifeStage>
</SkinSet> </SkinSet>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<SkinSet> <SkinSet>
<lifeStage> <lifeStage>
<appliesToAll>true</appliesToAll> <appliesToAll>true</appliesToAll>
<variants> <variants>
@@ -39,8 +38,6 @@
<commonality>0.25</commonality> <commonality>0.25</commonality>
<texName>Horse6</texName> <texName>Horse6</texName>
</skin> </skin>
</variants> </variants>
</lifeStage> </lifeStage>
</SkinSet> </SkinSet>
+68
View File
@@ -0,0 +1,68 @@
# Веб-клиент: spike KNI и решение A/B
Дата: 2026-06-12. Спайк жил в `spikes/KniWeb`; после успеха повышен до
**`src/LittleSim.Web`** — рабочего браузерного клиента мультиплеера (Blazor WASM + KNI):
он подключается к `LittleSim.Server --listen` по WebSocket, применяет дельта-снапшоты
`MrGameEng.Net` и рисует жителей через WebGL со сглаживанием позиций. Адрес сервера —
`?server=ws://host:port` в URL страницы (по умолчанию — хост страницы, порт 9050).
Сетевой контракт там продублирован бинарным зеркалом (`NetContract.cs`) — KNI- и
DesktopGL-сборки нельзя смешивать, пока графика движка не собирается пер-платформенно.
## Вопрос
Путь A — «MonoGame в браузере» через [KNI](https://github.com/kniEngine/kni)
(форк MonoGame с платформой Blazor WebAssembly/WebGL, те же неймспейсы
`Microsoft.Xna.Framework.*`). Путь B — тонкий веб-клиент без MonoGame
(TypeScript/PixiJS поверх сетевой репликации). Спайк проверял минимальную
жизнеспособность пути A: **ядро движка + Friflo + WebGL-спрайт в браузере**.
## Что сделано
`dotnet new kni-blazor-gl` (пакет шаблонов `nkast.Kni.Templates`, KNI 4.2.9001,
net8.0) + ProjectReference на `engine/src/MrGameEng.Core` + мини-хост в духе
`GameHost` поверх KNI `Game`. Сцена: 300 сущностей в Friflo `EntityStore`,
`QuerySystem` двигает их в Update-фазе (отскок от краёв, seed 42), Draw-фаза
рисует через KNI `SpriteBatch` (WebGL).
## Результат — путь A жизнеспособен
- **`MrGameEng.Core` работает в Blazor WASM без изменений**: `EngineContext`,
`GameClock`, `Scene`/`SceneManager`, тайминг переходов — всё ядро завелось
как есть (заслуга расслоения Core/Host: в ядре нет ни MonoGame, ни платформы).
- **Friflo.Engine.ECS 3.6 работает в wasm**: создание сущностей, архетипы,
`QuerySystem`, `ForEachEntity` — без ошибок в консоли браузера.
- **KNI 4.2.9001 рендерит через WebGL** с XNA-API: `Game`,
`GraphicsDeviceManager`, `SpriteBatch`, `Texture2D.SetData` — совпадает с
кодом, который пишется под десктопный MonoGame.
- Сборка тривиальна: обычный `Microsoft.NET.Sdk.BlazorWebAssembly` проект,
никаких wasm-workload-плясок не понадобилось.
## Известные ограничения пути A (работа на этапе «веб-клиент»)
1. **Пер-платформенная компиляция библиотек движка.** `MrGameEng.Graphics`,
`Content`, `Audio`, `UI` ссылаются на `MonoGame.Framework.DesktopGL`; для
веба их надо собирать против пакетов `nkast.*` (типы те же по API, но другие
сборки). Решение — msbuild-условие (`KniPlatform=BlazorGL` → nkast-пакеты),
без изменения исходников.
2. **Шейдеры.** `Renderer2D` использует прекомпилированные `dotnet-mgfxc`
эффекты — KNI имеет собственный компилятор эффектов; совместимость надо
проверять отдельным спайком, прежде чем тащить батчер в веб.
3. **Нет файловой системы.** Моды/дефы/атласы в браузер приезжают по HTTP;
текущая схема «собрать атласы при старте из PNG» в вебе не работает —
атласы пре-билдятся и кладутся в `wwwroot` (или приезжают с сервера).
4. **Потоки.** `Task.Run`-загрузка контента и `Thread.Sleep`-пейсинг не для
браузера (клиенту `HeadlessHost.Run` и не нужен — цикл гонит
`requestAnimationFrame` через KNI).
5. **Производительность не мерялась** (300 спрайтов — гладко); бюджет
сущностей в wasm-интерпретаторе будет заметно ниже десктопного, замерять
на реальной сцене с включённым AOT.
## Решение
**Путь A (KNI)** — основной для веб-клиента: переиспользуем ядро, сцены и
в перспективе графику движка; код игры один на все платформы. Путь B остаётся
запасным, если упрёмся в шейдеры (п. 2) или производительность (п. 5).
Порядок работ не меняется: сначала `MrGameEng.Net` (WebSocket-транспорт +
репликация — нужен любому пути) и сетевой мультиплеер на десктопе, затем
`MrGameEng.Host.Web` поверх KNI по образцу спайка.
+84
View File
@@ -0,0 +1,84 @@
# Растения как экосистема
Дизайн системы растений: рост зависит от среды, среда описана факторами, реакция растения на
факторы задаётся **генами**, а размножение передаёт гены потомкам с мутацией — появляется
естественный отбор. Документ — единый ориентир для фаз A–D; детали каждой фазы уточняются её
отдельным планом.
## Модель роста
Скорость роста растения в данный момент:
```
скорость = база × пригодность(свет) × пригодность(температура) × пригодность(плодородность)
```
Каждая «пригодность» — число в [0..1] из кривой отклика (движковый `ResponseCurve`, модуль
`MrGameEng.AI`) с **оптимумом** и **шириной толерантности**, которые берутся из генов растения.
Растение вне своих оптимумов растёт медленно или стоит; в своих — быстро. Так все факторы среды
сводятся к одному механизму, а различие видов/особей — целиком в генах.
Фактор «возраст/стадия» остаётся как сейчас (`PlantGrowthStage` в дефах): пригодности умножают
прирост `AgeDays`, стадии переключают спрайт/размер по накопленному возрасту.
## Факторы среды
- **Свет** — амбиент день/ночь (`MrGameEng.Lighting.DayNight`), сэмплируемый в точке
(`SampleAt(world)`); глобальный в фазе A, локальный (с тенями) в фазе D.
- **Температура** — непрерывная, из климата (`MrGameEng.Core.Climate`): сезонный синус по дню года
+ суточные колебания (холоднее ночью).
- **Плодородность** — пока константа типа террейна (`TerrainDef.Fertility`); позже возможна
по-клеточная почва с истощением/восстановлением.
## Гены (диплоидный геном, гибридная экспрессия)
Каждое растение несёт **геном**: по паре аллелей на ген. Экспрессия гибридная:
- **Числовые гены** (оптимум света, толерантность к свету, оптимум температуры, толерантность,
потребность в плодородности, макс. размер, базовая скорость, продолжительность жизни, дальность
расселения, плодовитость, темп мутаций, способность к самоопылению) — фенотип = **смешение**
(среднее двух аллелей). Реализм отбора: промежуточные значения, плавный дрейф.
- **Дискретные гены** (например, цвет/морфа, наличие признака) — **доминант/рецессив**: рецессивный
фенотип виден только в гомозиготе (aa).
Наследование (мейоз): каждый родитель отдаёт по одному случайному аллелю на ген; потомок получает
по одному от каждого. Затем — мутация (с вероятностью из гена темпа мутаций сдвигаем числовой
аллель/переключаем дискретный). Всё детерминировано сидом мира.
Стартовая популяция: геномы генерируются из сида (вид задаёт базовый геном и разброс аллелей в
дефе; особи отклоняются).
## Размножение (полуреалистичное)
- **Перекрёстное опыление (приоритет):** зрелое растение ищет зрелого соседа того же вида в радиусе
(ген дальности; запрос через spatial-hash движка). Есть партнёр → семя с геномом от двух
родителей (Мендель + мутация).
- **Самоопыление (запас):** нет партнёра и ген «способность к самоопылению» позволяет → семя из
одного родителя (само-скрещивание аллелей + мутация).
- **Расселение:** семя падает в клетку в радиусе расселения; прорастает, если клетка подходит
(суша, не занято сверх лимита, пригодность не нулевая).
- **Лимит плотности:** не больше N растений на клетку/площадь — иначе взрыв численности.
- **Старение/смерть:** возраст превышает ген жизни → смерть; или длительный стресс (пригодность
около нуля) → гибель. Смерть освобождает место — баланс с размножением и основа отбора.
## Производительность и сейв
- Представление — **по сущности на растение** (видно в рендере и ECS-инспекторе, индивидуальные
гены). Защита от взрыва: лимит плотности, смерть, разумные радиусы, проверки размножения по
таймеру/сезону (не каждый кадр на каждое растение), spatial-hash для соседей.
- **Персист:** геномы, позиции, возраст и стадия растений сохраняются в `WorldSave` (мир больше не
воспроизводится только из сида — эволюция переживает загрузку).
- **Детерминизм:** мутации/расселение/опыление — на сидированном `Random`, как в остальной симуляции.
## Фазы реализации
- **A — климат и день/ночь (движок).** `Climate` (температура/сезон поверх `Calendar`),
`DayNight` (амбиент, `Renderer2D.AmbientLight` на World-слоях, сэмплируемый `SampleAt`),
затемнение мира ночью, HUD: сезон/температура/время. Фундамент для влияния среды на рост.
- **B — гены и рост по пригодностям (игра).** Компонент `PlantGenome`, расчёт пригодностей
(свет из `DayNight.SampleAt`, температура из `Climate`, плодородность из террейна), замена
плоского `GrowthRate`. Геном виден/правится в инспекторе.
- **C — размножение и смерть (игра).** Перекрёстное/самоопыление, расселение, лимит плотности,
старение/стресс-смерть, персист популяции в сейв.
- **D — полноценное 2D-освещение (движок).** Точечные источники + тени (окклюзия от препятствий);
`DayNight.SampleAt` начинает возвращать локальную освещённость, и затенённые растения растут хуже.
+1 -1
Submodule engine updated: 1f87fb0b74...d044cafad9
+22
View File
@@ -0,0 +1,22 @@
param(
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$Root = Resolve-Path (Join-Path $PSScriptRoot "..")
$Project = Join-Path $Root "src\LittleSim\LittleSim.csproj"
Push-Location $Root
try {
dotnet build LittleSim.sln -c $Configuration
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
dotnet run --project $Project -c $Configuration --no-build @args
exit $LASTEXITCODE
}
finally {
Pop-Location
}
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIGURATION="${CONFIGURATION:-Release}"
while [[ $# -gt 0 ]]; do
case "$1" in
-c | --configuration)
CONFIGURATION="$2"
shift 2
;;
*)
break
;;
esac
done
cd "$ROOT"
dotnet build LittleSim.sln -c "$CONFIGURATION"
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
+126
View File
@@ -0,0 +1,126 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using LittleSim.Net;
using LittleSim.Scenes;
using LittleSim.Sim;
using Microsoft.Xna.Framework;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Net;
namespace LittleSim.Server;
/// <summary>
/// Серверный мир: жители бродят, устают и отдыхают (те же системы, что в WorldScene) на
/// фиксированном тике <see cref="HeadlessHost"/> — без окна, GPU и спрайтов. С
/// <see cref="WebSocketServer"/> мир ещё и реплицируется подключённым клиентам по схеме
/// <see cref="NetSchema"/> (10 снапшотов в секунду, дельты). Демонстрация MrGameEng.Net.
/// </summary>
internal sealed class HeadlessWorldScene : Scene
{
/// <summary>Жителей в серверном мире.</summary>
public const int PawnCount = 24;
/// <summary>Сид мира — рельефа пока нет, но блуждание детерминировано им.</summary>
public const int Seed = 424242;
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
private readonly GameContent _content;
private readonly WebSocketServer? _server;
/// <summary>Мир без сети (fast-forward) или, с <paramref name="server"/>, онлайн-мир.</summary>
public HeadlessWorldScene(GameContent content, WebSocketServer? server = null)
{
_content = content;
_server = server;
}
protected override void OnLoad()
{
Context.Services.Add(_content);
var calendar = Context.UseCalendar(WorldScene.SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
var replication = _server is null ? null : new ReplicationServer(NetSchema.Create(), Store);
var random = new Random(Seed);
for (var i = 0; i < PawnCount; i++)
{
var position = new Vector2(
random.NextSingle() * Bounds.Width,
random.NextSingle() * Bounds.Height
);
var transform = new Transform2D(position, scale: new Vector2(12f));
var needs = new PawnNeeds { Energy = 0.4f + random.NextSingle() * 0.6f };
if (replication is null)
{
Store.CreateEntity(transform, new Wander(), new PawnBrain(), needs);
}
else
{
Store.CreateEntity(
transform,
new Wander(),
new PawnBrain(),
needs,
new NetId { Value = replication.NextNetId() }
);
}
}
UpdateSystems.Add(new PawnDecisionSystem());
UpdateSystems.Add(new PawnNeedsSystem());
UpdateSystems.Add(new WanderSystem(Seed, Bounds));
UpdateSystems.Add(new DayReportSystem(calendar, climate));
if (_server is not null && replication is not null)
{
UpdateSystems.Add(new NetworkSystem(_server, replication));
}
}
/// <summary>Принимает новые соединения и шлёт дельта-снапшоты с сетевой частотой.</summary>
private sealed class NetworkSystem(WebSocketServer server, ReplicationServer replication)
: BaseSystem
{
// 60 тиков симуляции / 6 = 10 снапшотов в секунду.
private const int SendEveryTicks = 6;
private int _ticks;
protected override void OnUpdateGroup()
{
while (server.TryAcceptConnection(out var connection))
{
Log.Info(
$"Клиент #{connection.Id} подключился ({server.Connections.Count} онлайн)"
);
}
if (++_ticks % SendEveryTicks == 0)
{
replication.Send(server.Connections);
}
}
}
/// <summary>Пишет строку состояния мира в лог на рассвете каждого игрового дня.</summary>
private sealed class DayReportSystem(Calendar calendar, Climate climate) : BaseSystem
{
private int _lastDay;
protected override void OnUpdateGroup()
{
if (calendar.Day == _lastDay)
{
return;
}
_lastDay = calendar.Day;
Log.Info(
$"День {calendar.Day} | год {climate.Year}, день года {climate.DayOfYear + 1}, "
+ $"{climate.Season} | {climate.Temperature:+0.0;-0.0;0.0} °C"
);
}
}
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LittleSim\LittleSim.csproj" />
</ItemGroup>
</Project>
+151
View File
@@ -0,0 +1,151 @@
using System.Diagnostics;
using Friflo.Engine.ECS;
using LittleSim.Content;
using LittleSim.Net;
using LittleSim.Scenes;
using LittleSim.Server;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Net;
// Дедикейтед-сервер LittleSim: мир без окна и GPU на HeadlessHost движка.
// dotnet run --project src/LittleSim.Server — fast-forward N дней
// dotnet run --project src/LittleSim.Server -- --days 30 — то же, явное число дней
// dotnet run --project src/LittleSim.Server -- --listen [--port N] — онлайн-мир (WebSocket)
// dotnet run --project src/LittleSim.Server -- --probe [--port N] — проверка: подключиться
// к серверу и показать реплицированных жителей
var days = ReadOption("--days", 10);
var ticksPerSecond = ReadOption("--tps", 60);
var port = ReadOption("--port", NetSchema.DefaultPort);
Log.MessageLogged += (level, message) => Console.WriteLine($"[{level}] {message}");
if (Array.IndexOf(args, "--probe") >= 0)
{
return await Probe(port);
}
var content = GameContent.Load(buildAtlases: false);
Log.Info(
$"Моды: {string.Join(", ", content.Mods.Select(m => m.ToString()))} | "
+ $"дефов: {content.Defs.All<TerrainDef>().Count} террейна, "
+ $"{content.Defs.All<PlantDef>().Count} растений, "
+ $"{content.Defs.All<PawnDef>().Count} жителей"
);
if (Array.IndexOf(args, "--listen") >= 0)
{
using var socketServer = new WebSocketServer(port);
socketServer.Start();
using var host = new HeadlessHost(
new HeadlessHostOptions { TicksPerSecond = ticksPerSecond, Realtime = true },
new HeadlessWorldScene(content, socketServer)
);
using var cancel = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
cancel.Cancel();
};
Log.Info(
$"Мир онлайн: ws://localhost:{port}, {HeadlessWorldScene.PawnCount} жителей, "
+ $"{ticksPerSecond} тиков/с. Клиент: dotnet run --project src/LittleSim -- --connect. "
+ "Ctrl+C — остановка."
);
host.Run(cancel.Token);
Log.Info($"Сервер остановлен на тике {host.TickCount}.");
return 0;
}
using (
var host = new HeadlessHost(
new HeadlessHostOptions { TicksPerSecond = ticksPerSecond, Realtime = false },
new HeadlessWorldScene(content)
)
)
{
var ticks = (long)((double)days * WorldScene.SecondsPerDay * ticksPerSecond);
var stopwatch = Stopwatch.StartNew();
host.RunTicks(ticks);
Log.Info(
$"{days} игровых дней за {stopwatch.Elapsed.TotalSeconds:F1} с реального времени "
+ $"({ticks} тиков, {ticks / Math.Max(stopwatch.Elapsed.TotalSeconds, 0.001):F0} тиков/с)"
);
}
return 0;
// Подключается к работающему серверу, секунду слушает снапшоты и показывает жителей —
// консольная проверка репликации без игрового клиента.
async Task<int> Probe(int probePort)
{
var store = new EntityStore();
var replication = new ReplicationClient(NetSchema.Create(), store);
var uri = new Uri($"ws://localhost:{probePort}/");
Log.Info($"Подключение к {uri}…");
WebSocketClient connection;
try
{
connection = await WebSocketClient.ConnectAsync(
uri,
new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token
);
}
catch (Exception exception)
{
Log.Error($"Не подключилось: {exception.GetBaseException().Message}");
return 1;
}
using (connection)
{
await Task.Delay(1000);
replication.Pump(connection);
var first = SamplePositions(store);
Log.Info($"Снапшот получен: {replication.EntityCount} жителей");
await Task.Delay(2000);
replication.Pump(connection);
var second = SamplePositions(store);
for (var i = 0; i < first.Count; i++)
{
Log.Info(
$" житель {first[i].NetId}: ({first[i].X:F0}, {first[i].Y:F0}) → "
+ $"({second[i].X:F0}, {second[i].Y:F0})"
);
}
var moved = first.Where((p, i) => p.X != second[i].X || p.Y != second[i].Y).Count();
Log.Info($"Двигались {moved} из {first.Count} показанных — мир жив.");
return replication.EntityCount > 0 && moved > 0 ? 0 : 1;
}
}
static List<(int NetId, float X, float Y)> SamplePositions(EntityStore store)
{
var result = new List<(int, float, float)>();
foreach (var entity in store.Entities)
{
if (result.Count == 5)
{
break;
}
if (entity.HasComponent<NetId>() && entity.HasComponent<Transform2D>())
{
var position = entity.GetComponent<Transform2D>().Position;
result.Add((entity.GetComponent<NetId>().Value, position.X, position.Y));
}
}
return result;
}
int ReadOption(string name, int fallback)
{
var index = Array.IndexOf(args, name);
return index >= 0 && index + 1 < args.Length && int.TryParse(args[index + 1], out var value)
? value
: fallback;
}
+12
View File
@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@@ -0,0 +1,15 @@
#----------------------------- Global Properties ----------------------------#
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:BlazorGL
/config:
/profile:Reach
/compress:True
#-------------------------------- References --------------------------------#
#---------------------------------- Content ---------------------------------#
+1
View File
@@ -0,0 +1 @@
<Project></Project>
+63
View File
@@ -0,0 +1,63 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<RootNamespace>LittleSim.Web</RootNamespace>
<AssemblyName>LittleSim.Web</AssemblyName>
<DefineConstants>$(DefineConstants);BLAZORGL</DefineConstants>
<KniPlatform>BlazorGL</KniPlatform>
</PropertyGroup>
<PropertyGroup>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
</PropertyGroup>
<ItemGroup>
<Compile Include="Pages\Index.razor.cs" />
<Compile Include="Program.cs" />
<Compile Include="LittleSimWebGame.cs" />
<Compile Include="NetContract.cs" />
<Compile Include="WorldViewScene.cs" />
</ItemGroup>
<ItemGroup>
<!--
Веб-клиент собирается против KNI (форк MonoGame с платформой Blazor/WebGL), поэтому
ссылается только на платформо-независимые библиотеки движка: Core и Net.
Графические библиотеки движка (DesktopGL) сюда подключать нельзя — у KNI свои
сборки с теми же неймспейсами.
-->
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Net\MrGameEng.Net.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="nkast.Xna.Framework" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Content" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Graphics" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Audio" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Media" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Input" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Game" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Devices" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.Storage" Version="4.2.9001" />
<PackageReference Include="nkast.Xna.Framework.XR" Version="4.2.9001" />
<PackageReference Include="nkast.Kni.Platform.Blazor.GL" Version="4.2.9001.2" />
<PackageReference Include="nkast.Xna.Framework.Content.Pipeline.Builder" Version="4.2.9001" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.17" />
<PackageReference
Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer"
Version="8.0.17"
PrivateAssets="all"
/>
</ItemGroup>
<ItemGroup>
<KniContentReference Include="Content\LittleSimWebContent.mgcb" />
</ItemGroup>
</Project>
+62
View File
@@ -0,0 +1,62 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Core;
namespace LittleSim.Web;
/// <summary>
/// Веб-хост LittleSim поверх KNI (BlazorGL/WebGL) — браузерный аналог
/// MrGameEng.Host.GameHost: владеет EngineContext ядра, гонит GameClock и фазы сцены.
/// Цикл тикается из requestAnimationFrame (см. Pages/Index.razor.cs). Когда графика
/// движка научится собираться под KNI, этот класс переедет в MrGameEng.Host.Web.
/// </summary>
public class LittleSimWebGame : Game
{
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
public EngineContext Context { get; } = new EngineContext();
private readonly Uri _server;
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch = null!;
private Texture2D _pixel = null!;
/// <summary>Игра, подключающаяся к серверу <paramref name="server"/>.</summary>
public LittleSimWebGame(Uri server)
{
_server = server;
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
/// <inheritdoc />
protected override void Initialize()
{
base.Initialize();
Context.Scenes.Switch(new WorldViewScene(_server, () => _spriteBatch, () => _pixel));
}
/// <inheritdoc />
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
_pixel = new Texture2D(GraphicsDevice, 1, 1);
_pixel.SetData(new[] { Color.White });
}
/// <inheritdoc />
protected override void Update(GameTime gameTime)
{
Context.Clock.Advance((float)gameTime.ElapsedGameTime.TotalSeconds);
Context.Scenes.Update(Context.Clock);
base.Update(gameTime);
}
/// <inheritdoc />
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(new Color(12, 16, 24));
Context.Scenes.Draw(Context.Clock);
base.Draw(gameTime);
}
}
+7
View File
@@ -0,0 +1,7 @@
@inherits LayoutComponentBase
<div class="page">
<main>
@Body
</main>
</div>
+98
View File
@@ -0,0 +1,98 @@
.page
{
position: relative;
display: flex;
flex-direction: column;
}
main
{
flex: 1;
}
.sidebar
{
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row
{
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link
{
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover
{
text-decoration: underline;
}
.top-row ::deep a:first-child
{
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px)
{
.top-row:not(.auth)
{
display: none;
}
.top-row.auth
{
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link
{
margin-left: 0;
}
}
@media (min-width: 641px)
{
.page
{
flex-direction: row;
}
.sidebar
{
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row
{
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child
{
flex: 1;
text-align: right;
width: 0;
}
.top-row, article
{
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
+42
View File
@@ -0,0 +1,42 @@
using Friflo.Engine.ECS;
using Microsoft.Xna.Framework;
using MrGameEng.Net;
namespace LittleSim.Web;
// ВНИМАНИЕ: бинарное зеркало сетевого контракта десктопа (src/LittleSim/Net/NetSchema.cs).
// Веб-клиент не может ссылаться на LittleSim/MrGameEng.Graphics (они собраны против
// MonoGame DesktopGL, а тут KNI), поэтому реплицируемые компоненты продублированы
// со СТРОГО тем же лейаутом и порядком регистрации. Меняешь схему там — меняй здесь.
// Уйдёт после пер-платформенной сборки графических библиотек (см. docs/web-client.md).
/// <summary>Зеркало MrGameEng.Graphics.Transform2D: Position(8) + Rotation(4) + Scale(8).</summary>
public struct NetTransform : IComponent
{
/// <summary>Позиция в мировых координатах.</summary>
public Vector2 Position;
/// <summary>Поворот в радианах.</summary>
public float Rotation;
/// <summary>Масштаб (у жителей — размер квада в пикселях).</summary>
public Vector2 Scale;
}
/// <summary>Зеркало LittleSim.Sim.PawnNeeds: Energy(4).</summary>
public struct NetPawnNeeds : IComponent
{
/// <summary>Запас сил жителя в [0, 1] — затемняет спрайт.</summary>
public float Energy;
}
/// <summary>Схема репликации веб-клиента — порядок тот же, что в NetSchema десктопа.</summary>
public static class WebNetSchema
{
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
public const int DefaultPort = 9050;
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
public static ReplicationSchema Create() =>
new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
}
+20
View File
@@ -0,0 +1,20 @@
@page "/"
@page "/index.html"
@inject IJSRuntime JsRuntime
@using nkast.Wasm.Canvas
<PageTitle>LittleSim</PageTitle>
<div id="canvasHolder" style="
background: #000;
margin:0%;
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
width:100vw;
height:100vh;
">
<canvas id="theCanvas" style="touch-action:none;"></canvas>
</div>
+60
View File
@@ -0,0 +1,60 @@
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Microsoft.Xna.Framework;
namespace LittleSim.Web.Pages
{
public partial class Index
{
[Inject]
private NavigationManager Navigation { get; set; } = null!;
private Game? _game;
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (firstRender)
{
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
}
}
[JSInvokable]
public void TickDotNet()
{
if (_game == null)
{
_game = new LittleSimWebGame(ResolveServerUri());
_game.Run();
}
_game.Tick();
}
// Адрес сервера: ?server=ws://host:port в URL страницы; по умолчанию —
// хост самой страницы на порту LittleSim.Server.
private Uri ResolveServerUri()
{
var page = new Uri(Navigation.Uri);
var query = page.Query.TrimStart('?');
foreach (var pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var separator = pair.IndexOf('=');
if (separator > 0 && pair[..separator] == "server")
{
return new Uri(Uri.UnescapeDataString(pair[(separator + 1)..]));
}
}
return new UriBuilder
{
Scheme = "ws",
Host = page.Host,
Port = WebNetSchema.DefaultPort,
}.Uri;
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace LittleSim.Web
{
internal class Program
{
private static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient()
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress),
});
await builder.Build().RunAsync();
}
}
}
@@ -0,0 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56897",
"sslPort": 0
}
},
"profiles": {
"KniWebSpike": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5259",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+153
View File
@@ -0,0 +1,153 @@
using System;
using System.Threading.Tasks;
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Core;
using MrGameEng.Net;
namespace LittleSim.Web;
/// <summary>
/// Браузерный клиент мира LittleSim: подключается к дедикейтед-серверу
/// (LittleSim.Server --listen), применяет дельта-снапшоты в свой EntityStore и рисует
/// жителей через KNI SpriteBatch (WebGL). Симуляция целиком на сервере — сюда приезжают
/// только компоненты схемы (позиция + потребности); позиции сглаживаются до частоты
/// кадра, усталость затемняет квадратик жителя.
/// </summary>
public sealed class WorldViewScene : Scene
{
private readonly Uri _server;
private readonly Func<SpriteBatch> _spriteBatch;
private readonly Func<Texture2D> _pixel;
private ReplicationClient _replication = null!;
private Task<WebSocketClient>? _connecting;
private WebSocketClient? _connection;
/// <summary>Сцена, подключающаяся к <paramref name="server"/>.</summary>
public WorldViewScene(Uri server, Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
{
_server = server;
_spriteBatch = spriteBatch;
_pixel = pixel;
}
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
private struct NetLerp : IComponent
{
public Vector2 Visual;
public Vector2 Target;
public bool Initialized;
}
protected override void OnLoad()
{
_replication = new ReplicationClient(WebNetSchema.Create(), Store);
_replication.EntitySpawned += entity => entity.AddComponent(new NetLerp());
UpdateSystems.Add(new CallbackSystem(Pump));
UpdateSystems.Add(new NetSmoothingSystem());
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
Log.Info($"LittleSim.Web: connecting to {_server}…");
_connecting = WebSocketClient.ConnectAsync(_server);
}
protected override void OnUnload() => _connection?.Close();
private void Pump()
{
if (_connecting is { IsCompleted: true } finished)
{
_connecting = null;
if (finished.IsFaulted)
{
Log.Error(
$"Connect to {_server} failed: "
+ finished.Exception?.GetBaseException().Message
);
}
else
{
_connection = finished.Result;
Log.Info($"Connected to {_server}");
}
}
if (_connection is not null)
{
_replication.Pump(_connection);
}
}
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
private sealed class CallbackSystem(Action update) : BaseSystem
{
protected override void OnUpdateGroup() => update();
}
/// <summary>Та же экспонента, что в NetSmoothingSystem десктопа (LittleSim/Net/NetSmoothing.cs).</summary>
private sealed class NetSmoothingSystem : QuerySystem<NetTransform, NetLerp>
{
private const float Rate = 12f;
protected override void OnUpdate()
{
var blend = 1f - MathF.Exp(-Rate * Tick.deltaTime);
Query.ForEachEntity(
(ref NetTransform transform, ref NetLerp lerp, Entity _) =>
{
if (!lerp.Initialized)
{
lerp.Visual = lerp.Target = transform.Position;
lerp.Initialized = true;
return;
}
if (transform.Position != lerp.Visual)
{
lerp.Target = transform.Position;
}
lerp.Visual = Vector2.Lerp(lerp.Visual, lerp.Target, blend);
transform.Position = lerp.Visual;
}
);
}
}
/// <summary>Житель — квадратик размером Scale, затемняющийся с усталостью (как на десктопе).</summary>
private sealed class PawnDrawSystem(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
: QuerySystem<NetTransform, NetPawnNeeds>
{
private const float MinBrightness = 0.45f;
protected override void OnUpdate()
{
var batch = spriteBatch();
var white = pixel();
batch.Begin();
Query.ForEachEntity(
(ref NetTransform transform, ref NetPawnNeeds needs, Entity _) =>
{
var size = transform.Scale;
var brightness =
MinBrightness + (1f - MinBrightness) * Math.Clamp(needs.Energy, 0f, 1f);
batch.Draw(
white,
new Rectangle(
(int)(transform.Position.X - size.X / 2f),
(int)(transform.Position.Y - size.Y / 2f),
(int)size.X,
(int)size.Y
),
Color.White * brightness
);
}
);
batch.End();
}
}
}
+10
View File
@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using nkast.Wasm.Canvas
@using LittleSim.Web
+97
View File
@@ -0,0 +1,97 @@
html, body
{
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus
{
outline: none;
}
a, .btn-link
{
color: #0077cc;
}
.btn-primary
{
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.content
{
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox])
{
outline: 1px solid #26b050;
}
.invalid
{
outline: 1px solid red;
}
.validation-message
{
color: red;
}
#blazor-error-ui
{
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss
{
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary
{
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after
{
content: "An error has occurred."
}
#theCanvas
{
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
/* Disable text highlighting and magnifying glass on iPhone/webkit */
-webkit-user-select: none;
}
#canvas
{
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+114
View File
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>LittleSim</title>
<base href="./" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="LittleSim.Web.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<div id="loading" style="display: table-cell; margin: auto; width:100vw; height:100vh; vertical-align: middle; background: #ffcc10;">
<div style="display: block; margin: auto; width: 9em; color: white;font-family: 'Segoe UI', sans-serif;">
<div style="text-align: center; font-size: 0.85em;">Made with<br/><a href="https://github.com/kniEngine/kni"><img src="kni.png" border="0" alt="Kni"></a></div>
<div style="text-align: center; font-size: 1.8em;">loading&nbsp;<marquee style="width:0.9em; vertical-align: bottom;">.&nbsp;.&nbsp;.&nbsp;&nbsp;&nbsp;</marquee></div>
</div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">x</a>
</div>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script type="module">
import { BrotliDecode } from './js/decode.min.js';
window.BrotliDecode = BrotliDecode;
// Set this to enable Brotli (.br) decompression on static webServers
// that don't support content compression and http://.
var enableBrotliDecompression = false;
Blazor.start({
loadBootResource: function (type, name, defaultUri, integrity)
{
if (enableBrotliDecompression === true && type !== 'dotnetjs' && location.hostname !== 'localhost')
{
return (async function()
{
const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
if (!response.ok)
throw new Error(response.statusText);
const originalResponseBuffer = await response.arrayBuffer();
const originalResponseArray = new Int8Array(originalResponseBuffer);
const contentType = (type === 'dotnetwasm')
? 'application/wasm'
: 'application/octet-stream';
const decompressedResponseArray = BrotliDecode(originalResponseArray);
return new Response(decompressedResponseArray,
{ headers: { 'content-type': contentType }
});
})();
}
}
});
</script>
<script src="_content/nkast.Wasm.JSInterop/js/JSObject.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Dom/js/Window.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Dom/js/Document.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Dom/js/Navigator.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Dom/js/Gamepad.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Dom/js/Media.8.0.11.js"></script>
<script src="_content/nkast.Wasm.XHR/js/XHR.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Canvas/js/Canvas.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Canvas/js/CanvasGLContext.8.0.11.js"></script>
<script src="_content/nkast.Wasm.Audio/js/Audio.8.0.11.js"></script>
<script src="_content/nkast.Wasm.XR/js/XR.8.0.11.js"></script>
<script>
function tickJS()
{
window.theInstance.invokeMethod('TickDotNet');
window.requestAnimationFrame(tickJS);
}
window.initRenderJS = (instance) =>
{
window.theInstance = instance;
// set initial canvas size
var canvas = document.getElementById('theCanvas');
var holder = document.getElementById('canvasHolder');
canvas.width = holder.clientWidth;
canvas.height = holder.clientHeight;
// disable context menu on right click
canvas.addEventListener("contextmenu", e => e.preventDefault());
// begin game loop
window.requestAnimationFrame(tickJS);
};
window.addEventListener("keydown", function(event)
{
// Prevent Arrows Keys and Spacebar scrolling the outer page
// when running inside an iframe. e.g: itch.io embedding.
if ([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1)
event.preventDefault();
});
window.addEventListener("wheel", function(event)
{
// Prevent Mousewheel scrolling the outer page
// when running inside an iframe. e.g: itch.io embedding.
event.preventDefault();
}, { passive: false });
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
// micProcessor.js
class MicProcessor extends AudioWorkletProcessor
{
constructor()
{
super();
// global variables for testing
var sampleRate = globalThis.sampleRate;
var currentFrame = globalThis.currentFrame;
var currentTime = globalThis.currentTime;
var currentRenderQuantum = globalThis.currentRenderQuantum;
this.SampleRate = sampleRate;
this.TargetSamples = Math.floor(this.SampleRate * 0.1); // 100ms
this.Buffer = new Float32Array(this.TargetSamples);
this.BufferIndex = 0;
this.port.onmessage = (event) =>
{
var data = event.data;
if (typeof data === 'number')
{
//this.port.postMessage(data); // echo back test
}
if (data instanceof Uint8Array)
{
}
};
}
process(inputs, outputs, parameters)
{
var inChannel0 = inputs[0][0];
if (!inChannel0) return true;
let srcIndex = 0;
var srcLen = inChannel0.length;
while (srcIndex < srcLen)
{
var remaining = this.TargetSamples - this.BufferIndex;
var copyCount = Math.min(remaining, srcLen - srcIndex);
this.Buffer.set(
inChannel0.subarray(srcIndex, srcIndex + copyCount),
this.BufferIndex);
this.BufferIndex += copyCount;
srcIndex += copyCount;
if (this.BufferIndex >= this.TargetSamples)
{
this.SendBuffer();
this.BufferIndex = 0;
}
}
return true;
}
SendBuffer()
{
// convert to 16-6bit PCM
var int16 = new Int16Array(this.TargetSamples);
for (var i = 0; i < this.TargetSamples; i++)
{
int16[i] = this.Buffer[i] * 32767;
}
var byteArray = new Uint8Array(int16.buffer);
this.port.postMessage(byteArray, [byteArray.buffer]);
}
}
registerProcessor('mic-processor', MicProcessor);
@@ -0,0 +1,87 @@
// streamProcessor.js
class StreamProcessor extends AudioWorkletProcessor
{
constructor()
{
super();
this.queue = [];
this.port.onmessage = (event) =>
{
var data = event.data;
if (typeof data === 'number')
{
if (data === 2)
{
this.queue = [];
}
}
if (data instanceof Uint8Array)
{
const buffer = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
buffer.offset = 0;
this.queue.push(buffer);
}
};
}
process(inputs, outputs, parameters)
{
const output = outputs[0];
const channelCount = output.length;
const sampleCount = output[0].length;
let written = 0;
while (written < sampleCount && this.queue.length > 0)
{
const buffer = this.queue[0];
const offset = buffer.offset;
const available = buffer.length - offset;
const needed = sampleCount - written;
const copyCount = Math.min(available, needed);
for (let i = 0; i < copyCount; i++)
{
for (let c = 0; c < channelCount; c++)
{
const channel = output[c];
let value = (buffer[offset+i] / 32767);
channel[written+i] = value;
}
}
written += copyCount;
buffer.offset += copyCount;
if (buffer.offset >= buffer.length)
{
this.queue.shift();
this.port.postMessage(1);
}
}
// Fill remaining samples with silence
if (written < sampleCount)
{
for (let c = 0; c < channelCount; c++)
{
const channel = output[c];
for (let i = written; i < sampleCount; i++)
{
let value = 0;
channel[i] = value;
}
}
}
return true;
}
}
registerProcessor("stream-processor", StreamProcessor);
Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

+26
View File
@@ -0,0 +1,26 @@
using MrGameEng.Mods;
namespace LittleSim.App;
/// <summary>
/// Расположение пользовательских данных игры рядом с папкой <c>Mods</c> (где живёт и
/// <c>Cache/</c>): файл настроек и каталог сохранений. Корень ищется один раз — поднятием
/// вверх по дереву от исполняемого файла (как делает загрузка модов).
/// </summary>
public static class GamePaths
{
private static string? _root;
/// <summary>Корневая папка данных (родитель папки <c>Mods</c>).</summary>
public static string Root =>
_root ??= Path.GetDirectoryName(
ModLoader.FindModsRoot(AppContext.BaseDirectory)
?? throw new DirectoryNotFoundException("Папка Mods не найдена.")
)!;
/// <summary>Путь к файлу настроек игры.</summary>
public static string SettingsFile => Path.Combine(Root, "settings.json");
/// <summary>Каталог файлов сохранений.</summary>
public static string SavesDirectory => Path.Combine(Root, "Saves");
}
+90
View File
@@ -0,0 +1,90 @@
using System.Text.Json;
using Microsoft.Xna.Framework;
using MrGameEng.Audio;
using MrGameEng.Mods;
namespace LittleSim.App;
/// <summary>
/// Пользовательские настройки игры, сохраняемые в <c>settings.json</c>: язык интерфейса,
/// полноэкранный режим, вертикальная синхронизация, размер окна и общая громкость.
/// </summary>
public sealed class GameSettings
{
/// <summary>Код языка интерфейса (например, "ru" или "en").</summary>
public string Language { get; set; } = "ru";
/// <summary>Полноэкранный (безрамочный) режим.</summary>
public bool Fullscreen { get; set; }
/// <summary>Вертикальная синхронизация.</summary>
public bool VSync { get; set; } = true;
/// <summary>Ширина окна в пикселях.</summary>
public int Width { get; set; } = 1280;
/// <summary>Высота окна в пикселях.</summary>
public int Height { get; set; } = 720;
/// <summary>Общая громкость 0..1.</summary>
public float Volume { get; set; } = 1f;
}
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
public static class GameSettingsStore
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true,
};
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary>
public static GameSettings Load()
{
try
{
if (File.Exists(GamePaths.SettingsFile))
{
return JsonSerializer.Deserialize<GameSettings>(
File.ReadAllText(GamePaths.SettingsFile),
JsonOptions
) ?? new GameSettings();
}
}
catch (Exception exception) when (exception is IOException or JsonException)
{
// Битый файл настроек не должен ронять запуск — откатываемся к умолчаниям.
}
return new GameSettings();
}
/// <summary>Сохраняет настройки в <see cref="GamePaths.SettingsFile"/>.</summary>
public static void Save(GameSettings settings)
{
Directory.CreateDirectory(GamePaths.Root);
File.WriteAllText(GamePaths.SettingsFile, JsonSerializer.Serialize(settings, JsonOptions));
}
/// <summary>
/// Применяет настройки к движку: язык (<paramref name="languages"/>), полноэкранный
/// режим/VSync/размер окна (<paramref name="graphics"/>) и громкость (<paramref name="audio"/>).
/// </summary>
public static void Apply(
GameSettings settings,
GraphicsDeviceManager graphics,
LanguageManager languages,
AudioManager audio
)
{
languages.SetLanguage(settings.Language);
audio.MasterVolume = settings.Volume;
graphics.IsFullScreen = settings.Fullscreen;
graphics.SynchronizeWithVerticalRetrace = settings.VSync;
graphics.PreferredBackBufferWidth = settings.Width;
graphics.PreferredBackBufferHeight = settings.Height;
graphics.ApplyChanges();
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace LittleSim.App;
/// <summary>
/// Параметры одного мира: имя, размер сетки, сид, число проходов сглаживания рельефа и
/// население. Заменяет хардкод в <see cref="Scenes.WorldScene"/>; строится из выбранного
/// <see cref="Content.WorldPresetDef"/> и пользовательского ввода в меню «Новый мир» либо
/// восстанавливается из <see cref="WorldSave"/>.
/// </summary>
public sealed record WorldConfig
{
/// <summary>Отображаемое имя мира (и имя файла сохранения).</summary>
public string Name { get; init; } = "World";
/// <summary>Ширина мира в клетках.</summary>
public int Width { get; init; } = 120;
/// <summary>Высота мира в клетках.</summary>
public int Height { get; init; } = 70;
/// <summary>Целочисленный сид — весь мир воспроизводится из него.</summary>
public int Seed { get; init; }
/// <summary>Число проходов сглаживания рельефа (больше — крупнее «континенты»).</summary>
public int SmoothPasses { get; init; } = 4;
/// <summary>Число жителей при генерации.</summary>
public int Population { get; init; } = 80;
}
+196
View File
@@ -0,0 +1,196 @@
using System.Text.Json;
using MrGameEng.Genetics;
namespace LittleSim.App;
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
public sealed class PlantSave
{
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
public string Species { get; set; } = "";
/// <summary>Позиция X в мировых координатах.</summary>
public float X { get; set; }
/// <summary>Позиция Y в мировых координатах.</summary>
public float Y { get; set; }
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays { get; set; }
/// <summary>Текущая стадия роста.</summary>
public int Stage { get; set; }
/// <summary>Плодородность клетки.</summary>
public float CellFertility { get; set; }
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
public Dictionary<string, Allele> Genome { get; set; } = new();
}
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
public sealed class PawnSave
{
/// <summary>Имя дефа жителя — по нему при загрузке берётся текстура из атласов.</summary>
public string DefName { get; set; } = "";
/// <summary>Позиция X в мировых координатах.</summary>
public float X { get; set; }
/// <summary>Позиция Y в мировых координатах.</summary>
public float Y { get; set; }
/// <summary>Масштаб трансформа (зависит от размера дефа и клетки).</summary>
public float Scale { get; set; } = 1f;
/// <summary>Запас энергии [0, 1].</summary>
public float Energy { get; set; }
/// <summary>Текущее действие (значение <see cref="Sim.PawnAction"/>).</summary>
public int Action { get; set; }
/// <summary>Секунды до следующего пересмотра решения.</summary>
public float DecideIn { get; set; }
/// <summary>Направление блуждания X.</summary>
public float DirX { get; set; }
/// <summary>Направление блуждания Y.</summary>
public float DirY { get; set; }
/// <summary>Секунды до смены направления.</summary>
public float ChangeIn { get; set; }
}
/// <summary>
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
/// детерминированно регенерируется из сида при загрузке.
/// </summary>
public sealed class WorldSave
{
/// <summary>Имя мира (и основа имени файла).</summary>
public string Name { get; set; } = "";
/// <summary>Ширина мира в клетках.</summary>
public int Width { get; set; }
/// <summary>Высота мира в клетках.</summary>
public int Height { get; set; }
/// <summary>Сид генерации.</summary>
public int Seed { get; set; }
/// <summary>Число проходов сглаживания рельефа.</summary>
public int SmoothPasses { get; set; } = 4;
/// <summary>Население на момент сохранения.</summary>
public int Population { get; set; }
/// <summary>Момент сохранения (UTC).</summary>
public DateTime SavedUtc { get; set; }
/// <summary>Прошедшее время симуляции в секундах.</summary>
public double ElapsedSeconds { get; set; }
/// <summary>Снимок жителей.</summary>
public List<PawnSave> Pawns { get; set; } = [];
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
public List<PlantSave> Plants { get; set; } = [];
/// <summary>Конфиг мира для пересоздания сцены.</summary>
public WorldConfig ToConfig() =>
new()
{
Name = Name,
Width = Width,
Height = Height,
Seed = Seed,
SmoothPasses = SmoothPasses,
Population = Population,
};
}
/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
public sealed class SaveStore
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true,
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
};
private readonly string _directory;
/// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
public SaveStore(string? directory = null) =>
_directory = directory ?? GamePaths.SavesDirectory;
/// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
public readonly record struct Entry(string FileName, WorldSave Save);
/// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
public IReadOnlyList<Entry> List()
{
if (!Directory.Exists(_directory))
{
return [];
}
var entries = new List<Entry>();
foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
{
try
{
var save = JsonSerializer.Deserialize<WorldSave>(
File.ReadAllText(file),
JsonOptions
);
if (save is not null)
{
entries.Add(new Entry(Path.GetFileName(file), save));
}
}
catch (Exception exception) when (exception is IOException or JsonException)
{
// Битый файл сохранения не должен ломать список — пропускаем.
}
}
entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
return entries;
}
/// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
public string Write(WorldSave save)
{
Directory.CreateDirectory(_directory);
var fileName = Sanitize(save.Name) + ".json";
File.WriteAllText(
Path.Combine(_directory, fileName),
JsonSerializer.Serialize(save, JsonOptions)
);
return fileName;
}
/// <summary>Удаляет файл сохранения по имени.</summary>
public void Delete(string fileName)
{
var path = Path.Combine(_directory, fileName);
if (File.Exists(path))
{
File.Delete(path);
}
}
private static string Sanitize(string name)
{
var trimmed = name.Trim();
var safe = string.Concat(
trimmed.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)
);
return string.IsNullOrEmpty(safe) ? "world" : safe;
}
}
+35 -14
View File
@@ -1,5 +1,6 @@
using MrGameEng.Atlases; using MrGameEng.Atlases;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Mods; using MrGameEng.Mods;
namespace LittleSim.Content; namespace LittleSim.Content;
@@ -44,8 +45,10 @@ public sealed class GameContent
/// <summary> /// <summary>
/// Находит папку Mods (вверх по дереву от исполняемого файла), загружает моды, дефы и /// Находит папку Mods (вверх по дереву от исполняемого файла), загружает моды, дефы и
/// языки и инкрементально собирает атласы из смерженного дерева текстур всех модов. /// языки и инкрементально собирает атласы из смерженного дерева текстур всех модов.
/// С <paramref name="buildAtlases"/> = false атласы не собираются — headless-режим
/// (дедикейтед-сервер) текстур не рисует.
/// </summary> /// </summary>
public static GameContent Load() public static GameContent Load(bool buildAtlases = true)
{ {
var modsRoot = var modsRoot =
ModLoader.FindModsRoot(AppContext.BaseDirectory) ModLoader.FindModsRoot(AppContext.BaseDirectory)
@@ -56,8 +59,19 @@ public sealed class GameContent
var defs = new DefDatabase(); var defs = new DefDatabase();
defs.RegisterType<TerrainDef>("Terrain"); defs.RegisterType<TerrainDef>("Terrain");
defs.RegisterType<GeneDef>("Gene");
defs.RegisterType<ProductDef>("Product");
defs.RegisterType<PlantDef>("Plant"); defs.RegisterType<PlantDef>("Plant");
defs.RegisterType<PawnDef>("Pawn"); defs.RegisterType<PawnDef>("Pawn");
defs.RegisterType<WorldPresetDef>("WorldPreset");
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
defs.RegisterValidator(
"Product",
"defName",
"^Product",
"product defName must start with 'Product'"
);
defs.Load(mods); defs.Load(mods);
var languages = new LanguageManager(defaultLanguage: "ru"); var languages = new LanguageManager(defaultLanguage: "ru");
@@ -70,19 +84,26 @@ public sealed class GameContent
"Cache", "Cache",
"Atlases" "Atlases"
); );
var textures = ModContentTree.Build(mods, "Textures", ".png", ".jpg", ".jpeg", ".bmp"); if (buildAtlases)
var result = AtlasBuilder.Build( {
new AtlasBuildOptions var textures = ModContentTree.Build(mods, "Textures", ".png", ".jpg", ".jpeg", ".bmp");
{ var result = AtlasBuilder.Build(
OutputDirectory = atlasCacheDirectory, new AtlasBuildOptions
GroupDepth = ModAtlases.GroupDepth, {
}, OutputDirectory = atlasCacheDirectory,
textures.Files.Select(f => (f.FullPath, f.RelativePath)) GroupDepth = ModAtlases.GroupDepth,
); },
var built = result.Groups.Count(g => !g.Skipped); textures.Files.Select(f => (f.FullPath, f.RelativePath))
Log.Info( );
$"Atlases: {built} built, {result.Groups.Count - built} up to date ({result.Groups.Count} total)" var built = result.Groups.Count(g => !g.Skipped);
); Log.Info(
$"Atlases: {built} built, {result.Groups.Count - built} up to date ({result.Groups.Count} total)"
);
}
else
{
Log.Info("Atlases: skipped (headless)");
}
return new GameContent( return new GameContent(
mods, mods,
+137 -3
View File
@@ -27,27 +27,161 @@ public sealed class TerrainDef : Def
/// <summary>Суша: здесь появляются жители, животные и растения.</summary> /// <summary>Суша: здесь появляются жители, животные и растения.</summary>
public bool IsLand { get; init; } public bool IsLand { get; init; }
/// <summary>Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.</summary>
public bool BlocksLight { get; init; }
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary> /// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
public string? Surface { get; init; } public string? Surface { get; init; }
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary> /// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
public List<ScatterEntry> Scatter { get; init; } = []; public List<ScatterEntry> Scatter { get; init; } = [];
/// <summary>
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
/// </summary>
public float Fertility { get; init; } = 1f;
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary> /// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta; public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
} }
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие.</summary> /// <summary>
/// Одна стадия роста растения (вложенный объект <see cref="PlantDef.Stages"/>): свой спрайт и
/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
/// Растёт на месте — последняя стадия терминальная (её <see cref="GrowDays"/> игнорируется).
/// </summary>
public sealed class PlantGrowthStage
{
/// <summary>Ключ текстуры стадии; null — берётся базовая <see cref="PlantDef.Texture"/>.</summary>
public string? Texture { get; init; }
/// <summary>Размер спрайта в клетках на этой стадии; 0 — берётся базовый <see cref="PlantDef.SizeCells"/>.</summary>
public float SizeCells { get; init; }
/// <summary>Сколько игровых дней расти на этой стадии до перехода к следующей.</summary>
public float GrowDays { get; init; }
/// <summary>Ключ локализации названия стадии (опционально, для будущих тултипов).</summary>
public string? Label { get; init; }
}
/// <summary>
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
/// </summary>
public sealed class ProductDef : Def
{
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
public string Kind { get; init; } = "";
}
/// <summary>
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
/// </summary>
public sealed class GenomeDef
{
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
public float OptimalLight { get; init; } = 0.7f;
/// <summary>Ширина толерантности по свету.</summary>
public float LightTolerance { get; init; } = 0.4f;
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
public float OptimalTemperature { get; init; } = 18f;
/// <summary>Ширина толерантности по температуре (°C).</summary>
public float TemperatureTolerance { get; init; } = 12f;
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
public float OptimalFertility { get; init; } = 1f;
/// <summary>Ширина толерантности по плодородности.</summary>
public float FertilityTolerance { get; init; } = 0.8f;
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
public float Vigor { get; init; } = 1f;
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
public float Lifespan { get; init; } = 80f;
/// <summary>Дальность расселения семян (в клетках).</summary>
public float DispersalRange { get; init; } = 2f;
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
public float ReproduceInterval { get; init; } = 8f;
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
public float SelfPollination { get; init; } = 0.4f;
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
public float MutationRate { get; init; } = 0.05f;
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
public float VariantChance { get; init; } = 0.15f;
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
public float Spread { get; init; } = 0.08f;
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
public float FruitYield { get; init; }
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
public float FruitSeason { get; init; } = 1f;
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
public float HarvestAmount { get; init; } = 1f;
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
public float LeafHue { get; init; } = 0.33f;
}
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
public sealed class PlantDef : Def public sealed class PlantDef : Def
{ {
/// <summary>Ключ текстуры (путь региона в атласах).</summary> /// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
public string Texture { get; init; } = ""; public string Texture { get; init; } = "";
/// <summary>Размер спрайта в клетках.</summary> /// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
public float SizeCells { get; init; } = 1f; public float SizeCells { get; init; } = 1f;
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary> /// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
public float TrunkRadiusCells { get; init; } public float TrunkRadiusCells { get; init; }
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
public List<PlantGrowthStage> Stages { get; init; } = [];
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
public GenomeDef Genome { get; init; } = new();
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
public string? HarvestProduct { get; init; }
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
public string? FruitProduct { get; init; }
}
/// <summary>
/// Пресет размера мира (Defs/worldpresets.json): габариты сетки и население. Меню «Новый
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
/// </summary>
public sealed class WorldPresetDef : Def
{
/// <summary>Ширина мира в клетках.</summary>
public int Width { get; init; }
/// <summary>Высота мира в клетках.</summary>
public int Height { get; init; }
/// <summary>Число жителей при генерации.</summary>
public int Population { get; init; }
/// <summary>Порядок в списке UI (по возрастанию).</summary>
public int Order { get; init; }
} }
/// <summary>Существо (Defs/pawns.json): текстура, размер и вид (житель/животное).</summary> /// <summary>Существо (Defs/pawns.json): текстура, размер и вид (житель/животное).</summary>
+150
View File
@@ -0,0 +1,150 @@
using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Content;
/// <summary>
/// Готовая (resolved) таблица растений для сцены: по каждому <see cref="PlantDef"/> — массив
/// стадий с уже загруженными регионами атласа, размерами и порогами входа в днях. Строится один
/// раз при загрузке сцены (как <see cref="MrGameEng.Tilemaps.TileSet"/>, потому что нужны атлас и
/// устройство). <see cref="Sim.PlantGrowthSystem"/> индексируется в неё без словарей и аллокаций.
/// </summary>
public sealed class PlantSet
{
/// <summary>Разрешённая стадия: спрайт, размер в клетках и день, с которого стадия активна.</summary>
public readonly record struct Stage(Texture2DRegion Region, float SizeCells, float EnterDay);
/// <summary>Вид растения: его деф и стадии роста по возрастанию <see cref="Stage.EnterDay"/>.</summary>
public sealed class Species
{
/// <summary>Исходный деф растения.</summary>
public required PlantDef Def { get; init; }
/// <summary>Стадии роста по порядку; последняя — терминальная.</summary>
public required Stage[] Stages { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи (геном из общих <see cref="GeneDef"/>).</summary>
public required GenomeTemplate Template { get; init; }
/// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary>
public float MaturityDays => Stages[^1].EnterDay;
/// <summary>Индекс стадии для данного возраста в днях.</summary>
public int StageAt(float ageDays)
{
var stage = 0;
while (stage + 1 < Stages.Length && ageDays >= Stages[stage + 1].EnterDay)
{
stage++;
}
return stage;
}
}
private readonly Species[] _species;
private readonly Dictionary<PlantDef, int> _index = new();
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков растений.</summary>
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
/// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary>
public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device)
{
var genes = content
.Defs.All<GeneDef>()
.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
GeneRegistry = genes;
var defs = content.Defs.All<PlantDef>().ToArray();
_species = new Species[defs.Length];
for (var i = 0; i < defs.Length; i++)
{
var def = defs[i];
_species[i] = new Species
{
Def = def,
Stages = BuildStages(def, atlases, device),
Template = BuildTemplate(def.Genome, genes),
};
_index[def] = i;
}
}
/// <summary>Число видов в таблице.</summary>
public int Count => _species.Length;
/// <summary>Вид по индексу.</summary>
public Species this[int index] => _species[index];
/// <summary>Индекс вида по дефу.</summary>
public int IndexOf(PlantDef def) => _index[def];
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
private static GenomeTemplate BuildTemplate(
GenomeDef g,
IReadOnlyDictionary<string, GeneDef> genes
)
{
GeneDef Gene(string id) =>
genes.TryGetValue(id, out var def)
? def
: throw new KeyNotFoundException(
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?"
);
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
new(Gene(id), baseValue, g.Spread);
return new GenomeTemplate([
Numeric("GeneOptimalLight", g.OptimalLight),
Numeric("GeneLightTolerance", g.LightTolerance),
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
Numeric("GeneOptimalFertility", g.OptimalFertility),
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
Numeric("GeneVigor", g.Vigor),
Numeric("GeneLifespan", g.Lifespan),
Numeric("GeneDispersalRange", g.DispersalRange),
Numeric("GeneReproduceInterval", g.ReproduceInterval),
Numeric("GeneSelfPollination", g.SelfPollination),
Numeric("GeneMutationRate", g.MutationRate),
// Контент (G4): плодоношение, добыча, цвет.
Numeric("GeneFruitYield", g.FruitYield),
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
Numeric("GeneHarvestAmount", g.HarvestAmount),
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
new GenomeTemplate.Entry(
Gene("GeneMorph"),
0f,
0f,
[1f - g.VariantChance, g.VariantChance]
),
]);
}
private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device)
{
// Без явных стадий растение существует как одна терминальная стадия из базовой текстуры.
if (def.Stages.Count == 0)
{
var region = atlases.GetRegion(device, def.Texture);
return [new Stage(region, def.SizeCells, 0f)];
}
var stages = new Stage[def.Stages.Count];
var enterDay = 0f;
for (var i = 0; i < def.Stages.Count; i++)
{
var stage = def.Stages[i];
var region = atlases.GetRegion(device, stage.Texture ?? def.Texture);
var size = stage.SizeCells > 0f ? stage.SizeCells : def.SizeCells;
stages[i] = new Stage(region, size, enterDay);
enterDay += stage.GrowDays; // следующая стадия входит после GrowDays этой
}
return stages;
}
}
+4 -9
View File
@@ -6,18 +6,13 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Host\MrGameEng.Host.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Input\MrGameEng.Input.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Content\MrGameEng.Content.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Atlases\MrGameEng.Atlases.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Simulation\MrGameEng.Simulation.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Tilemaps\MrGameEng.Tilemaps.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Net\MrGameEng.Net.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.AI\MrGameEng.AI.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Collisions\MrGameEng.Collisions.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.UI\MrGameEng.UI.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.UI\MrGameEng.UI.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Mods\MrGameEng.Mods.csproj" />
<ProjectReference <ProjectReference
Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj" Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
OutputItemType="Analyzer" OutputItemType="Analyzer"
+23
View File
@@ -0,0 +1,23 @@
using LittleSim.Sim;
using MrGameEng.Graphics;
using MrGameEng.Net;
namespace LittleSim.Net;
/// <summary>
/// Сетевой контракт LittleSim: какие компоненты реплицируются с сервера на клиентов.
/// Сервер (LittleSim.Server) и клиент (<see cref="Scenes.MultiplayerScene"/>) обязаны
/// строить схему одинаково — порядок регистрации определяет wire-id компонентов.
/// ВНИМАНИЕ: у веб-клиента бинарное зеркало этой схемы
/// (src/LittleSim.Web/NetContract.cs — он собран против KNI и не может ссылаться сюда);
/// меняешь состав или порядок — меняй и там.
/// </summary>
public static class NetSchema
{
/// <summary>Порт сервера по умолчанию.</summary>
public const int DefaultPort = 9050;
/// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary>
public static ReplicationSchema Create() =>
new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
}
+65
View File
@@ -0,0 +1,65 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework;
using MrGameEng.Graphics;
namespace LittleSim.Net;
/// <summary>
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
/// </summary>
public struct NetLerp : IComponent
{
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
public Vector2 Visual;
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
public Vector2 Target;
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
public bool Initialized;
}
/// <summary>
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
/// </summary>
public sealed class NetSmoothingSystem : QuerySystem<Transform2D, NetLerp>
{
// Скорость экспоненциального сглаживания: за ~0.25 с визуал почти догоняет цель.
private const float Rate = 12f;
protected override void OnUpdate()
{
var blend = 1f - MathF.Exp(-Rate * Tick.deltaTime);
foreach (var (transforms, lerps, _) in Query.Chunks)
{
var t = transforms.Span;
var l = lerps.Span;
for (var i = 0; i < t.Length; i++)
{
ref var lerp = ref l[i];
ref var position = ref t[i].Position;
if (!lerp.Initialized)
{
lerp.Visual = lerp.Target = position;
lerp.Initialized = true;
continue;
}
// Транформ трогает только снапшот: разошёлся с нарисованным — новая цель.
if (position != lerp.Visual)
{
lerp.Target = position;
}
lerp.Visual = Vector2.Lerp(lerp.Visual, lerp.Target, blend);
position = lerp.Visual;
}
}
}
}
+41 -7
View File
@@ -1,11 +1,48 @@
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Net;
using LittleSim.Scenes; using LittleSim.Scenes;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Core; using MrGameEng.Genetics;
using MrGameEng.Host;
// Контент модов (дефы, локализация, атласы) загружается до создания окна — чистый CPU. // Контент-линт без окна/GPU: грузит моды/дефы (патчи и валидаторы тоже), компилирует формулы генов
var content = GameContent.Load(); // (включая группировку gsum) и выходит. Удобно для CI.
if (args.Contains("--check-content"))
{
try
{
var content = GameContent.Load();
var genes = content.Defs.All<GeneDef>();
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
var traits = Phenotype.Compute(Genome.Generate(genes, new Random(1)), registry);
Console.WriteLine(
$"content ok: {content.Defs.TypeKeys.Count} def types, {genes.Count} genes, "
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
);
return;
}
catch (Exception error)
{
Console.Error.WriteLine($"content FAILED: {error.Message}");
Environment.Exit(1);
return;
}
}
// --connect [ws://host:port] — после загрузки контента сразу в сетевую сцену
// (см. MultiplayerScene); без аргументов — обычный запуск в главное меню.
string? connect = null;
var connectIndex = Array.IndexOf(args, "--connect");
if (connectIndex >= 0)
{
connect =
connectIndex + 1 < args.Length && !args[connectIndex + 1].StartsWith("--")
? args[connectIndex + 1]
: $"ws://localhost:{NetSchema.DefaultPort}/";
}
// Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне.
using var host = new GameHost( using var host = new GameHost(
new GameHostOptions new GameHostOptions
{ {
@@ -14,10 +51,7 @@ using var host = new GameHost(
Height = 720, Height = 720,
ClearColor = new Color(12, 16, 24), ClearColor = new Color(12, 16, 24),
}, },
new WorldScene(seed: 1337) new BootScene(connect)
); );
host.Context.Services.Add(content);
host.Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory));
host.Run(); host.Run();
+91
View File
@@ -0,0 +1,91 @@
using System;
using System.Threading.Tasks;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using MrGameEng.Audio;
using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Загрузочный экран. Поднимает аудио и управление скоростью, грузит настройки и применяет
/// окно/громкость, затем грузит контент Core-мода в фоне (чистый CPU/диск — без GPU) с
/// анимированной надписью. По готовности регистрирует сервисы и уходит в главное меню.
/// </summary>
public sealed class BootScene : Scene
{
private readonly string[] _steps = { "Загрузка", "Loading" };
private readonly string? _connectTo;
private Task<GameContent>? _load;
private GameSettings _settings = new();
private Label _label = null!;
/// <summary>Обычный запуск — в главное меню.</summary>
public BootScene()
: this(null) { }
/// <summary>С <paramref name="connectTo"/> (ws://host:port) грузится сразу в сетевую сцену.</summary>
public BootScene(string? connectTo) => _connectTo = connectTo;
protected override void OnLoad()
{
Context.UseAudio();
Context.UseGameSpeed(1f, 3f, 6f);
_settings = GameSettingsStore.Load();
var host = (GameHost)Context.Services.Get<Game>();
host.Graphics.IsFullScreen = _settings.Fullscreen;
host.Graphics.SynchronizeWithVerticalRetrace = _settings.VSync;
host.Graphics.PreferredBackBufferWidth = _settings.Width;
host.Graphics.PreferredBackBufferHeight = _settings.Height;
host.Graphics.ApplyChanges();
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
_label = new Label
{
TextColor = Ui.Accent,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
desktop.Root = Ui.Screen(_label);
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
_load = Task.Run(() => GameContent.Load());
UpdateSystems.Add(new CallbackSystem(Tick));
}
private void Tick()
{
var word = _settings.Language == "en" ? _steps[1] : _steps[0];
var dots = new string('.', (int)(Context.Clock.UnscaledTotalTime * 2) % 4);
_label.Text = word + dots;
if (_load is null || !_load.IsCompleted || Context.Scenes.IsTransitioning)
{
return;
}
if (_load.IsFaulted)
{
_label.Text = _load.Exception?.GetBaseException().Message ?? "load failed";
Log.Error($"Content load failed: {_label.Text}");
return;
}
var content = _load.Result;
_load = null;
Context.Services.Add(content);
Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory));
content.Languages.SetLanguage(_settings.Language);
Scene next = _connectTo is null
? new MainMenuScene()
: new MultiplayerScene(new Uri(_connectTo));
Context.Scenes.Switch(next, Transitions.Fade(0.6f));
}
}
+23 -31
View File
@@ -1,32 +1,30 @@
using Friflo.Engine.ECS; using Friflo.Engine.ECS;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Sim;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.DevConsole; using MrGameEng.DevConsole;
using MrGameEng.Graphics; using MrGameEng.Graphics;
namespace LittleSim.Scenes; namespace LittleSim.Scenes;
/// <summary>Спавн растительности по скаттеру дефов террейна (общий для обеих сцен).</summary> /// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
internal static class ScatterSpawner internal static class ScatterSpawner
{ {
/// <summary> /// <summary>
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение /// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
/// со случайным смещением. Деревья (ствол &gt; 0) получают коллайдер-препятствие, /// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
/// когда заданы слои коллизий. /// разновозрастный. Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением
/// управляет <see cref="PlantGrowthSystem"/>.
/// </summary> /// </summary>
public static void Spawn( public static void Spawn(
Scene scene, Scene scene,
GameContent content, GameContent content,
ModAtlases atlases, PlantSet plants,
GraphicsDevice device,
TerrainDef terrain, TerrainDef terrain,
Point cell, Point cell,
int cellSize, int cellSize,
Random random, Random random
uint obstacleLayer = 0,
uint collidesWith = 0
) )
{ {
foreach (var entry in terrain.Scatter) foreach (var entry in terrain.Scatter)
@@ -36,28 +34,22 @@ internal static class ScatterSpawner
continue; continue;
} }
var plant = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]); var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
var region = atlases.GetRegion(device, plant.Texture); var index = plants.IndexOf(def);
var sprite = new Sprite(region, GameLayers.Beings); var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
sprite.CenterOrigin(); var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var entity = scene.Store.CreateEntity( var age = random.NextSingle() * plants[index].MaturityDays;
new Transform2D(
new Vector2(
(cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize,
(cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize
),
scale: new Vector2(cellSize * plant.SizeCells / region.Width)
),
sprite
);
if (obstacleLayer != 0 && plant.TrunkRadiusCells > 0f) PlantFactory.Create(
{ scene.Store,
var trunk = MrGameEng.Collisions.Collider.Circle(cellSize * plant.TrunkRadiusCells); plants,
trunk.Layer = obstacleLayer; index,
trunk.CollidesWith = collidesWith; new Vector2(px, py),
entity.AddComponent(trunk); age,
} plants[index].Template.Generate(random),
terrain.Fertility,
cellSize
);
} }
} }
} }
+47
View File
@@ -0,0 +1,47 @@
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.UI;
namespace LittleSim.Scenes;
/// <summary>Экран «Авторы»: демонстрационный, пока один автор. Назад — кнопкой или Esc.</summary>
public sealed class CreditsScene : Scene
{
protected override void OnLoad()
{
var lang = Context.Services.Get<GameContent>().Languages;
var input = this.UseInput();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12);
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
desktop.Root = Ui.Screen(column);
UpdateSystems.Add(
new CallbackSystem(() =>
{
if (input.IsKeyPressed(Keys.Escape))
{
Back();
}
})
);
}
private void Back()
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
}
}
}
+111
View File
@@ -0,0 +1,111 @@
using LittleSim.App;
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Экран загрузки: список сохранений из <see cref="SaveStore"/> (имя, размер, дата) с
/// кнопками «Загрузить» и «Удалить». Загрузка пересоздаёт мир из полного состояния симуляции.
/// </summary>
public sealed class LoadGameScene : Scene
{
private readonly SaveStore _store = new();
private GameContent _content = null!;
private Desktop _desktop = null!;
protected override void OnLoad()
{
_content = Context.Services.Get<GameContent>();
var input = this.UseInput();
_desktop = this.UseUI();
Rebuild();
UpdateSystems.Add(
new CallbackSystem(() =>
{
if (input.IsKeyPressed(Keys.Escape))
{
Back();
}
})
);
}
private void Rebuild()
{
var lang = _content.Languages;
var column = Ui.Column(8);
column.Widgets.Add(Ui.Title(lang.Get("load.title")));
column.Widgets.Add(new Label { Height = 8 });
var saves = _store.List();
if (saves.Count == 0)
{
column.Widgets.Add(Ui.Subtitle(lang.Get("load.empty")));
}
foreach (var entry in saves)
{
var save = entry.Save;
var row = Ui.Row(8);
row.Widgets.Add(
new Label
{
TextColor = Ui.Accent,
Text =
$"{save.Name} — {save.Width}×{save.Height} — "
+ save.SavedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
}
);
var file = entry.FileName;
row.Widgets.Add(
Ui.Button(
lang.Get("load.load"),
() =>
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(
new WorldScene(save.ToConfig(), save),
Transitions.Fade(0.6f)
);
}
},
width: 130
)
);
row.Widgets.Add(
Ui.Button(
lang.Get("load.delete"),
() =>
{
_store.Delete(file);
Rebuild();
},
width: 130
)
);
column.Widgets.Add(row);
}
column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
_desktop.Root = Ui.Screen(column);
}
private void Back()
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
}
}
}
+44
View File
@@ -0,0 +1,44 @@
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Главное меню: заголовок и кнопки Новый мир / Загрузка / Настройки / Авторы / Выход.
/// Все подписи — из локализации Core-мода. Чистый UI-экран (Myra), без мира и рендерера.
/// </summary>
public sealed class MainMenuScene : Scene
{
protected override void OnLoad()
{
var content = Context.Services.Get<GameContent>();
var lang = content.Languages;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12);
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
column.Widgets.Add(
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
);
desktop.Root = Ui.Screen(column);
}
private void Go(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(scene, Transitions.Fade(0.4f));
}
}
}
+136
View File
@@ -0,0 +1,136 @@
using System;
using System.Threading.Tasks;
using Friflo.Engine.ECS;
using LittleSim.Content;
using LittleSim.Net;
using LittleSim.Sim;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.Net;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Сетевой клиент: подключается к дедикейтед-серверу (LittleSim.Server --listen) и рендерит
/// реплицированных жителей. Симуляция целиком на сервере; сюда приезжают только компоненты
/// из <see cref="NetSchema"/>, а спрайты вешаются локально при спавне — граница
/// sim/presentation проходит теперь через сеть. Esc — назад в меню, команда консоли `net` —
/// состояние соединения.
/// </summary>
public sealed class MultiplayerScene : Scene
{
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
private readonly Uri _server;
private Task<WebSocketClient>? _connecting;
private WebSocketClient? _connection;
private ReplicationClient _replication = null!;
private InputManager _input = null!;
private GameContent _content = null!;
private Label _hud = null!;
private string _statusKey = "net.connecting";
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
public MultiplayerScene(Uri server) => _server = server;
protected override void OnLoad()
{
_content = Context.Services.Get<GameContent>();
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
_input = this.UseInput();
var renderer = this.UseRenderer2D(
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
);
GameLayers.EnsureRegistered(renderer);
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
Store.CreateEntity(new Camera(Bounds.Center, zoom: 1f, bounds: Bounds));
// Реплика пишет в ECS сцены; презентацию (спрайт) вешаем сами при спавне.
_replication = new ReplicationClient(NetSchema.Create(), Store);
_replication.EntitySpawned += entity =>
{
var sprite = new Sprite(white, GameLayers.Beings);
sprite.CenterOrigin();
entity.AddComponent(sprite);
entity.AddComponent(new NetLerp());
};
var desktop = this.UseUI();
_hud = new Label { Left = 10, Top = 8 };
desktop.Root = Ui.Screen(_hud);
UpdateSystems.Add(new CallbackSystem(Pump));
// Снапшоты приходят ~10 раз в секунду — сглаживаем позиции до частоты кадра.
UpdateSystems.Add(new NetSmoothingSystem());
// Усталость жителей видна и по сети: PawnNeeds реплицируется, спрайт темнеет локально.
UpdateSystems.Add(new PawnAppearanceSystem());
var console = this.UseDevConsole();
console.Register(
"net",
"net — connection status and replicated entity count",
(c, _) =>
{
c.WriteLine($"server: {_server}");
c.WriteLine($"state: {(_connection?.IsOpen == true ? "connected" : "offline")}");
c.WriteLine($"entities: {_replication.EntityCount}");
}
);
_connecting = WebSocketClient.ConnectAsync(_server);
}
protected override void OnUnload() => _connection?.Close();
private void Pump()
{
if (_connecting is { IsCompleted: true } finished)
{
_connecting = null;
if (finished.IsFaulted)
{
_statusKey = "net.failed";
Log.Error(
$"Connect to {_server} failed: "
+ finished.Exception?.GetBaseException().Message
);
}
else
{
_connection = finished.Result;
_statusKey = "net.connected";
Log.Info($"Connected to {_server}");
}
}
if (_connection is not null)
{
_replication.Pump(_connection);
if (!_connection.IsOpen)
{
_statusKey = "net.lost";
}
}
_hud.Text = _content.Languages.Format(
"net.hud",
_content.Languages.Get(_statusKey),
_replication.EntityCount
);
if (_input.IsKeyPressed(Keys.Escape))
{
_connection?.Close();
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
}
}
}
+167
View File
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Окно настройки нового мира: имя, размер (пресеты из <see cref="WorldPresetDef"/> Core-мода),
/// сид (с кнопкой «случайно») и сглаживание рельефа. «Создать» запускает <see cref="WorldScene"/>.
/// </summary>
public sealed class NewWorldScene : Scene
{
private const int MinSmoothing = 1;
private const int MaxSmoothing = 8;
private WorldPresetDef _preset = null!;
private int _smoothing = 4;
private TextBox _name = null!;
private TextBox _seed = null!;
private readonly List<Action> _refreshers = [];
protected override void OnLoad()
{
var content = Context.Services.Get<GameContent>();
var lang = content.Languages;
var input = this.UseInput();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
_preset = presets[0];
var column = Ui.Column(10);
column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
column.Widgets.Add(new Label { Height = 8 });
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
// Размер — сегменты пресетов.
var sizeRow = Ui.Row(6);
sizeRow.Widgets.Add(new Label { Text = lang.Get("newworld.size"), TextColor = Ui.Muted });
foreach (var preset in presets)
{
var value = preset;
var button = new TextButton { Text = lang.Get(preset.Label) };
button.Click += (_, _) =>
{
_preset = value;
Refresh();
};
sizeRow.Widgets.Add(button);
_refreshers.Add(() =>
button.TextColor = ReferenceEquals(_preset, value) ? Ui.Accent : Ui.Muted
);
}
column.Widgets.Add(sizeRow);
// Сид + случайно.
_seed = new TextBox { Text = Random.Shared.Next().ToString(), Width = 200 };
var seedRow = Ui.Row(6);
seedRow.Widgets.Add(new Label { Text = lang.Get("newworld.seed"), TextColor = Ui.Muted });
seedRow.Widgets.Add(_seed);
seedRow.Widgets.Add(
Ui.Button(
lang.Get("newworld.random"),
() => _seed.Text = Random.Shared.Next().ToString(),
width: 120
)
);
column.Widgets.Add(seedRow);
// Сглаживание /+.
var smoothRow = Ui.Row(6);
var smoothLabel = new Label { TextColor = Ui.Muted };
_refreshers.Add(() => smoothLabel.Text = $"{lang.Get("newworld.smoothing")}: {_smoothing}");
var minus = new TextButton { Text = "" };
minus.Click += (_, _) =>
{
_smoothing = Math.Max(MinSmoothing, _smoothing - 1);
Refresh();
};
var plus = new TextButton { Text = "+" };
plus.Click += (_, _) =>
{
_smoothing = Math.Min(MaxSmoothing, _smoothing + 1);
Refresh();
};
smoothRow.Widgets.Add(smoothLabel);
smoothRow.Widgets.Add(minus);
smoothRow.Widgets.Add(plus);
column.Widgets.Add(smoothRow);
column.Widgets.Add(new Label { Height = 8 });
var buttons = Ui.Row(10);
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.create"), Create, width: 150));
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
column.Widgets.Add(buttons);
desktop.Root = Ui.Screen(column);
Refresh();
UpdateSystems.Add(
new CallbackSystem(() =>
{
if (input.IsKeyPressed(Keys.Escape))
{
Back();
}
})
);
}
private static Widget LabeledRow(string label, Widget control)
{
var row = Ui.Row(6);
row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted });
row.Widgets.Add(control);
return row;
}
private void Refresh()
{
foreach (var refresh in _refreshers)
{
refresh();
}
}
private void Create()
{
if (Context.Scenes.IsTransitioning)
{
return;
}
var seed = int.TryParse(_seed.Text, out var parsed) ? parsed : Random.Shared.Next();
var name = string.IsNullOrWhiteSpace(_name.Text) ? "World" : _name.Text.Trim();
var config = new WorldConfig
{
Name = name,
Width = _preset.Width,
Height = _preset.Height,
Population = _preset.Population,
Seed = seed,
SmoothPasses = _smoothing,
};
Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f));
}
private void Back()
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
}
}
}
+168
View File
@@ -0,0 +1,168 @@
using System;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using MrGameEng.Audio;
using MrGameEng.Core;
using MrGameEng.Host;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Внутриигровое меню-пауза: затемняющий оверлей поверх мира (мир остаётся загруженным).
/// Открытие ставит игру на паузу через <see cref="GameSpeed"/>, закрытие — снимает.
/// Кнопки: Продолжить / Настройки (общая <see cref="SettingsPanel"/>) / Сохранить /
/// Главное меню / Выход. <see cref="Root"/> добавляется в корневую панель сцены.
/// </summary>
internal sealed class PauseMenu
{
private readonly EngineContext _context;
private readonly GameContent _content;
private readonly GameSpeed _speed;
private readonly GameHost _host;
private readonly AudioManager _audio;
private readonly Func<string> _onSave;
private readonly Action _onMainMenu;
private readonly Action _onQuit;
private readonly Panel _overlay;
private GameSettings _settings = GameSettingsStore.Load();
private bool _inSettings;
private string? _savedToast;
public PauseMenu(
EngineContext context,
GameContent content,
GameSpeed speed,
Func<string> onSave,
Action onMainMenu,
Action onQuit
)
{
_context = context;
_content = content;
_speed = speed;
_host = (GameHost)context.Services.Get<Game>();
_audio = context.Services.Get<AudioManager>();
_onSave = onSave;
_onMainMenu = onMainMenu;
_onQuit = onQuit;
_overlay = new Panel
{
Visible = false,
Background = new SolidBrush(new Color(0, 0, 0, 200)),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
};
}
/// <summary>Виджет оверлея для добавления в корень сцены.</summary>
public Widget Root => _overlay;
/// <summary>Открыт ли оверлей паузы.</summary>
public bool IsOpen { get; private set; }
/// <summary>
/// Реакция на Esc: закрыть подменю настроек → вернуться к кнопкам; иначе открыть/закрыть
/// меню паузы.
/// </summary>
public void Toggle()
{
if (!IsOpen)
{
Open();
}
else if (_inSettings)
{
_inSettings = false;
ShowButtons();
}
else
{
Close();
}
}
private void Open()
{
IsOpen = true;
_inSettings = false;
_savedToast = null;
_speed.Pause();
ShowButtons();
_overlay.Visible = true;
}
private void Close()
{
IsOpen = false;
_inSettings = false;
_overlay.Visible = false;
_speed.Resume();
}
private void ShowButtons()
{
var lang = _content.Languages;
var column = Ui.Column(10);
column.Widgets.Add(Ui.Title(lang.Get("pause.title")));
column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add(Ui.Button(lang.Get("pause.resume"), Close));
column.Widgets.Add(
Ui.Button(
lang.Get("pause.settings"),
() =>
{
_inSettings = true;
ShowSettings();
}
)
);
column.Widgets.Add(Ui.Button(lang.Get("pause.save"), Save));
column.Widgets.Add(Ui.Button(lang.Get("pause.mainmenu"), _onMainMenu));
column.Widgets.Add(Ui.Button(lang.Get("pause.quit"), _onQuit));
if (_savedToast is not null)
{
column.Widgets.Add(
new Label { Text = lang.Format("pause.saved", _savedToast), TextColor = Ui.Muted }
);
}
SetContent(column);
}
private void ShowSettings() =>
SetContent(
SettingsPanel.Build(
_settings,
_content.Languages,
onApply: () =>
{
GameSettingsStore.Save(_settings);
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
ShowSettings(); // язык мог смениться
},
onBack: () =>
{
_inSettings = false;
ShowButtons();
}
)
);
private void Save()
{
_savedToast = _onSave();
ShowButtons();
}
private void SetContent(Widget content)
{
_overlay.Widgets.Clear();
_overlay.Widgets.Add(content);
}
}
+66
View File
@@ -0,0 +1,66 @@
using LittleSim.App;
using LittleSim.Content;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Audio;
using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Экран настроек игры (язык, экран, громкость, разрешение). Использует общий
/// <see cref="SettingsPanel"/>. «Применить» сохраняет в settings.json и применяет к движку
/// (смена языка перестраивает подписи на лету); «Назад»/Esc — в главное меню.
/// </summary>
public sealed class SettingsScene : Scene
{
private GameContent _content = null!;
private AudioManager _audio = null!;
private GameHost _host = null!;
private Desktop _desktop = null!;
private GameSettings _settings = GameSettingsStore.Load();
protected override void OnLoad()
{
_content = Context.Services.Get<GameContent>();
_audio = Context.Services.Get<AudioManager>();
_host = (GameHost)Context.Services.Get<Game>();
var input = this.UseInput();
_desktop = this.UseUI();
Rebuild();
UpdateSystems.Add(
new CallbackSystem(() =>
{
if (input.IsKeyPressed(Keys.Escape))
{
Back();
}
})
);
}
private void Rebuild() =>
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back));
private void Apply()
{
GameSettingsStore.Save(_settings);
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
Rebuild(); // язык мог смениться — перестраиваем подписи
}
private void Back()
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
}
}
}
-177
View File
@@ -1,177 +0,0 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
using LittleSim.Sim;
using Microsoft.Xna.Framework;
using MrGameEng.Assets;
using MrGameEng.Collisions;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Graphics;
using MrGameEng.Input;
using MrGameEng.Tilemaps;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Демо-сцена тайлового террейна: карта-Tilemap по дефам рельефа (поверхность из атласа
/// или тонированный тайл), растительность по скаттеру дефов (деревья — с коллайдером
/// ствола), животные — дефы вида "animal" с расталкиванием. Команды: "regen", "world".
/// </summary>
public sealed class TerrainScene : Scene
{
private const int TerrainWidth = 44;
private const int TerrainHeight = 28;
private const int CellSize = 24;
private const int Population = 14;
private static readonly RectF TerrainBounds = new(
0f,
0f,
TerrainWidth * CellSize,
TerrainHeight * CellSize
);
private const uint AnimalLayer = 0b01;
private const uint ObstacleLayer = 0b10;
private readonly int _seed;
public TerrainScene(int seed) => _seed = seed;
protected override void OnLoad()
{
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
var content = Context.Services.Get<GameContent>();
var atlases = Context.Services.Get<ModAtlases>();
var device = Context.GraphicsDevice;
var input = this.UseInput();
var renderer = this.UseRenderer2D(
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
);
GameLayers.EnsureRegistered(renderer);
this.UseTilemaps();
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
// Тайлсет из дефов: у дефа есть поверхность — текстура из атласа, нет — тонированный тайл.
var tiles = new TileSet();
var tileByDef = new Dictionary<TerrainDef, ushort>();
foreach (var terrain in content.Terrains.All)
{
tileByDef[terrain] = terrain.Surface is { } surface
? tiles.Add(atlases.GetRegion(device, surface))
: tiles.Add(white, terrain.Tint);
}
// Террейн строится кодом: высоты по сиду → деф клетки → id тайла; поверх — скаттер.
var random = new Random(_seed);
var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed);
var grid = new TileGrid(TerrainWidth, TerrainHeight);
var landCells = new List<Point>();
for (var x = 0; x < TerrainWidth; x++)
{
for (var y = 0; y < TerrainHeight; y++)
{
var terrain = content.Terrains.Classify(heights[x, y]);
grid[x, y] = tileByDef[terrain];
var cell = new Point(x, y);
if (terrain.IsLand)
{
landCells.Add(cell);
}
ScatterSpawner.Spawn(
this,
content,
atlases,
device,
terrain,
cell,
CellSize,
random,
obstacleLayer: ObstacleLayer,
collidesWith: AnimalLayer
);
}
}
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
// Животные: дефы вида "animal", не наслаиваются (коллайдеры + расталкивание).
var animals = content.Defs.All<PawnDef>().Where(p => p.Kind == PawnDef.KindAnimal).ToList();
for (var i = 0; i < Population && landCells.Count > 0 && animals.Count > 0; i++)
{
var cell = landCells[random.Next(landCells.Count)];
var animal = animals[random.Next(animals.Count)];
var body = atlases.GetRegion(device, animal.Texture);
var sprite = new Sprite(body, GameLayers.Beings);
sprite.CenterOrigin();
var collider = Collider.Circle(animal.SizeCells * CellSize * 0.32f);
collider.Layer = AnimalLayer;
collider.CollidesWith = AnimalLayer | ObstacleLayer;
Store.CreateEntity(
new Transform2D(
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
scale: new Vector2(CellSize * animal.SizeCells / body.Width)
),
sprite,
new Wander(),
collider
);
}
var camera = Store.CreateEntity(
new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds)
);
var desktop = this.UseUI();
var hudLabel = new Label { Left = 10, Top = 8 };
desktop.Root = hudLabel;
var console = this.UseDevConsole();
ContentCommands.Register(console, content, atlases);
console.Register(
"regen",
"regen [seed] — regenerate the terrain",
(c, args) =>
{
if (Context.Scenes.IsTransitioning)
{
return;
}
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
c.WriteLine($"regenerating terrain, seed {seed}");
Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
}
);
console.Register(
"world",
"world [seed] — switch to the world scene",
(c, args) =>
{
if (Context.Scenes.IsTransitioning)
{
return;
}
var seed = args.Length > 0 ? int.Parse(args[0]) : _seed;
Context.Scenes.Switch(new WorldScene(seed), Transition.Fade(0.6f));
}
);
UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds));
var collisions = this.UseCollisions(cellSize: CellSize * 2f); // после движения
UpdateSystems.Add(new SeparationSystem(collisions));
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
UpdateSystems.Add(
new HudSystem(Context, content.Languages, hudLabel, "hud.terrain", _seed, Population)
);
Log.Info(
$"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells, {Population} animals"
);
}
}
+539 -120
View File
@@ -1,124 +1,464 @@
using System;
using System.Collections.Generic;
using Friflo.Engine.ECS; using Friflo.Engine.ECS;
using LittleSim.App;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Sim; using LittleSim.Sim;
using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets; using MrGameEng.Assets;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.DevConsole; using MrGameEng.DevConsole;
using MrGameEng.Formulas;
using MrGameEng.Genetics;
using MrGameEng.Graphics; using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input; using MrGameEng.Input;
using MrGameEng.Inspector;
using MrGameEng.Lighting;
using MrGameEng.Tilemaps;
using MrGameEng.UI; using MrGameEng.UI;
using Myra.Graphics2D;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
namespace LittleSim.Scenes; namespace LittleSim.Scenes;
/// <summary> /// <summary>
/// Мир LittleSim: рельеф, растения и жители целиком описаны дефами Core-мода /// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
/// (Mods/Core/Defs), текстуры — атласами из дерева текстур модов, строки HUD — /// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
/// локализацией. Камера бога, HUD и консоль ("regen", "terrain", "mods", "lang", …). /// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
/// </summary> /// </summary>
public sealed class WorldScene : Scene public sealed class WorldScene : Scene
{ {
public const int WorldWidth = 120;
public const int WorldHeight = 70;
public const int CellSize = 16; public const int CellSize = 16;
private const int Population = 80;
private static readonly RectF WorldBounds = new( /// <summary>
0f, /// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые
0f, /// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с.
WorldWidth * CellSize, /// </summary>
WorldHeight * CellSize public const float SecondsPerDay = 480f;
);
private readonly int _seed; /// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
public const int DensityCap = 4;
public WorldScene(int seed) => _seed = seed; private readonly WorldConfig _config;
private readonly WorldSave? _save;
private readonly RectF _bounds;
private GameSpeed _speed = null!;
private PauseMenu _pause = null!;
private readonly List<Action> _speedRefreshers = [];
private PlantSet _plants = null!;
private float[] _cellFertility = [];
private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
/// <summary>Новый мир из конфига.</summary>
public WorldScene(WorldConfig config)
: this(config, null) { }
/// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
public WorldScene(WorldConfig config, WorldSave? save)
{
_config = config;
_save = save;
_bounds = new RectF(0f, 0f, config.Width * CellSize, config.Height * CellSize);
}
protected override void OnLoad() protected override void OnLoad()
{ {
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets(); var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
var content = Context.Services.Get<GameContent>(); var content = Context.Services.Get<GameContent>();
var atlases = Context.Services.Get<ModAtlases>(); var atlases = Context.Services.Get<ModAtlases>();
var device = Context.GraphicsDevice; var device = Context.GetGraphicsDevice();
var input = this.UseInput(); var input = this.UseInput();
_speed = Context.Services.Get<GameSpeed>();
_speed.SetStep(0); // новый мир/загрузка стартуют на x1
var renderer = this.UseRenderer2D( var renderer = this.UseRenderer2D(
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) } new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
); );
GameLayers.EnsureRegistered(renderer); GameLayers.EnsureRegistered(renderer);
this.UseTilemaps();
var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
var dayNight = new DayNight(calendar, DayNightSettings.Default);
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
_plants = new PlantSet(content, atlases, device);
var loadingPlants = _save?.Plants is { Count: > 0 };
BuildTerrain(
content,
atlases,
device,
assets,
loadingPlants ? null : new Random(_config.Seed)
);
if (loadingPlants)
{
RestorePlants(content);
}
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
var lighting = this.UseLighting(
renderer,
dayNight,
_config.Width,
_config.Height,
CellSize,
Vector2.Zero,
BuildOccluders
);
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
// HUD, полоса скорости и меню-пауза в одной корневой панели.
var desktop = this.UseUI();
var hudLabel = new Label { Left = 10, Top = 8 };
var speedBar = BuildSpeedBar(content);
_pause = new PauseMenu(
Context,
content,
_speed,
onSave: SaveWorld,
onMainMenu: () => Switch(new MainMenuScene()),
onQuit: () => Context.Services.Get<Game>().Exit()
);
desktop.Root = Ui.Screen(hudLabel, speedBar, _pause.Root);
this.UseInspector(renderer);
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
console.Register(
"light",
"light [radius] — place a point light at the cursor (night shadows demo)",
(c, args) =>
{
var radius =
args.Length > 0
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
: 120f;
var mouse = Mouse.GetState();
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
Store.CreateEntity(
Transform2D.At(world),
new PointLight
{
Radius = radius,
Color = Color.White,
Intensity = 0.9f,
}
);
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
}
);
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
UpdateSystems.Add(
new PlantLifecycleSystem(
Store,
_plants,
calendar,
Context.Clock,
_config.Width,
_config.Height,
CellSize,
_cellFertility,
_cellLand,
DensityCap,
_config.Seed
)
);
UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
);
UpdateSystems.Add(
new HudSystem(
Context,
content.Languages,
hudLabel,
"hud.world",
_config.Seed,
calendar,
climate
)
);
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
Log.Info(
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
+ (_save is null ? " (new)" : " (loaded)")
);
}
/// <summary>
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
/// </summary>
private void BuildTerrain(
GameContent content,
ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
AssetManager assets,
Random? scatterRandom
)
{
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
// Рельеф: одна текстура на все клетки → один draw call на слой; // Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
// тип клетки и растительность приходят из TerrainDef'ов. var tiles = new TileSet();
var random = new Random(_seed); var tileByDef = new Dictionary<TerrainDef, ushort>();
var heights = WorldGenerator.GenerateHeights(WorldWidth, WorldHeight, _seed); foreach (var terrain in content.Terrains.All)
var landCells = new List<Point>();
for (var x = 0; x < WorldWidth; x++)
{ {
for (var y = 0; y < WorldHeight; y++) tileByDef[terrain] = terrain.Surface is { } surface
? tiles.Add(atlases.GetRegion(device, surface))
: tiles.Add(white, terrain.Tint);
}
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
var heights = WorldGenerator.Generate(
_config.Width,
_config.Height,
_config.Seed,
_config.SmoothPasses
);
var grid = new TileGrid(_config.Width, _config.Height);
var cells = _config.Width * _config.Height;
_cellFertility = new float[cells];
_cellLand = new bool[cells];
_cellOccluderBase = new bool[cells];
_cellOccluder = new bool[cells];
for (var x = 0; x < _config.Width; x++)
{
for (var y = 0; y < _config.Height; y++)
{ {
var terrain = content.Terrains.Classify(heights[x, y]); var terrain = content.Terrains.Classify(heights[x, y]);
Store.CreateEntity( grid[x, y] = tileByDef[terrain];
Transform2D.At(new Vector2(x * CellSize, y * CellSize)), var cell = y * _config.Width + x;
new Sprite(white) { Color = terrain.Tint } _cellFertility[cell] = terrain.Fertility;
); _cellLand[cell] = terrain.IsLand;
_cellOccluderBase[cell] = terrain.BlocksLight;
var cell = new Point(x, y); if (scatterRandom is not null)
if (terrain.IsLand)
{ {
landCells.Add(cell); ScatterSpawner.Spawn(
this,
content,
_plants,
terrain,
new Point(x, y),
CellSize,
scatterRandom
);
} }
ScatterSpawner.Spawn(
this,
content,
atlases,
device,
terrain,
cell,
CellSize,
random
);
} }
} }
// Жители: дефы вида "being", появляются на суше, бродят (Y-sort: кто ниже — ближе). Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
var beings = content.Defs.All<PawnDef>().Where(p => p.Kind == PawnDef.KindBeing).ToList(); }
for (var i = 0; i < Population && landCells.Count > 0 && beings.Count > 0; i++)
private HorizontalStackPanel BuildSpeedBar(GameContent content)
{
var bar = Ui.Row(6);
bar.HorizontalAlignment = HorizontalAlignment.Center;
bar.VerticalAlignment = VerticalAlignment.Bottom;
bar.Margin = new Thickness(0, 0, 0, 12);
var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
pause.Click += (_, _) => _speed.Pause();
bar.Widgets.Add(pause);
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
for (var i = 0; i < _speed.Steps.Count; i++)
{ {
var cell = landCells[random.Next(landCells.Count)]; var index = i;
var being = beings[random.Next(beings.Count)]; var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
var body = atlases.GetRegion(device, being.Texture); button.Click += (_, _) => _speed.SetStep(index);
var sprite = new Sprite(body, GameLayers.Beings); bar.Widgets.Add(button);
sprite.CenterOrigin(); _speedRefreshers.Add(() =>
Store.CreateEntity( button.TextColor =
new Transform2D( !_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
scale: new Vector2(CellSize * being.SizeCells / body.Width)
),
sprite,
new Wander(),
// Начальная энергия и фаза решения разбросаны сидом — жители не отдыхают синхронно.
new PawnNeeds { Energy = 0.5f + random.NextSingle() * 0.5f },
new PawnBrain { Action = PawnAction.Wander, DecideIn = random.NextSingle() * 0.75f }
); );
} }
var camera = Store.CreateEntity( void Refresh()
new Camera(WorldBounds.Center, zoom: 1f, bounds: WorldBounds) {
); foreach (var refresh in _speedRefreshers)
{
refresh();
}
}
// HUD и консоль. _speed.Changed += Refresh;
var desktop = this.UseUI(); RegisterUnload(() => _speed.Changed -= Refresh);
var hudLabel = new Label { Left = 10, Top = 8 }; Refresh();
desktop.Root = hudLabel; return bar;
}
var console = this.UseDevConsole(); private void Hotkeys(InputManager input)
{
if (input.IsKeyPressed(Keys.Escape))
{
_pause.Toggle();
}
if (_pause.IsOpen)
{
return;
}
if (input.IsKeyPressed(Keys.Space))
{
_speed.TogglePause();
}
if (input.IsKeyPressed(Keys.D1))
{
_speed.SetStep(0);
}
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
{
_speed.SetStep(1);
}
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
{
_speed.SetStep(2);
}
}
private string SaveWorld()
{
var save = new WorldSave
{
Name = _config.Name,
Width = _config.Width,
Height = _config.Height,
Seed = _config.Seed,
SmoothPasses = _config.SmoothPasses,
Population = _config.Population,
SavedUtc = DateTime.UtcNow,
ElapsedSeconds = Context.Clock.TotalTime,
};
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
Store
.Query<PlantGrowth, PlantOrganism, Transform2D>()
.ForEachEntity(
(
ref PlantGrowth grow,
ref PlantOrganism org,
ref Transform2D transform,
Entity _
) =>
{
save.Plants.Add(
new PlantSave
{
Species = _plants[grow.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
AgeDays = grow.AgeDays,
Stage = grow.Stage,
CellFertility = grow.CellFertility,
Genome = org.Genome.ToDictionary(),
}
);
}
);
new SaveStore().Write(save);
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
return _config.Name;
}
private void RestorePlants(GameContent content)
{
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
foreach (var plant in _save!.Plants)
{
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
{
continue; // вид пропал (мод убрали) — пропускаем растение
}
var index = _plants.IndexOf(def);
var genome = plant.Genome is { Count: > 0 }
? new Genome(plant.Genome)
: _plants[index].Template.Generate(fallback);
PlantFactory.Create(
Store,
_plants,
index,
new Vector2(plant.X, plant.Y),
plant.AgeDays,
genome,
plant.CellFertility,
CellSize
);
}
}
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
private bool[] BuildOccluders()
{
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
Store
.Query<PlantGrowth, Transform2D>()
.ForEachEntity(
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
{
var species = _plants[grow.Species];
if (
species.Def.TrunkRadiusCells <= 0f
|| grow.Stage < species.Stages.Length - 1
)
{
return; // затеняют только зрелые деревья (со стволом)
}
var cx = Math.Clamp(
(int)(transform.Position.X / CellSize),
0,
_config.Width - 1
);
var cy = Math.Clamp(
(int)(transform.Position.Y / CellSize),
0,
_config.Height - 1
);
_cellOccluder[cy * _config.Width + cx] = true;
}
);
return _cellOccluder;
}
private void Switch(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
{
_speed.Resume();
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
}
}
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
{
ContentCommands.Register(console, content, atlases); ContentCommands.Register(console, content, atlases);
console.Register( console.Register(
"regen", "regen",
"regen [seed] — regenerate the world", "regen [seed] — regenerate the world with a new seed",
(c, args) => (c, args) =>
{ {
if (Context.Scenes.IsTransitioning) if (Context.Scenes.IsTransitioning)
@@ -128,71 +468,150 @@ public sealed class WorldScene : Scene
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next(); var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
c.WriteLine($"regenerating world, seed {seed}"); c.WriteLine($"regenerating world, seed {seed}");
Context.Scenes.Switch(new WorldScene(seed), Transition.Fade(0.6f)); Context.Scenes.Switch(
new WorldScene(_config with { Seed = seed }),
Transitions.Fade(0.6f)
);
} }
); );
console.Register( console.Register(
"terrain", "formula",
"terrain [seed] — switch to the tile terrain scene", "formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
(c, args) => (c, args) =>
{ {
if (Context.Scenes.IsTransitioning) if (args.Length == 0)
{ {
c.WriteLine(
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
);
return; return;
} }
var seed = args.Length > 0 ? int.Parse(args[0]) : _seed; var expression = string.Join(' ', args);
c.WriteLine($"switching to terrain scene, seed {seed}"); try
Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
}
);
var decisions = new PawnDecisionSystem();
UpdateSystems.Add(decisions);
UpdateSystems.Add(new WanderSystem(_seed, WorldBounds));
UpdateSystems.Add(new PawnNeedsSystem());
UpdateSystems.Add(new PawnAppearanceSystem());
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
UpdateSystems.Add(
new HudSystem(Context, content.Languages, hudLabel, "hud.world", _seed, Population)
);
console.Register(
"ai",
"ai [energy] — utility scores at the given energy (0..1), plus the rest/wander split",
(c, args) =>
{
var energy =
args.Length > 0 && float.TryParse(args[0], out var e)
? Math.Clamp(e, 0f, 1f)
: 0.3f;
c.WriteLine($"utility scores at energy {energy:0.00}:");
foreach (var (name, score) in decisions.Inspect(energy))
{ {
c.WriteLine($" {name, -8} {score:0.000}"); c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
}
catch (FormulaException e)
{
c.WriteLine($"error: {e.Message}");
} }
var resting = 0;
var total = 0;
Store
.Query<PawnBrain>()
.ForEachEntity(
(ref PawnBrain brain, Entity _) =>
{
total++;
if (brain.Action == PawnAction.Rest)
{
resting++;
}
}
);
c.WriteLine($"population: {resting} resting / {total - resting} wandering");
} }
); );
console.Register(
Log.Info( "gene",
$"World generated: seed {_seed}, {WorldWidth}x{WorldHeight} cells, population {Population}" "gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
(c, args) => RunGeneDemo(c, content, args)
);
console.Register(
"plant",
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
(c, args) => RunPlantDemo(c, content, args)
);
console.Register(
"menu",
"menu — return to the main menu",
(_, _) => Switch(new MainMenuScene())
); );
} }
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
{
var genes = content.Defs.All<GeneDef>();
if (genes.Count == 0)
{
console.WriteLine("no Gene defs loaded");
return;
}
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
var random = new Random(seed);
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
var parentA = Genome.Generate(genes, random);
foreach (var gene in genes)
{
var allele = parentA[gene.DefName];
console.WriteLine(
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
);
}
console.WriteLine("traits:");
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
{
console.WriteLine($" {trait} = {value:0.###}");
}
var parentB = Genome.Generate(genes, random);
var child = Genome.Breed(parentA, parentB, registry, random);
var childTraits = Phenotype.Compute(child, registry);
console.WriteLine(
$"bred child: {child.Alleles.Count} genes, "
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
);
}
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Plant");
if (args.Length == 0)
{
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
{
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var index = _plants.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
var genome = _plants[index].Template.Generate(new Random(seed));
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
console.WriteLine(
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
);
console.WriteLine(
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
);
if (def.HarvestProduct is { } harvest)
{
console.WriteLine(
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
);
}
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
{
var season = content.Languages.Get(
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
);
console.WriteLine(
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
);
}
else
{
console.WriteLine(" barren (no fruit)");
}
}
private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
: productDefName;
} }
+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));
}
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
using Friflo.Engine.ECS;
namespace LittleSim.Sim;
/// <summary>
/// Состояние роста растения: индекс вида в <see cref="Content.PlantSet"/>, текущая стадия и
/// накопленный возраст в игровых днях. Плоские данные — переход между стадиями и смену спрайта
/// выполняет <see cref="PlantGrowthSystem"/>, читая пороги стадий из таблицы вида.
/// </summary>
public struct PlantGrowth : IComponent
{
/// <summary>Индекс вида растения в <see cref="Content.PlantSet"/>.</summary>
public int Species;
/// <summary>Текущая стадия роста (индекс в стадиях вида).</summary>
public int Stage;
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays;
/// <summary>Плодородность клетки, на которой растёт растение — вход пригодности(плодородность).</summary>
public float CellFertility;
}
+95
View File
@@ -0,0 +1,95 @@
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Lighting;
namespace LittleSim.Sim;
/// <summary>
/// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов
/// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) ·
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из
/// признаков (<see cref="PlantPhenotype"/>), вычисленных формулами генов. Свет локальный (лайтмап),
/// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному
/// возрасту как прежде. Терминальную стадию пропускает.
/// </summary>
public sealed class PlantGrowthSystem(
PlantSet plants,
Calendar calendar,
Climate climate,
Lighting lighting,
int cellSize
) : QuerySystem<PlantGrowth, PlantOrganism, Sprite, Transform2D>
{
protected override void OnUpdate()
{
var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
if (deltaDays <= 0f)
{
return; // пауза — рост стоит
}
// Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее).
var temperature = climate.Temperature;
foreach (var (growths, organisms, sprites, transforms, _) in Query.Chunks)
{
var g = growths.Span;
var orgs = organisms.Span;
var s = sprites.Span;
var t = transforms.Span;
for (var i = 0; i < g.Length; i++)
{
ref var grow = ref g[i];
var stages = plants[grow.Species].Stages;
if (grow.Stage >= stages.Length - 1)
{
grow.AgeDays += deltaDays; // зрелое: старение в реальном времени → смерть по старости
continue;
}
ref var traits = ref orgs[i].Traits;
var light = lighting.SampleAt(t[i].Position);
var rate =
traits.Vigor
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Gaussian(
temperature,
traits.OptimalTemperature,
traits.TemperatureTolerance
)
* Suitability.Gaussian(
grow.CellFertility,
traits.OptimalFertility,
traits.FertilityTolerance
);
grow.AgeDays += deltaDays * rate;
var changed = false;
while (
grow.Stage < stages.Length - 1
&& grow.AgeDays >= stages[grow.Stage + 1].EnterDay
)
{
grow.Stage++;
changed = true;
}
if (changed)
{
Apply(ref s[i], ref t[i], stages[grow.Stage]);
}
}
}
}
private void Apply(ref Sprite sprite, ref Transform2D transform, in PlantSet.Stage stage)
{
sprite.Region = stage.Region;
sprite.Origin = new Vector2(stage.Region.Width / 2f, stage.Region.Height / 2f);
transform.Scale = new Vector2(cellSize * stage.SizeCells / stage.Region.Width);
}
}
+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;
}
+45 -10
View File
@@ -14,6 +14,12 @@ public struct Wander : IComponent
public float ChangeIn; public float ChangeIn;
} }
/// <summary>Имя дефа жителя — нужно сохранению, чтобы при загрузке пересобрать спрайт из атласа.</summary>
public struct PawnId : IComponent
{
public string DefName;
}
/// <summary> /// <summary>
/// Блуждание жителей. Один Random на систему, сид от мира — при одинаковом сиде /// Блуждание жителей. Один Random на систему, сид от мира — при одинаковом сиде
/// и порядке обновления симуляция воспроизводима. Отдыхающие жители (см. /// и порядке обновления симуляция воспроизводима. Отдыхающие жители (см.
@@ -110,16 +116,22 @@ public sealed class SeparationSystem(MrGameEng.Collisions.CollisionWorld world)
} }
} }
/// <summary>Камера бога: WASD/стрелки — панорамирование, колесо — зум. Молчит, пока открыта консоль.</summary> /// <summary>
/// Камера бога: WASD/стрелки — панорамирование, колесо — зум к курсору (мировая точка под мышью
/// остаётся под мышью). Молчит, пока открыта консоль или активен <paramref name="blocked"/>
/// (например, открыто меню-пауза).
/// </summary>
public sealed class GodCameraSystem( public sealed class GodCameraSystem(
Entity cameraEntity, Entity cameraEntity,
InputManager input, InputManager input,
MrGameEng.DevConsole.DevConsole console Renderer2D renderer,
MrGameEng.DevConsole.DevConsole console,
Func<bool>? blocked = null
) : BaseSystem ) : BaseSystem
{ {
protected override void OnUpdateGroup() protected override void OnUpdateGroup()
{ {
if (console.IsOpen) if (console.IsOpen || blocked?.Invoke() == true)
{ {
return; return;
} }
@@ -136,7 +148,20 @@ public sealed class GodCameraSystem(
camera.Position += pan * (500f / camera.Zoom) * Tick.deltaTime; camera.Position += pan * (500f / camera.Zoom) * Tick.deltaTime;
} }
camera.Zoom = Math.Clamp(camera.Zoom * (1f + input.WheelDelta * 0.001f), 0.4f, 8f); if (input.WheelDelta != 0)
{
var oldZoom = camera.Zoom;
var newZoom = Math.Clamp(oldZoom * (1f + input.WheelDelta * 0.001f), 0.4f, 8f);
if (newZoom != oldZoom)
{
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
var anchor = renderer.ScreenToWorld(
new Vector2(input.MousePosition.X, input.MousePosition.Y)
);
camera.Zoom = newZoom;
camera.Position = anchor - (anchor - camera.Position) * (oldZoom / newZoom);
}
}
} }
private static float Axis( private static float Axis(
@@ -151,8 +176,8 @@ public sealed class GodCameraSystem(
} }
/// <summary> /// <summary>
/// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, населением и FPS) /// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, датой/временем календаря и
/// и подсказка управления. Обновляется 4 раза в секунду; смена языка подхватывается сама. /// FPS) и подсказка управления. Обновляется 4 раза в секунду; смена языка подхватывается сама.
/// </summary> /// </summary>
public sealed class HudSystem( public sealed class HudSystem(
MrGameEng.Core.EngineContext context, MrGameEng.Core.EngineContext context,
@@ -160,7 +185,8 @@ public sealed class HudSystem(
Myra.Graphics2D.UI.Label label, Myra.Graphics2D.UI.Label label,
string titleKey, string titleKey,
int seed, int seed,
int population MrGameEng.Core.Calendar? calendar = null,
MrGameEng.Core.Climate? climate = null
) : BaseSystem ) : BaseSystem
{ {
private float _accumulated; private float _accumulated;
@@ -178,9 +204,18 @@ public sealed class HudSystem(
var fps = (int)MathF.Round(_frames / _accumulated); var fps = (int)MathF.Round(_frames / _accumulated);
_accumulated = 0f; _accumulated = 0f;
_frames = 0; _frames = 0;
var when = calendar is null
? ""
: languages.Format("hud.datetime", calendar.Day, calendar.Hour, calendar.Minute);
if (climate is not null)
{
var season = languages.Get("season." + climate.Season.ToString().ToLowerInvariant());
when +=
" | "
+ languages.Format("hud.climate", season, (int)MathF.Round(climate.Temperature));
}
label.Text = label.Text =
languages.Format(titleKey, seed, population, fps) languages.Format(titleKey, seed, when, fps) + "\n" + languages.Get("hud.controls");
+ "\n"
+ languages.Get("hud.controls");
} }
} }
+23 -66
View File
@@ -1,76 +1,33 @@
using MrGameEng.WorldGen;
namespace LittleSim.Sim; namespace LittleSim.Sim;
/// <summary> /// <summary>
/// Детерминированная генерация рельефа: случайное поле, сглаженное несколькими проходами. /// Игровой адаптер процедурной генерации: превращает параметры мира в настройки движкового
/// Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой симуляции. /// <see cref="HeightmapGenerator"/> (шум Перлина → fBm → island-маска) и отдаёт нормализованную
/// Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>). /// карту высот. Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой
/// симуляции. Классификация высот в типы клеток живёт в дефах (<see cref="Content.TerrainSet"/>).
/// </summary> /// </summary>
public static class WorldGenerator public static class WorldGenerator
{ {
public static float[,] GenerateHeights(int width, int height, int seed) /// <summary>
/// Строит нормализованную карту высот <paramref name="width"/>×<paramref name="height"/> из
/// <paramref name="seed"/>. <paramref name="detail"/> (1..8, из меню «масштаб деталей»/сейва)
/// задаёт размер «континентов»: больше — крупнее и плавнее суша (ниже частота шума).
/// </summary>
public static Heightmap Generate(int width, int height, int seed, int detail = 4)
{ {
var random = new Random(seed); var scale = Math.Clamp(detail, 1, 8);
var heights = new float[width, height]; var settings = new HeightmapSettings
for (var x = 0; x < width; x++)
{ {
for (var y = 0; y < height; y++) Seed = seed,
{ Frequency = 6f / scale, // больше «масштаба деталей» → ниже частота → крупнее континенты
heights[x, y] = random.NextSingle(); Octaves = 5,
} Persistence = 0.5f,
} Lacunarity = 2f,
IslandStrength = 0.55f, // края мира уходят под воду — мир выглядит как континент
// Сглаживание усреднением окрестности 3×3 — дёшево и даёт связные "континенты". IslandPower = 3f,
for (var pass = 0; pass < 4; pass++) };
{ return new HeightmapGenerator(settings).Generate(width, height);
var smoothed = new float[width, height];
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
var sum = 0f;
var count = 0;
for (var dx = -1; dx <= 1; dx++)
{
for (var dy = -1; dy <= 1; dy++)
{
var nx = x + dx;
var ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
{
sum += heights[nx, ny];
count++;
}
}
}
smoothed[x, y] = sum / count;
}
}
heights = smoothed;
}
Normalize(heights, width, height);
return heights;
}
private static void Normalize(float[,] heights, int width, int height)
{
var min = float.MaxValue;
var max = float.MinValue;
foreach (var value in heights)
{
min = Math.Min(min, value);
max = Math.Max(max, value);
}
var range = Math.Max(0.0001f, max - min);
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
heights[x, y] = (heights[x, y] - min) / range;
}
}
} }
} }
+86
View File
@@ -0,0 +1,86 @@
using System;
using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework;
using Myra.Graphics2D.UI;
namespace LittleSim.UI;
/// <summary>
/// Общие фабрики виджетов Myra для экранов меню — единый стиль и отсутствие дублирования
/// разметки между сценами. Все подписи приходят готовыми строками (локализованными вызывающим).
/// </summary>
internal static class Ui
{
/// <summary>Акцентный цвет заголовков и активных элементов.</summary>
public static readonly Color Accent = new(120, 200, 255);
/// <summary>Приглушённый цвет вторичного текста.</summary>
public static readonly Color Muted = new(150, 160, 175);
private const int ButtonWidth = 280;
/// <summary>Крупный заголовок по центру.</summary>
public static Label Title(string text) =>
new()
{
Text = text,
TextColor = Accent,
HorizontalAlignment = HorizontalAlignment.Center,
};
/// <summary>Подпись вторичным цветом по центру.</summary>
public static Label Subtitle(string text) =>
new()
{
Text = text,
TextColor = Muted,
HorizontalAlignment = HorizontalAlignment.Center,
};
/// <summary>Кнопка фиксированной ширины с обработчиком клика.</summary>
public static TextButton Button(string text, Action onClick, int width = ButtonWidth)
{
var button = new TextButton
{
Text = text,
Width = width,
HorizontalAlignment = HorizontalAlignment.Center,
};
button.Click += (_, _) => onClick();
return button;
}
/// <summary>Вертикальная колонка по центру экрана.</summary>
public static VerticalStackPanel Column(int spacing = 10) =>
new()
{
Spacing = spacing,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
/// <summary>Горизонтальный ряд.</summary>
public static HorizontalStackPanel Row(int spacing = 8) =>
new() { Spacing = spacing, HorizontalAlignment = HorizontalAlignment.Center };
/// <summary>Оборачивает содержимое в панель на весь экран (абсолютное позиционирование детей).</summary>
public static Panel Screen(params Widget[] children)
{
var panel = new Panel();
foreach (var child in children)
{
panel.Widgets.Add(child);
}
return panel;
}
}
/// <summary>
/// Минимальная система, вызывающая колбэк каждый кадр обновления. Сцены меню используют её
/// для опроса ввода (Esc/горячие клавиши) без отдельного класса-системы на каждую сцену.
/// </summary>
internal sealed class CallbackSystem(Action update) : BaseSystem
{
protected override void OnUpdateGroup() => update();
}
+171
View File
@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using LittleSim.App;
using MrGameEng.Mods;
using Myra.Graphics2D.UI;
namespace LittleSim.UI;
/// <summary>
/// Конструктор панели настроек (язык, полный экран, VSync, разрешение, громкость).
/// Возвращает виджет Myra, работающий с переданной копией <see cref="GameSettings"/>; общий
/// для сцены настроек и оверлея паузы. Вместо неоднозначных комбо/чекбоксов — кнопки-сегменты
/// и тумблеры (состояние показывает цвет текста), что переносимо между версиями Myra.
/// </summary>
internal static class SettingsPanel
{
private static readonly (int W, int H)[] Resolutions =
{
(1280, 720),
(1600, 900),
(1920, 1080),
};
/// <summary>Строит панель. <paramref name="onApply"/>/<paramref name="onBack"/> — кнопки внизу.</summary>
public static Widget Build(
GameSettings settings,
LanguageManager lang,
Action onApply,
Action onBack
)
{
var refreshers = new List<Action>();
var column = Ui.Column(10);
column.Widgets.Add(Ui.Title(lang.Get("settings.title")));
column.Widgets.Add(new Label { Height = 8 });
// Язык — по одному сегменту на доступный язык.
column.Widgets.Add(
Segment(
lang.Get("settings.language"),
lang.AvailableLanguages,
code => code.ToUpperInvariant(),
() => settings.Language,
code => settings.Language = code,
refreshers
)
);
// Полный экран и VSync — тумблеры (цвет = состояние).
column.Widgets.Add(
Toggle(
lang.Get("settings.fullscreen"),
() => settings.Fullscreen,
v => settings.Fullscreen = v,
refreshers
)
);
column.Widgets.Add(
Toggle(
lang.Get("settings.vsync"),
() => settings.VSync,
v => settings.VSync = v,
refreshers
)
);
// Разрешение — сегменты пресетов.
var resolutions = new List<(int, int)>(Resolutions);
column.Widgets.Add(
Segment(
lang.Get("settings.resolution"),
resolutions,
r => $"{r.Item1}×{r.Item2}",
() => (settings.Width, settings.Height),
r =>
{
settings.Width = r.Item1;
settings.Height = r.Item2;
},
refreshers
)
);
// Громкость — кнопки −/+ с подписью процента.
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
column.Widgets.Add(new Label { Height = 8 });
var buttons = Ui.Row(10);
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));
buttons.Widgets.Add(Ui.Button(lang.Get("settings.back"), onBack, width: 150));
column.Widgets.Add(buttons);
foreach (var refresh in refreshers)
{
refresh();
}
return column;
}
private static Widget Segment<T>(
string label,
IReadOnlyList<T> options,
Func<T, string> text,
Func<T> get,
Action<T> set,
List<Action> refreshers
)
{
var row = Ui.Row(6);
row.Widgets.Add(new Label { Text = label, TextColor = Ui.Muted });
foreach (var option in options)
{
var value = option;
var button = new TextButton { Text = text(value) };
button.Click += (_, _) => set(value);
row.Widgets.Add(button);
refreshers.Add(() =>
button.TextColor = EqualityComparer<T>.Default.Equals(get(), value)
? Ui.Accent
: Ui.Muted
);
}
return row;
}
private static Widget Toggle(
string label,
Func<bool> get,
Action<bool> set,
List<Action> refreshers
)
{
var button = new TextButton();
button.Click += (_, _) => set(!get());
refreshers.Add(() =>
{
button.Text = (get() ? "● " : "○ ") + label;
button.TextColor = get() ? Ui.Accent : Ui.Muted;
});
return button;
}
private static Widget Volume(string label, GameSettings settings, List<Action> refreshers)
{
var row = Ui.Row(6);
var caption = new Label { TextColor = Ui.Muted };
refreshers.Add(() => caption.Text = $"{label}: {(int)MathF.Round(settings.Volume * 100)}%");
void Adjust(float delta)
{
settings.Volume = Math.Clamp(settings.Volume + delta, 0f, 1f);
foreach (var refresh in refreshers)
{
refresh();
}
}
var minus = new TextButton { Text = "" };
minus.Click += (_, _) => Adjust(-0.1f);
var plus = new TextButton { Text = "+" };
plus.Click += (_, _) => Adjust(+0.1f);
row.Widgets.Add(caption);
row.Widgets.Add(minus);
row.Widgets.Add(plus);
return row;
}
}
+31
View File
@@ -0,0 +1,31 @@
using Microsoft.Xna.Framework;
using Myra.Graphics2D.UI;
namespace LittleSim.UI;
/// <summary>
/// Кнопка с текстовой подписью. В Myra 1.6 нет отдельного TextButton: <see cref="Button"/> несёт
/// произвольный <see cref="Button.Content"/>, поэтому оборачиваем <see cref="Label"/> и проксируем
/// к нему <see cref="Text"/> и <see cref="TextColor"/>.
/// </summary>
internal sealed class TextButton : Button
{
private readonly Label _label = new();
/// <summary>Создаёт кнопку с пустой подписью.</summary>
public TextButton() => Content = _label;
/// <summary>Текст подписи.</summary>
public string Text
{
get => _label.Text;
set => _label.Text = value;
}
/// <summary>Цвет подписи.</summary>
public Color TextColor
{
get => _label.TextColor;
set => _label.TextColor = value;
}
}