Author SHA1 Message Date
Leonid Pershin ba8e590941 Refactor game settings and remove unused files
- Removed the GameSettingsStore class and its associated methods for loading and saving game settings, streamlining the GameSettings class.
- Deleted obsolete files related to world saving, game definitions, network smoothing, and content helpers, which were no longer in use, reducing code complexity and improving maintainability.
- Updated the WorldScene class to reflect changes in the GameSettings structure, ensuring compatibility with the new settings management approach.

This update enhances the overall codebase by eliminating redundancy and focusing on essential components.
2026-06-17 08:13:52 +03:00
Leonid PershinandClaude Opus 4.8 9f5f5e2ca9 Events jump-to-creature, UI effects, sane mating & mushroom habitat
- Event log: double-click an entry to focus camera + select the responsible creature; hover highlight, fade-in, hint line.

- UI effects: fade-in for events/perf overlays, hover on inspect tabs and speed buttons, breathing selection bracket.

- Mating: same-species only (no boar x chicken); event strings reworded gender/case-neutral (fixes 'кабан спарилась с курица').

- Mushrooms: shade plants no longer scattered into bright cells (no canopy -> they don't appear), matching the low-light niche.

- Bump engine: lighter night floor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:34:21 +03:00
Leonid Pershin 010d401b75 Enhance HUD with inspector and spawner hints
- Added localization for inspector (F1) and spawner (F9) hints in both English and Russian.
- Updated the WorldScene to display a new HUD card that includes date/time/season and control hints.
- Refactored HudSystem to streamline the display of date/time/season information, removing seed and FPS from the HUD.

This update improves user experience by providing clearer control hints and a more informative HUD layout.
2026-06-16 14:45:21 +03:00
Leonid Pershin d0a0714563 Enhance animal genetics and UI for feeding styles
- Added "GeneFeedingStyle" to animal definitions, allowing for varied feeding behaviors (grazing vs. gorging).
- Updated animal JSON files to include feeding style genes with appropriate values.
- Enhanced UI localization for feeding styles in both English and Russian, providing clearer descriptions and inspection options.
- Introduced new event logging features in the world scene to track animal behaviors related to feeding.
- Improved animal decision-making systems to utilize the new feeding style attributes for more dynamic behavior.

This update aims to enrich gameplay by introducing more nuanced animal behaviors and enhancing the user interface for better clarity and interaction.
2026-06-16 14:27:46 +03:00
Leonid PershinandClaude Opus 4.8 92d0302138 UI: rework settings panel — card, aligned rows, themed segmented controls
The settings screen was ragged: bare gray TextButtons (off-theme), each row
center-aligned on its own so labels/controls never lined up, and a latent bug —
clicking a segment or toggle only changed the value, the highlight didn''t refresh
until Apply rebuilt the panel.

- Wrap in a Card with title + accent rule, matching the other menus.
- Every setting is a "label (fixed width) + control" row, left-aligned, so the
  controls form a clean column.
- Segments and toggles use the themed ChoiceButton with an active state
  (Ui.SetChoiceActive: accent fill/border) instead of text-color-only.
- Toggles read On/Off (new settings.on/off loc) instead of ●/○ glyphs.
- Any click now runs all refreshers immediately, so highlights track the value
  live, not just after Apply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 01:22:30 +03:00
Leonid PershinandClaude Opus 4.8 36b6d576a1 UI: themed dark menu — cards, accent rule, hover/press buttons, backdrop
The menus were default gray Myra buttons on the raw clear color. Give the shared
Ui helpers a cohesive dark theme so every menu screen benefits at once:
- Button: dark fill, accent border, hover (OverBackground) and press (PressedBackground)
  highlights, padded, light text — real feedback instead of flat gray boxes.
- Card: bordered, translucent-dark, padded container; AccentRule: thin accent divider
  under the title.
- MenuScreen: full-screen dark backdrop behind menu content (separate from Screen,
  which stays background-free so the in-game HUD over the world is unaffected).

Main menu, New World, Credits and Load are now framed cards with a title + accent
rule; Settings/Boot/Loading get the backdrop. WorldScene/MultiplayerScene keep the
background-free Screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 01:07:30 +03:00
Leonid PershinandClaude Opus 4.8 02dcd01c0f UI: redesign inspect panel — stat bars, sections, scroll, clearer tabs
Replace the single per-tab text blob with structured widgets:
- StatBar: RimWorld-style colored bars for needs, skills, capacities, blood/pain,
  mood. Good→bad gradient (red/amber/green); drives & skills use a neutral blue;
  pain fills by amount but colors inverted (more pain = red).
- Section headers (accent) group Health (vitals/capacities/conditions) and Mood;
  section keys lose the "— … —" dashes (the header styling separates them now).
- Body lives in a fixed-height ScrollViewer so long tabs (Health) scroll and the
  panel keeps a constant size instead of jumping between tabs.
- Active tab gets a background highlight, not just accent text; tab buttons padded.
- Narrative data (overview/genes/products) stays as styled lines; plant growth-to-
  next-stage shown as a bar.

New loc: inspect.health.vitals, inspect.vital.blood/pain (ru/en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:53:08 +03:00
Leonid PershinandClaude Opus 4.8 56f2c22932 UI: visible pause button + tighter, dimmer selection bracket
- speed.pause was "⏸" (U+23F8), a glyph the bitmap font lacks, so the pause button
  rendered blank/invisible (x1/x3/x6 are ASCII and showed). Use the word Pause/Пауза.
- Selection bracket was radius x2.2 of the sprite''s bounding CIRCLE (half-diagonal),
  so it sat much larger than the entity, in a bright opaque blue. Shrink to x1.4 and
  mute the tint so it hugs the target without shouting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:40:06 +03:00
Leonid PershinandClaude Opus 4.8 9f6b8a0554 Stop tracking runtime settings.json (gitignore it)
settings.json is written by the game at runtime in the working dir and was
accidentally added; untrack it and gitignore /settings.json and /Saves/ (the local
file is kept so the game still reads it). .vscode/settings.json is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:24:11 +03:00
Leonid PershinandClaude Opus 4.8 ccb7c960f9 Disable tree light occlusion (fixes strange isolated shadows on open ground)
A mature tree marked a single lightmap cell as an occluder (shaded to 35% + a
directional sun-shadow tail up to 7 cells). With mountains gone, trees were the
only occluders, so on open soil these showed up as strange dark blobs detached
from the (larger, offset) tree sprite. Gated tree occlusion behind TreesCastShade
(off) so the forest is evenly lit; the occluder infra stays for light-blocking
terrain and re-enabling once tree canopies have a real multi-cell shade footprint.
Trade-off: understory shade no longer dims growth under canopy (minor). Engine
lighting constants are untouched (would need the submodule workflow). Build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:22:56 +03:00
Leonid PershinandClaude Opus 4.8 d196437462 Review pass: fix stale comments (drop mountain refs, update AnimalSet summary)
Code review of the session''s work found the simulation deterministic, save/load
complete, def/localization consistent, and no real correctness bugs (the rest of
the flagged items were defensive-paranoia false positives). Only fixes needed were
stale comments: AnimalSet still described the Phase-A1 single-sprite era (now
directional sprites + stages/sex), and two occluder comments referenced the removed
mountains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:15:05 +03:00
Leonid PershinandClaude Opus 4.8 65ecd2c7b2 Merge branch 'senses-memory' into main
Animal senses (sight/hearing/smell/touch) and memory: smell/touch capacities +
organs (reduced by hediffs/damage), acuity + detectability genes, multi-sense
threat/prey detection with a coarse smell zone, and a brain-bounded memory that
records threats and steers wandering animals away from danger zones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:08:52 +03:00
Leonid PershinandClaude Opus 4.8 f60c55399b Senses stage 3: bounded animal memory (remember & avoid threats)
Memory component holds a small ring of entries {kind, perceived pos, confidence,
age}, capacity = brainSize x 16 (tiny brains remember nothing). On detecting a
threat the animal records it (Memory.Remember, merge-by-proximity, evict weakest);
AnimalMemorySystem forgets entries over ~2 days. While wandering, an animal steers
away from the nearest remembered threat (danger zone lingers after the predator is
out of sight). Component is general (Food/Water/Mate kinds reserved for later) and
transient (not serialized, like AI targets). Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:08:32 +03:00
Leonid PershinandClaude Opus 4.8 3031bf2954 Senses stage 2: multi-sense detection (sight/hearing/smell) for threat & prey
Predator/prey detection now uses three senses instead of one radius. Per observer,
SenseRanges = sight (vision gene x Sight capacity), hearing (x1.4, Hearing gene/cap),
smell (x1.8, Smell gene/cap). Detect(): sight beats the target''s camouflage and
hearing scales with its noise (both give exact position); smell reaches furthest but
only a COARSE zone -- the position snaps to a smell cell, so a predator tracking by
scent heads roughly toward prey until sight takes over. Damaged eyes/ears/nose or
hediff capMods shrink the matching sense; detectability genes (camouflage/scent/noise)
ride in the animal grid entry. Deterministic (snap, not noise). Mate/forage stay on
sight for now. Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:02:59 +03:00
Leonid PershinandClaude Opus 4.8 70e79a5c30 Senses stage 1: smell/touch capacities, organs, acuity + detectability genes
Foundation for the perception system. Adds Smell and Touch capacities (CapacityCalc
dependency chain x consciousness) with organs on the Quadruped body (nose->Smell,
skin->Touch; eyes/ears already give Sight/Hearing) so hediffs and organ damage
reduce them for free. New genes: acuity GeneHearing/GeneSmell/GeneTouch (radius
multipliers, default 1 if a species omits them) and detectability GeneCamouflage
(lower sight), GeneScent (raise smell), GeneNoise (raise hearing) on AnimalPhenotype.
Deer/wolf/boar/chicken get sensible values (wolf smells well, deer is alert +
dappled). cap.smell/cap.touch localized. Build + --check-content clean
(48 genes, 47 traits). Multi-sense detection + memory come next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:55:56 +03:00
Leonid PershinandClaude Opus 4.8 dc617521c2 Merge branch 'animal-bob' into main
Walking sway for animals (AnimalBobSystem) to make movement feel livelier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:29:15 +03:00
Leonid PershinandClaude Opus 4.8 0c07eba021 Liven up animal movement with a walking sway (presentation only)
AnimalBobSystem tilts a moving animal side-to-side via Transform2D.Rotation (a
gentle waddle) and eases it upright when standing. Phase runs on game time (frozen
on pause, faster when sped up) and is offset per entity id so animals sway out of
sync. Purely presentational -- the simulation ignores rotation, and the facing
system writes scale (not rotation), so no conflict. Corpses/eggs have no brain so
they stay still. Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:29:14 +03:00
Leonid PershinandClaude Opus 4.8 405ed192a8 Merge branch 'remove-mountains' into main
Remove mountain terrain (forest covers the high elevation band for now).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:24:19 +03:00
Leonid PershinandClaude Opus 4.8 cad4b5f38f Remove mountains from the map (forest covers the high band for now)
Dropped the Mountain terrain def and extended Forest to maxHeight 1.01, so the
former mountain elevations generate forest instead. No terrain blocks light now;
the occluder mechanism stays generic (mature trees still cast shadows) -- updated
the stale "mountain" comments. Removed the unused terrain.mountain localization.
Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:24:19 +03:00
Leonid PershinandClaude Opus 4.8 0d7ea55824 Merge branch 'animal-facing' into main
Animals turn to face their direction of travel (east/north/south sprites + west
flip), via AnimalFacingSystem and directional sprite sets in AnimalSet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:12:32 +03:00
Leonid PershinandClaude Opus 4.8 edfbb15251 Animals face their movement direction (4-way: east/west-flip/north/south)
Animals had a single east-facing sprite and never turned. AnimalSet now loads a
directional set per look (female/male/baby): east from the def texture, north/south
by the _east->_north/_south naming convention (falls back to east if absent), with
west rendered as a horizontal flip of east. New AnimalFacingSystem orients each
animal toward its brain Target (dominant axis of travel), updating sprite region +
flip and recomputing origin/scale when the region changes; a standing animal keeps
its facing. ModAtlases gained TryGetRegion for the optional directional lookups.
Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:12:32 +03:00
Leonid PershinandClaude Opus 4.8 d9beac9331 Merge branch 'world-loading-scene' into main
Add a loading screen (WorldLoadingScene) between world setup/load and WorldScene so
the blocking world generation shows a "Generating world" indicator instead of a
blank fade.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:02:19 +03:00
Leonid PershinandClaude Opus 4.8 6ec70cab91 Loading screen between world setup and the game scene
World generation (WorldScene.OnLoad: terrain, atlases, spawn) runs synchronously
and blocks the frame; previously only a blank fade covered it, with no feedback.
New WorldLoadingScene shows a "Generating world" label, lets it draw, then switches
to WorldScene WITHOUT a transition so its last drawn frame (the label) stays on
screen during the blocking generation. Both New World and Load Game route through
it. Localized loading.world ru/en. Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:02:19 +03:00
Leonid PershinandClaude Opus 4.8 282ec018f7 Merge branch 'animal-grid' into main
Spatial grid for animal neighbour queries: threat/prey/mate/herd scans now use a
per-frame SpatialGrid<NeighborEntry> instead of iterating every animal per decision,
removing the O(animals^2) cost. Deterministic (id tie-breaks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:55:41 +03:00
Leonid PershinandClaude Opus 4.8 b61c6acff3 Perf: spatial grid for animal neighbour queries (drops O(animals^2) AI scans)
Generalized the plant grid into SpatialGrid<T> (PlantGrid.cs -> SpatialGrid.cs)
and added an animal grid. AnimalDecisionSystem rebuilds both once per frame; the
animal grid entry precomputes the fields the AI needs (species, body size, eats-meat,
sex, adult) so queries do not refetch components. Threat / prey / mate / herd scans
now gather only neighbours from cells overlapping the vision radius instead of
iterating every animal per decision. IsThreatTo/IsPreyFor take precomputed values;
dead MateNeedIndex removed. Mate stays species-agnostic (unchanged behaviour).
Added id tie-breaks to threat/mate selection so results are order-invariant ->
fully deterministic regardless of grid iteration order. Build + check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:55:27 +03:00
Leonid PershinandClaude Opus 4.8 347fac2c1b Merge branch 'perf-hotpaths' into main
Hot-path performance: removed per-frame allocations and string genome lookups
(Kleiber via precomputed RefBodySize, single Skills fetch, senescence scale inside
CapacityCalc, alloc-free SkillSeed), and a plant spatial grid that replaces the
dominant per-decision scan of all plants with a local cell query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:46:00 +03:00
Leonid PershinandClaude Opus 4.8 15cad39240 Perf: spatial grid for plant foraging (drops the dominant O(plants) scan)
Plants vastly outnumber animals, and every herbivore forage decision scanned ALL
plants — the dominant per-decision cost. New PlantGrid (uniform cell grid, cell
lists pooled so rebuild is alloc-free) is rebuilt once per frame from plant
positions; TryFindPlant/TryFindBestPlant now gather only candidates from cells
overlapping the vision radius instead of iterating every plant. The entry carries
position/id/toxicity/palatability so no component refetch is needed. Selection is
unchanged (nearest / max-attractiveness, id tie-break), and since the result is
order-invariant the behaviour is identical and deterministic. Build + check clean.

Note: the animal-side neighbour scans (threat/prey/mate/herd) are still O(animals)
per decision — animals are far fewer than plants, and an animal grid is a riskier
change best done with a GUI run; deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:44:35 +03:00
Leonid PershinandClaude Opus 4.8 2b9d1fd105 Perf: kill per-frame allocations and string lookups in hot sim paths
- AnimalNeedsSystem (per-frame, every animal): drop the string-keyed genome lookup
  and Mass.OfAnimal call; Kleiber factor is now (size/refBody)^2.25 using a
  precomputed AnimalSet.Species.RefBodySize. No dict probe per animal per frame.
- AnimalActionSystem: fetch the Skills component ONCE per acting animal instead of
  twice per action (SkillFactor + GrantXp took in Skills now).
- RecomputeCapacities: apply the senescence scale inside CapacityCalc.Compute
  instead of a second pass that allocated a List<string> of keys every recompute
  (per animal per health tick, and per injury in combat).
- AnimalFactory.SkillSeed: iterate genome.Alleles (IReadOnlyDictionary, no copy)
  instead of ToDictionary() which allocated a dict per spawn.

Behaviour-preserving (same numbers). Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:39:36 +03:00
Leonid PershinandClaude Opus 4.8 3dfa8c658f Merge branch 'skills-and-devtools' into main
Developer-mode setting gating the dev console, a practice-grown skills system
(with passion) for animals, and a RimWorld-style moddable dev spawner/damage tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:25:04 +03:00
Leonid PershinandClaude Opus 4.8 cdb8439edf Dev spawner: RimWorld-style moddable spawn/damage tool (dev mode only)
A side panel (F9, only when developer mode is on) lists entries by category:
animals and plants are auto-populated from their defs (so modded content shows up
automatically), plus action tools. Spawn entries arm a "tool in hand" — left-click
the world to spawn at the cursor (right-click clears); action tools (injure/kill/
infect) apply to the currently selected creature, so you can line up entities and
damage them to compare. Moddable via a new DevToolDef def type: mods add buttons
referencing a code handler kind (spawn-animal/plant/infect/damage/kill) + target,
without touching code. Localized dev.* ru/en. Build + --check-content clean
(13 def types).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:20:59 +03:00
Leonid PershinandClaude Opus 4.8 58380adc31 Skills system: practice-grown abilities with passion (animals now, humans later)
Generic data-driven skills (SkillDef + skills.json: Foraging, Hunting, Evasion),
mirroring the needs registry. Skills component (per-skill level [0..1] + passion)
on every animal, built by AnimalFactory; passion is rolled deterministically from
the genome (no RNG param threaded). Levels grow from PRACTICE (XP on the matching
action, scaled by passion''s learn-rate) and slowly decay toward a floor when
unused (AnimalSkillSystem), so animals specialize.

Effects wired now: Foraging -> satiety from grazing, Hunting -> attack damage,
Evasion -> flee speed. A missing skill def -> factor 1 (no effect), so it is safe
to mod the set. Inspector gains a Skills tab (level% + passion stars); `skills`
console command shows live mean levels; skills are saved/loaded (levels+passions).
Same model will carry to future humans. Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:15:11 +03:00
Leonid PershinandClaude Opus 4.8 cf2bc77882 Developer-mode setting gates the dev console
GameSettings.DeveloperMode (default ON) + a toggle in the settings panel. WorldScene
only stands up the dev console (and, later, the dev spawner) when it is enabled, so
without dev mode the backtick key does nothing. GodCameraSystem''s console arg is now
nullable (no console when dev mode off). Localized settings.devmode ru/en. Build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:57:21 +03:00
Leonid PershinandClaude Opus 4.8 d44e17198b Merge branch 'mass-system' into main
Weight system: cubic body mass for plants/animals (BaseMassKg), carry capacity,
Kleiber metabolism, mass-based corpse meat, and a dormant Carrier foundation for
future item hauling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:36:18 +03:00
Leonid PershinandClaude Opus 4.8 1a3f205f86 Weight system: body mass, carry capacity, Kleiber metabolism
Mass is derived by a cubic law: mass = BaseMassKg * (size / reference)^3, so a
juvenile/seedling weighs far less than an adult/mature and the size gene moves
mass too. New Mass helper + BaseMassKg on AnimalDef/PlantDef (deer 70, wolf 45,
boar 80, chicken 2.5; trees ~450, grass 0.4, etc.) and MassKg per unit on
ProductDef (foundation for hauling/inventory).

Wired into the sim (as chosen):
- Corpse meat is now proportional to body mass (Mass.MeatFraction), not raw
  bodySize; Corpse gained a BodySize field so sprite scale stays independent of
  meat (save/load updated).
- Kleiber metabolism: need decay x= (mass/refMass)^0.75, normalized per species,
  so juveniles are cheaper to feed and heavier individuals eat a bit more,
  without cross-species balance blowups (adults of typical size are unchanged).
- Carry capacity = mass x fraction x carry capability (Moving for quadrupeds;
  Manipulation for future humans), shown in inspector/console. A dormant Carrier
  component + Mass.LoadSpeedFactor hook slow an overloaded animal — the
  foundation for item hauling.

Inspector shows body/plant weight (+carry); `animal`/`plant` console print mass.
Build + --check-content clean (42 genes, 41 traits). Not GUI-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:30:36 +03:00
Leonid PershinandClaude Opus 4.8 0e2cf55a54 Merge branch 'ecology-thermal-social' into main
Thermoregulation (revives GeneInsulation) + wound infection, seed dispersal by
herbivores (endozoochory), and herding/flocking via GeneSociability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:29:11 +03:00
Leonid PershinandClaude Opus 4.8 c0822de650 Herding / flocking via GeneSociability (boids)
Social species (deer 0.8, wolf 0.6, chicken 0.55, boar 0.3) now school together:
AnimalDecisionSystem scans nearby same-species neighbours (HerdScan) and, when
wandering, a lone social animal steers toward the herd centroid (cohesion) while
close members just mill about (separation). "Safety in numbers" shrinks the
threat-detection radius in a group, so herded prey are calmer and scatter less.
Asocial animals (sociability < 0.3) keep the old random wander. GeneSociability
+ trait; `animal` console prints insulation/sociability. Build + --check-content
clean (42 genes, 41 traits). Not GUI-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:02:28 +03:00
Leonid PershinandClaude Opus 4.8 5ac34f4dc7 Seed dispersal by herbivores (endozoochory)
Eating a mature plant has a chance to lodge a seed in the herbivore's gut
(GutSeed component, cloned plant genome). SeedDispersalSystem ticks the timer and
plants a seedling of that species wherever the animal has wandered to (if the
cell is land), then drops the component. Herbivores become agents of plant
spread — flora follows grazing routes — closing a plant<->animal loop on top of
the existing Fruiting/PlantFactory machinery. GutSeed is transient (not
serialized), like AI targets. Build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:58:28 +03:00
Leonid PershinandClaude Opus 4.8 562e365be1 Animal thermoregulation (insulation) + wound infection
Two health threads finished, both via the existing hediff engine in
AnimalHealthSystem (now also takes Climate + the new hediff defs):

- Thermoregulation revives the dead GeneInsulation. A comfort band is derived
  per animal: fur (insulation) widens cold tolerance but worsens heat tolerance,
  larger body resists cold. Outside it, Hypothermia/Heatstroke severity builds
  (Poisoned-pattern: Intensify zeroes immunity so it can't recover while still
  exposed), capacities drop, severity 1 = death; back in comfort it heals.
  Seasonal/climate mortality and a real cold↔heat adaptation tradeoff.
- Wound infection marries the injury and disease systems: while bleeding, a
  chance (∝ wound severity) to contract Infection, a progressing disease fought
  by immunity × blood filtration — weak kidneys/liver or many wounds = worse.

New hediffs Hypothermia/Heatstroke/Infection (+ru/en). Build + --check-content
clean. Not GUI-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:53:27 +03:00
Leonid PershinandClaude Opus 4.8 5b0cf1dd4a Merge branch 'coevolution-health' into main
Plant–herbivore coevolution (toxin/thorns ↔ tolerance, smart avoidance, costs),
RimWorld-style health capacities wired to behaviour, senescence, and birth-type
genes (live-bearing vs egg-laying with eggs) + hybrid-creation foundation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:44:17 +03:00
Leonid PershinandClaude Opus 4.8 b2e3eb490e Birth type gene (live-bearing vs egg-laying) + hybrid-creation foundation
GeneEggLaying (species-fixed) selects the conception cycle: viviparous animals
carry to a live birth (existing path); oviparous animals, after the same gravid
period, LAY eggs instead. New Egg entity (carries the already-bred offspring
genome) + EggSystem incubates and hatches it into a baby. AnimalPregnancySystem
branches on the mother's birth type. Adds an oviparous showcase species, Chicken.
Eggs are serialized (WorldSave.Eggs) like corpses, so clutches survive save/load.

Also lays the foundation for creating any species or hybrid (full command/UI
deferred): AnimalSet.HybridGenome(base, other) produces a cross anchored to a
base species' body/gene-set with shared genes blended Mendelian-style — so
Create(species, GenerateGenome) makes any species and Create(species,
HybridGenome) makes any hybrid.

EggTexture on AnimalDef (defaults to the seed dot placeholder); reproduction line
added to the `animal` console command. Build + --check-content clean (41 genes,
40 traits). Not GUI-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:24:04 +03:00
Leonid PershinandClaude Opus 4.8 e6b1367a3a Senescence: aging degrades capacities and fertility
GeneLifespan already set the moment of death from old age; now aging actually
manifests before it. RecomputeCapacities takes a capacityScale; AnimalHealthSystem
applies SenescenceFactor(age, lifespan) — capacities fade from 60% of lifespan to
~0.55× near the end, so old animals move/see/filter blood worse and are frailer.
Fertility falls with the same factor (smaller litters), and past 90% of lifespan
females no longer conceive (post-reproductive). Makes GeneLifespan and the Senior
stage meaningful. Build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:13:26 +03:00
Leonid PershinandClaude Opus 4.8 249b603497 Wire Eating + Digestion capacities to feeding (option 1)
Eating (jaw) scales graze bite and Digestion×Eating scales satiety gained from
food (plant grazing and corpse scavenging via Refill efficiency). A damaged jaw
or sick gut means less nutrition per feeding, so the animal must eat more often.
Completes wiring the new health capacities to behaviour; Talking remains an
indicator (no social system yet). Build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:58:10 +03:00
Leonid PershinandClaude Opus 4.8 e105938a22 Wire health capacities to behaviour: filtration→immunity, sight/hearing→perception
The expanded capacities were mostly display-only; make two of them affect the
simulation (Moving→speed and Consciousness→intelligence gating already existed):

- Blood filtration → toxin/disease clearance: AdvanceHediffs takes an immunity
  scale (default 1); AnimalHealthSystem passes the BloodFiltration capacity, so
  damaged kidneys/liver clear Poisoned and diseases slower (RimWorld model).
  Ties the new capacities to the coevolution work — weak filtration makes toxic
  plants deadlier.
- Sight/hearing → perception radius: foraging, hunting and mate search scale by
  Sight; threat detection (flee) scales by max(Sight, Hearing) — you see or hear
  a predator. Injured eyes/ears shrink awareness.

Eating/Talking/Digestion remain indicators for now. Build + --check-content
clean. Not GUI-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:18:35 +03:00
Leonid PershinandClaude Opus 4.8 8807045303 Plant–herbivore coevolution + RimWorld-style health capacities
Adds the first plant↔animal coupling beyond "herbivore eats plant": a toxin/
thorns ↔ tolerance arms race built on the existing gene + hediff engines, plus
the fuller RimWorld capacity set on the health scaffold.

Coevolution C1 — plant defense → poisons/injures the eater:
- Plant genes GeneToxicity/GeneThorns/GenePalatability (GenomeDef + PlantSet
  template + PlantPhenotype); animal gene GeneToxinTolerance.
- Hediff Poisoned (non-progressing; grows from eating via HealthState.Intensify,
  immunity heals it, overdose is lethal).
- AnimalActionSystem.ApplyPlantDefense on each bite: poison dose =
  toxicity × (1 − tolerance); thorns → ApplyInjury (reuses predator-cluster).
- Cost of defense: PlantGrowthSystem growth ×= DefenseGrowthFactor()
  (1 − 0.4·toxicity − 0.3·thorns) — without a cost defense would max out and
  coevolution would stall.
- Showcase: cactus thorns, poisonous mushroom; grass starts clean so toxicity
  can evolve on the staple. Deer/boar get small starting tolerance.

Coevolution C2 — discrimination + the other half of the arms race:
- Smart foraging (reuses A7 intelligence gating): herbivores with effective
  intelligence ≥ tier-4 weigh plants by palatability − perceived-toxin −
  distance; reflex grazers (or sick animals with dropped consciousness) eat the
  nearest plant and risk poison.
- Cost of tolerance: detox raises metabolism (need decay ×= 1 + 0.5·tolerance),
  closing the arms race so tolerance doesn't fix at 1.
- popstats reports mean toxinTolerance and a plants line (mean toxicity/thorns/
  palatability) to watch the drift.

Health capacities (RimWorld parity, user-requested):
- New capacities BloodFiltration/Hearing/Talking/Eating; CapacityCalc rewritten
  to emit only capacities the body declares (a destroyed organ still shows 0%)
  with the full dependency chain (blood → pumping/breathing/filtration →
  consciousness → moving/sight/hearing/talking/eating, digestion).
- Quadruped body gains kidneys + liver (filtration), ears (hearing), jaw/tongue
  (eating/talking). Inspector health tab lists all of a body's capacities.

Localization (ru/en) for new genes' defense line, Poisoned, and the new
capacity labels. Build + --check-content clean (11 def types, 40 genes,
39 traits). Not GUI-verified (headless can't render the world scene).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:13:51 +03:00
Leonid Pershin 97d83606ce Implement diet genes for animals, introducing GeneHerbivory, GeneCarnivory, and GeneOmnivory to define dietary behaviors. Add new animal types: Wolf (carnivore) and WildBoar (omnivore), with associated attributes and hunting mechanics. Enhance animal behavior systems to include hunting and fleeing dynamics based on genetic traits. Update documentation to reflect these changes and ensure localization for new terms. Clean build with no content errors. 2026-06-14 13:14:36 +03:00
Leonid PershinandClaude Opus 4.8 aae76e9083 Merge branch 'animals' into main: animal/zoology system (A1-A8 + serialization)
Brings the full animal foundation onto main and folds its new defs into the
Defs/ subfolder layout introduced on main:
- animal genes -> Defs/Genes/Animal.json (split out of genes.json)
- animals/bodies/hediffs/needs/thoughts -> own Defs/<Type>/ subfolders
- ProductMeat/Bone/Leather + bigger world presets auto-merged via rename detection
- PawnDef un-sealed (AnimalDef : PawnDef); def-location doc comments updated to subfolders
- engine pointer kept at main's newer 96e19c7 (only adds directional sun shadows)

Build + --check-content clean: 11 def types, 33 genes, 35 plants, 32 traits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 12:48:49 +03:00
Leonid Pershin c47f99bc29 Update engine subproject commit to 96e19c7. Remove obsolete gene, patch, pawn, plant, product, terrain, and world preset definitions from the Core mod. Enhance documentation to reflect new directory structure for Defs, improving clarity on file organization and localization paths. 2026-06-14 12:42:09 +03:00
Leonid Pershin 77c6e84683 Обновление данных: добавлены новые продукты (мясо, кости, кожа) в products.json и изменены размеры и население в worldpresets.json. Также обновлено описание метода регистрации слоев в GameLayers.cs. Обновлен статус подпроекта в engine. 2026-06-14 07:01:38 +03:00
Leonid PershinandClaude Opus 4.8 4b53eb8984 Животные: сериализация — сейв/лоад популяции и трупов
Закрывает сквозное требование плана (сейв/лоад данных каждой фазы). WorldSave получил Animals (AnimalSave: геном+пол, поколение, стадия/возраст, нужды, здоровье — кровь/HP частей/хедифы, настроение+мысли, беременность+геном отца) и Corpses (CorpseSave). SaveAnimals снимает популяцию (Health/Mood/Pregnant через entity); RestoreAnimals/RestoreAnimalState собирают особь фабрикой из генома + накладывают состояние (ref-правки до AddComponent(Pregnant); труп пере-тинтится через Stage=-1). Corpse получил Species; CreateCorpse принимает индекс вида + HealthState.AddHediff(def,severity,immunity). На загрузке стадо не пересыпается при наличии животных в сейве; старый/пустой сейв регенерируется из сида. Сборка + --check-content чистые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:59:26 +03:00
Leonid PershinandClaude Opus 4.8 003f235440 Животные A8 часть 2: инспектор животных (вкладки нужды/здоровье/настроение)
InspectPanel осматривает и животных: вкладки Обзор (стадия/возраст/пол/поколение/беременность), Нужды, Здоровье (кровь/боль, способности, хедифы), Настроение (значение + мысли). Набор вкладок по типу выбранного. SelectionSystem пикает и животных (запрос расширен PlantGrowth → +AnimalOrganism). Локализация inspect.*/need.*/hediff.*/cap.*/animalstage.* ru+en. Базовый план зоологии A1-A8 закрыт. Сборка + --check-content чистые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:19:23 +03:00
Leonid PershinandClaude Opus 4.8 1b12831817 Животные A8 часть 1: настроение — ядро (ThoughtDef + Mood + MoodSystem)
Тип дефа ThoughtDef + thoughts.json (роды/спаривание/жажда — сила и длительность данными). Компонент Mood (значение 0..1, активность по тиру мозга, список затухающих мыслей) на всех животных; AnimalMoodSystem (тир 4+: настроение из непрерывного фона — средняя удовлетворённость убывающих нужд минус боль — плюс сумма мыслей). Реестр ThoughtSet + id-константы AnimalThoughts; триггеры: роды (PregnancySystem), спаривание и утоление жажды (ActionSystem). Низкое настроение подавляет спаривание (DecisionSystem). Команда mood [species]. Локализация thought.* ru/en. Сборка + --check-content чистые (11 типов дефов). Осталось A8.2 — вкладки инспектора.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:13:46 +03:00
Leonid PershinandClaude Opus 4.8 26bf83db64 Животные A7: мозг → интеллект → гейт нужд/действий
Эффективный интеллект = brainSize (ген) × Consciousness (capacity из здоровья), AnimalFactory.Intelligence. AnimalContext гейтит выбор ИИ: нужда с minBrain выше интеллекта особи в выбор не входит (AnimalDecisionSystem читает Health поэлементно). Пороги в needs.json — тиры мозга (голод 0.0, жажда 0.25, сон/секс 0.40; у оленя мозг ~0.45). Обратная связь от повреждения мозга: болезнь/боль/кровопотеря роняют сознание → интеллект падает → первыми отключаются сон/спаривание. Команда intel <species> [consciousness]. Сборка + --check-content чистые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 06:00:11 +03:00
Leonid PershinandClaude Opus 4.8 9f8ff93591 Животные A6: болезни и иммунитет
HediffDef расширен (initialSeverity/severityPerDay/immunityPerDay/lethalSeverity/pain/ambientPerDay/capMods); хедифы стали инстансами (Hediff: тяжесть+иммунитет). AnimalHealthSystem (медленный тик ~час): фоновое заражение, гонка тяжесть<->иммунитет (выздоровление при иммунитете 1 / смерть при летальной тяжести), пересчёт способностей с болью и capMods; смерть по здоровью оставляет труп. Первый реальный источник урона: Moving подключён к скорости (больные медленнее). Болезнь Fever; команда infect для теста.

Сборка чистая, --check-content (10 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 05:48:05 +03:00
Leonid PershinandClaude Opus 4.8 9db12273c9 Животные A5 (часть 2): части тела, кровь, capacities
BodyDef/BodyPartDef + bodies.json (Quadruped — данные: иерархия органов, вклады в способности). AnimalDef.Body. HealthState расширен Parts/BloodLevel/Pain/Capacities; CapacityCalc обобщённо считает способности по зависимостям (кровь->сердце/лёгкие->сознание->движение/зрение), модовые проходят сырыми. Фабрика строит части (HP x размер тела) и начальные способности. Источников урона нет -> всё на полном HP, способности=1 (каркас под болезни/хищников). Команда body <species>.

Сборка чистая, --check-content (10 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 05:36:34 +03:00
Leonid PershinandClaude Opus 4.8 c90bf32460 Животные A5 (часть 1): смерть → труп → разложение
Компонент Corpse + CorpseSystem: после смерти животное оставляет труп (AnimalFactory.CreateCorpse, мясо ∝ размер тела) вместо деспавна; труп разлагается Fresh→Rotting→Skeleton→исчезает со скоростью от температуры, тинтуясь по стадии. AnimalDef.CorpseTexture + спрайт трупа в AnimalSet; смертность спавнит труп. Продукты мясо/кости/шкура заданы (разделка/падальщики — позже).

Сборка чистая, --check-content (9 типов дефов). Части тела/кровь/capacities — часть 2 A5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 05:22:11 +03:00
Leonid PershinandClaude Opus 4.8 cbc74c0794 Рефактор: зрение/скорость/выедание животного — из дефа вида
Завершает вынос баланса в данные: AnimalDecisionSystem.VisionRadius берёт дальность из AnimalDef.VisionCells (× ген), TryFindPlant/TryFindMate принимают готовый радиус; AnimalActionSystem берёт BaseSpeed и ForageBiteDays из дефа вида (через AnimalSet). Убраны хардкод-константы скорости/зрения/выедания.

Сборка чистая, --check-content (9 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 05:16:44 +03:00
Leonid PershinandClaude Opus 4.8 5d0cbd2115 Рефактор: стадии роста и спавн — данными
Стадии стали данными: AnimalStageDef + AnimalDef.Stages (возраст входа = EnterAt x якорь maturity/lifespan, масштаб, набор текстур baby/adult, флаг adult для размножения); пусто -> встроенный дефолт Baby/Juvenile/Adult/Senior. AnimalFactory.StageAt/RegionFor/Scale/IsAdult работают по данным стадий; системы роста/решения/гона используют флаг adult вместо индекса. Спавн стал данными: AnimalDef.SpawnPer1000Cells — WorldScene рассыпает ВСЕ виды по плотности, не только захардкоженного оленя. Устраняет блокеры расширяемости #5 и #7.

Сборка чистая, --check-content (9 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:57:44 +03:00
Leonid PershinandClaude Opus 4.8 f12b0f170c Рефактор: нужды и диета — данными, а не кодом
Нужды стали данными: NeedDef + needs.json (Hunger/Thirst/Rest/Mating), NeedSet (реестр: индексы, тип deplete/drive, поведение-исполнитель по id), динамический AnimalNeeds.Values (managed-массив — новая нужда не меняет структуру). AnimalNeedsSystem универсально считает убывание/рост по NeedDef; UtilityAi строится из NeedSet; действия исполняются по строковому id (реестр поведений) с восполнением нужды через NeedSet. Диета — AnimalDef.Diet (поиск корма гейтится). Добавить нужду/рацион виду = JSON без кода. Устраняет блокеры расширяемости #2 и #4.

Сборка чистая, --check-content (9 типов дефов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:46:47 +03:00
Leonid PershinandClaude Opus 4.8 d4a370e02d Рефактор: геном животного — данными, а не кодом
AnimalGenomeDef (фикс. поля) убран; AnimalDef.Genome теперь словарь geneId->base. AnimalSet.BuildTemplate строит шаблон generically по реестру генов (разброс из самого GeneDef, дискретные поддержаны). Модер добавляет ген особи одной строкой JSON в animals.json — без правки кода. Устраняет блокер расширяемости #1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:39:16 +03:00
Leonid PershinandClaude Opus 4.8 97521f81b8 Животные A4: размножение (гон-хедиф, беременность, роды)
Нужда Mating + лёгкий hediff-каркас (HediffDef, hediffs.json с Rut, Health/HealthState). Гены GeneBreedingSeason/GeneGestationDays/GeneLitterSize. AnimalRutSystem навешивает сезонный гон-хедиф взрослым и поднимает влечение (вне сезона зачатия нет). Действие Mate ищет партнёра противоположного пола и сближается; при контакте самка получает Pregnant (геном отца). AnimalPregnancySystem тикает срок и рожает помёт через Genome.Breed (пол наследуется без YY, поколение+1), с потолком численности.

Сборка чистая, --check-content (8 типов дефов, 33 гена, 32 признака).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:32:18 +03:00
Leonid PershinandClaude Opus 4.8 355ced4937 Животные A3: жизненный цикл, стадии и пол
Компонент AnimalGrowth (стадия + возраст). Ген пола GeneSex (локус XX/XY; генерация основателя задаёт пол явно через AnimalSet.GenerateGenome, исключая невозможный YY) и GeneMaturityAge. Стадии Baby->Juvenile->Adult->Senior с порогами из генов. AnimalGrowthSystem меняет спрайт (детёныш/самец/самка) и размер по стадии; AnimalMortalitySystem — смерть от старости и истощения (деспавн), замыкает контур ёмкости среды. Стадо спавнится разновозрастным.

Сборка чистая, --check-content (30 генов, 29 признаков).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:20:44 +03:00
Leonid PershinandClaude Opus 4.8 e35e620655 Животные A2: нужды и ИИ (выбор + исполнение)
AnimalNeeds (голод/жажда/отдых) падают по гену метаболизма. Два слоя ИИ: AnimalDecisionSystem (движковый UtilityAi выбирает действие и ищет ближайшую еду/воду) и AnimalActionSystem (движение к цели со скоростью по гену + утоление: выедание растений с гибелью выеденной травы, питьё у кромки воды, сон). AnimalAppearanceSystem тускнеет с острой нуждой. Кромка воды считается из террейна; команда popstats для наблюдаемости дрейфа генов.

Сборка чистая. Контур ёмкости среды частичный — смерть от голода придёт в A3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:13:50 +03:00
Leonid PershinandClaude Opus 4.8 5e61491960 Животные A1: фундамент — AnimalDef, гены зверя, спавн оленя
Новый тип дефа Animal (AnimalDef : PawnDef) + animals.json (Deer). 8 организм-агностичных животных генов в genes.json. AnimalPhenotype/AnimalOrganism, AnimalSet (шаблон генома вида) и AnimalFactory — зеркала растительных PlantSet/PlantFactory. Детерминированный спавн стада оленей по суше в WorldScene с рендером и тинтом меха по гену; консольная команда 'animal <species>'.

Проверено: сборка чистая, --check-content (7 типов дефов, 28 генов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:03:51 +03:00
Leonid PershinandClaude Opus 4.8 516e263c77 docs: дизайн системы животных (зоология)
Полный план зоологии на генетическом фундаменте: 7 столпов (гены, жизненный цикл, размножение, здоровье по модели RimWorld, мозг/интеллект, нужды, настроение), моддинг данными, фазы A1-A8 и экологический горизонт.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 04:03:39 +03:00
Leonid Pershin 9c9502414b Update plant definitions and localization. Adjusted GrassA sizeCells from 1.2 to 1.5 and added new plant types including BaseWarmTree, BaseColdTree, and various cacti. Enhanced terrain definitions with scatter probabilities for new plants. Updated product definitions to include fiber and mushrooms, and localized new plant and product names in English and Russian. Updated engine subproject commit to 08381703f79377a5bc48fc0745620f8815695b05-dirty. 2026-06-13 06:55:43 +03:00
Leonid Pershin b9bb7006be Enhance UI localization and settings functionality. Added new localization keys for inspection tabs and UI scale settings in both English and Russian. Updated GameSettings to include a UiScale property. Refactored scenes to utilize UseScaledUI for consistent UI scaling across various game scenes. Improved InspectPanel to support tabbed navigation for overview, genes, and products. Adjusted SettingsPanel to allow users to modify UI scale dynamically. 2026-06-13 06:24:53 +03:00
Leonid Pershin c60ae072e9 Update engine subproject commit to 08381703f79377a5bc48fc0745620f8815695b05 2026-06-13 05:21:30 +03:00
Leonid Pershin f46ae15dc6 Update engine subproject commit to f382fc98eadc4a489689e51e88748b616661a560-dirty; adjust plant genetics for optimal temperature and hardiness traits, enhance terrain scatter probabilities, and improve UI localization for connection status. Refactor multiplayer scene to implement exponential backoff for reconnections and streamline game initialization in web client. 2026-06-13 05:21:17 +03:00
Leonid PershinandClaude Fable 5 6b29a40b7f Bump engine: MrGameEng.Net robustness
Points the engine submodule at f382fc9 (pushed): heartbeat liveness +
idle-timeout drop, replication protocol-version byte, reassembled-message
size cap, and ReplicationClient.Clear() for reconnect. Enables the desktop
and web clients to auto-reconnect and the server to shed dead connections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 04:45:13 +03:00
Leonid Pershin 6f38d949ab Update engine subproject commit to d360093be1a9352fd2f531dde620a400c83a5d71 2026-06-13 04:34:39 +03:00
Leonid Pershin cc1841c2e2 Enhance plant genetics system with cold and heat hardiness traits. Updated genes.json to include GeneColdHardiness and GeneHeatHardiness, and modified plant definitions to incorporate these traits. Adjusted temperature tolerance calculations in PlantPhenotype and PlantLifecycleSystem to account for temperature stress. Improved documentation and added temperature stress management in the lifecycle system. 2026-06-13 04:34:03 +03:00
Leonid Pershin 4fda693994 Update engine subproject commit to d044cafad93177e0fbffb79872bde12afcf03f9c-dirty; remove outdated documentation files: растения.md and roadmap.md. 2026-06-13 04:27:35 +03:00
mrleo1nid d712afa518 Merge pull request 'Multiplayer: headless dedicated server, MrGameEng.Net, desktop and browser clients' (#1) from claude/fervent-shannon-8d884e into main
Reviewed-on: #1
2026-06-13 01:25:07 +00:00
194 changed files with 13553 additions and 1993 deletions
+3
View File
@@ -417,4 +417,7 @@ FodyWeavers.xsd
# LittleSim # LittleSim
Cache/ Cache/
# Рантайм-файлы (пишутся игрой в рабочем каталоге) — не версионируем
/settings.json
/Saves/
+174
View File
@@ -0,0 +1,174 @@
{
"type": "Animal",
// Виды-животные: подтип Pawn (kind=animal) + базовый геном вида из общих Gene-дефов (см. Defs/Genes/).
// Особь при спавне получает аллели вокруг этих баз, фенотип — формулами генов. Диета задаётся ГЕНАМИ
// (herbivory/carnivory/omnivory): олень — травоядный, волк — хищник, кабан — всеядный. Хищники несут
// боевые параметры (attack*) и охотятся на добычу не крупнее себя; жертвы убегают (см. AnimalSystems).
"defs": [
{
"defName": "Deer",
"label": "pawn.deer",
"kind": "animal",
"texture": "things/pawn/animal/deer/DeerFemale_east",
"maleTexture": "things/pawn/animal/deer/DeerMale_east",
"babyTexture": "things/pawn/animal/deer/DeerBaby_east",
"corpseTexture": "things/pawn/animal/deer/Dessicated_DeerFemale_east",
"body": "Quadruped",
"diet": ["plant"],
"spawnPer1000Cells": 2.5,
"baseMassKg": 70,
"genome": {
"GeneMaxBodySize": 1.5,
"GeneMetabolism": 1.0,
"GeneMoveSpeed": 1.2,
"GeneBloodVolume": 1.4,
"GeneVision": 1.3,
"GeneHearing": 1.4,
"GeneSmell": 1.2,
"GeneTouch": 1.0,
"GeneCamouflage": 0.35,
"GeneScent": 0.5,
"GeneNoise": 0.4,
"GeneBrainSize": 0.45,
"GeneInsulation": 0.55,
"GeneFurColor": 0.45,
"GeneMaturityAge": 90,
"GeneLifespan": 360,
"GeneBreedingSeason": 2,
"GeneGestationDays": 30,
"GeneLitterSize": 1,
"GeneHerbivory": 1.0,
"GeneToxinTolerance": 0.1,
"GeneFeedingStyle": 0.15,
"GeneSociability": 0.8
}
},
{
"defName": "Wolf",
"label": "pawn.wolf",
"kind": "animal",
"texture": "things/pawn/animal/wolf_timber/Wolf_Timber_east",
"maleTexture": "things/pawn/animal/wolf_timber/Wolf_Timber3_east",
"body": "Quadruped",
"diet": ["meat"],
"spawnPer1000Cells": 0.8,
"baseSpeed": 32,
"visionCells": 18,
"attackRangeCells": 1.3,
"attackIntervalSec": 1.4,
"attackDamage": 16,
"attackBleed": 0.16,
"attackBloodLoss": 0.06,
"baseMassKg": 45,
"genome": {
"GeneMaxBodySize": 1.6,
"GeneMetabolism": 1.1,
"GeneMoveSpeed": 1.4,
"GeneBloodVolume": 1.3,
"GeneVision": 1.5,
"GeneHearing": 1.5,
"GeneSmell": 1.8,
"GeneTouch": 1.0,
"GeneCamouflage": 0.2,
"GeneScent": 0.6,
"GeneNoise": 0.5,
"GeneBrainSize": 0.5,
"GeneInsulation": 0.6,
"GeneFurColor": 0.7,
"GeneMaturityAge": 70,
"GeneLifespan": 240,
"GeneBreedingSeason": 3,
"GeneGestationDays": 35,
"GeneLitterSize": 4,
"GeneCarnivory": 1.0,
"GeneFeedingStyle": 0.9,
"GeneSociability": 0.6
}
},
{
"defName": "WildBoar",
"label": "pawn.boar",
"kind": "animal",
"texture": "things/pawn/animal/wildboar/WildBoar_east",
"body": "Quadruped",
"diet": ["plant", "meat"],
"spawnPer1000Cells": 1.0,
"baseSpeed": 26,
"visionCells": 13,
"attackRangeCells": 1.2,
"attackIntervalSec": 2.0,
"attackDamage": 9,
"attackBleed": 0.1,
"attackBloodLoss": 0.035,
"baseMassKg": 80,
"genome": {
"GeneMaxBodySize": 1.3,
"GeneMetabolism": 1.2,
"GeneMoveSpeed": 1.0,
"GeneBloodVolume": 1.2,
"GeneVision": 1.0,
"GeneHearing": 1.2,
"GeneSmell": 1.6,
"GeneTouch": 1.0,
"GeneCamouflage": 0.2,
"GeneScent": 0.7,
"GeneNoise": 0.7,
"GeneBrainSize": 0.42,
"GeneInsulation": 0.5,
"GeneFurColor": 0.85,
"GeneMaturityAge": 80,
"GeneLifespan": 280,
"GeneBreedingSeason": 0,
"GeneGestationDays": 28,
"GeneLitterSize": 5,
"GeneHerbivory": 0.4,
"GeneCarnivory": 0.2,
"GeneOmnivory": 0.85,
"GeneToxinTolerance": 0.25,
"GeneFeedingStyle": 0.55,
"GeneSociability": 0.3
}
},
{
"defName": "Chicken",
"label": "pawn.chicken",
"kind": "animal",
"texture": "things/pawn/animal/chicken/Chicken_east",
"body": "Quadruped",
"diet": ["plant"],
"spawnPer1000Cells": 1.5,
"baseSpeed": 22,
"visionCells": 10,
"baseMassKg": 2.5,
"genome": {
"GeneMaxBodySize": 0.5,
"GeneMetabolism": 1.3,
"GeneMoveSpeed": 0.9,
"GeneBloodVolume": 0.7,
"GeneVision": 1.1,
"GeneHearing": 1.0,
"GeneSmell": 0.6,
"GeneTouch": 1.0,
"GeneCamouflage": 0.2,
"GeneScent": 0.4,
"GeneNoise": 0.6,
"GeneBrainSize": 0.3,
"GeneInsulation": 0.45,
"GeneFurColor": 0.6,
"GeneMaturityAge": 50,
"GeneLifespan": 180,
"GeneBreedingSeason": 1,
"GeneGestationDays": 12,
"GeneLitterSize": 6,
"GeneHerbivory": 0.8,
"GeneToxinTolerance": 0.15,
"GeneEggLaying": 1.0,
"GeneFeedingStyle": 0.2,
"GeneSociability": 0.55
}
}
]
}
+51
View File
@@ -0,0 +1,51 @@
{
"type": "Body",
// Анатомия видов (фаза A5): дерево частей/органов. Вклады в способности (capacities) по телу
// суммируются ≈1 у здоровой особи. maxHp умножается на размер тела особи при создании.
// Источников урона пока нет — части на полном HP, способности = 1; каркас под болезни/хищников.
"defs": [
{
"defName": "Quadruped",
"parts": [
{ "name": "torso", "coverage": 0.3, "maxHp": 40, "vital": true },
{ "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "BloodPumping": 1.0 } },
{ "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } },
{ "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } },
{ "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true,
"capacities": { "BloodFiltration": 0.4 } },
{ "name": "kidneyLeft", "parent": "torso", "coverage": 0.02, "maxHp": 10,
"capacities": { "BloodFiltration": 0.3 } },
{ "name": "kidneyRight", "parent": "torso", "coverage": 0.02, "maxHp": 10,
"capacities": { "BloodFiltration": 0.3 } },
{ "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Digestion": 1.0 } },
{ "name": "head", "coverage": 0.08, "maxHp": 25 },
{ "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "Consciousness": 1.0 } },
{ "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } },
{ "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } },
{ "name": "earLeft", "parent": "head", "coverage": 0.01, "maxHp": 8,
"capacities": { "Hearing": 0.5 } },
{ "name": "earRight", "parent": "head", "coverage": 0.01, "maxHp": 8,
"capacities": { "Hearing": 0.5 } },
{ "name": "nose", "parent": "head", "coverage": 0.01, "maxHp": 8,
"capacities": { "Smell": 1.0 } },
{ "name": "skin", "coverage": 0.05, "maxHp": 20,
"capacities": { "Touch": 1.0 } },
{ "name": "jaw", "parent": "head", "coverage": 0.02, "maxHp": 10,
"capacities": { "Eating": 1.0, "Talking": 0.5 } },
{ "name": "tongue", "parent": "head", "coverage": 0.01, "maxHp": 8,
"capacities": { "Talking": 0.5 } },
{ "name": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legFrontRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legBackRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } }
]
}
]
}
+116
View File
@@ -0,0 +1,116 @@
{
"type": "Gene",
// Гены животных: организм-агностичные гены зверя (их несёт геном животного). Тело/обмен/движение/
// чувства/мозг + пол (локус) и размножение. Признаки читают системы животных (рост, ИИ, здоровье).
"defs": [
{ "defName": "GeneMaxBodySize", "parent": "BaseNumericGene", "label": "gene.bodySize",
"default": 1.0, "min": 0.1, "max": 4.0, "tags": ["animal", "body"],
"effects": { "bodySize": "value" } },
{ "defName": "GeneMetabolism", "parent": "BaseNumericGene", "label": "gene.metabolism",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "metabolism"],
"effects": { "metabolism": "value" } },
{ "defName": "GeneMoveSpeed", "parent": "BaseNumericGene", "label": "gene.moveSpeed",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "movement"],
"effects": { "moveSpeed": "value" } },
{ "defName": "GeneBloodVolume", "parent": "BaseNumericGene", "label": "gene.bloodVolume",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "health"],
"effects": { "bloodVolume": "value" } },
{ "defName": "GeneVision", "parent": "BaseNumericGene", "label": "gene.vision",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "vision": "value" } },
// --- Чувства (острота = множитель радиуса восприятия) и заметность (как видят/слышат/чуют ЭТУ особь) ---
// Острота работает вместе со способностью органа (глаза/уши/нос/кожа): ген × capacity × радиус чувства.
{ "defName": "GeneHearing", "parent": "BaseNumericGene", "label": "gene.hearing",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "hearing": "value" } },
{ "defName": "GeneSmell", "parent": "BaseNumericGene", "label": "gene.smell",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "smell": "value" } },
{ "defName": "GeneTouch", "parent": "BaseNumericGene", "label": "gene.touch",
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
"effects": { "touch": "value" } },
// Заметность цели: камуфляж снижает обнаружение зрением (1−camouflage), запах повышает обонянием,
// шум при движении повышает обнаружение слухом. См. формулу радиуса в системе восприятия.
{ "defName": "GeneCamouflage", "parent": "BaseNumericGene", "label": "gene.camouflage",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
"effects": { "camouflage": "value" } },
{ "defName": "GeneScent", "parent": "BaseNumericGene", "label": "gene.scent",
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
"effects": { "scent": "value" } },
{ "defName": "GeneNoise", "parent": "BaseNumericGene", "label": "gene.noise",
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
"effects": { "noise": "value" } },
{ "defName": "GeneBrainSize", "parent": "BaseNumericGene", "label": "gene.brainSize",
"default": 0.45, "min": 0.0, "max": 1.0, "spread": 0.03, "tags": ["animal", "brain"],
"effects": { "brainSize": "value" } },
{ "defName": "GeneInsulation", "parent": "BaseNumericGene", "label": "gene.insulation",
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "temperature"],
"effects": { "insulation": "value" } },
{ "defName": "GeneFurColor", "parent": "BaseNumericGene", "label": "gene.furColor",
"default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.06, "tags": ["animal", "morphology", "color"],
"effects": { "furHue": "value" } },
{ "defName": "GeneMaturityAge", "parent": "BaseNumericGene", "label": "gene.maturityAge",
"default": 90, "min": 1, "max": 400, "tags": ["animal", "lifecycle"],
"effects": { "maturityAge": "value" } },
// --- Диета как ГЕНЫ (хищничество/травоядство/всеядство) ---
// Каждый ген выражает свой признак [0..1]; рацион выводится порогами: ест растения, если
// max(herbivory, omnivory) ≥ порога; ест мясо (охота/падаль), если max(carnivory, omnivory) ≥ порога.
// Поэтому всеядность — отдельный ген, который включает ОБА источника пищи (генералист), а
// специалисты задаются высоким herbivory ЛИБО carnivory. См. AnimalFactory.Diet.
{ "defName": "GeneHerbivory", "parent": "BaseNumericGene", "label": "gene.herbivory",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "herbivory": "value" } },
{ "defName": "GeneCarnivory", "parent": "BaseNumericGene", "label": "gene.carnivory",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "carnivory": "value" } },
{ "defName": "GeneOmnivory", "parent": "BaseNumericGene", "label": "gene.omnivory",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "omnivory": "value" } },
// Устойчивость к растительным ядам (коэволюция): снижает дозу отравления при поедании токсичных
// растений (effectivePoison = toxicity × (1 toxinTolerance), см. AnimalActionSystem). Растёт под
// давлением ядовитого корма — другая сторона гонки вооружений с GeneToxicity растений.
{ "defName": "GeneToxinTolerance", "parent": "BaseNumericGene", "label": "gene.toxinTolerance",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "toxinTolerance": "value" } },
// Стиль питания [0..1]: 0 — пастьба (graze, частые мелкие приёмы, держит сытость высокой),
// 1 — обжорство (gorge, ищет еду лишь при сильном голоде и ест до полного — редкие крупные приёмы,
// как у хищников). Задаёт пороги начала/конца кормёжки в AnimalDecisionSystem. Эволюционирует.
{ "defName": "GeneFeedingStyle", "parent": "BaseNumericGene", "label": "gene.feedingStyle",
"default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.05, "tags": ["animal", "diet"],
"effects": { "feedingStyle": "value" } },
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
// Социальность [0..1]: стадные/стайные виды держатся вместе (когезия при блуждании) и спокойнее в
// группе («безопасность в числе» снижает дальность реакции на хищника). См. AnimalDecisionSystem.
{ "defName": "GeneSociability", "parent": "BaseNumericGene", "label": "gene.sociability",
"default": 0.3, "min": 0.0, "max": 1.0, "spread": 0.05, "tags": ["animal", "behaviour"],
"effects": { "sociability": "value" } },
// Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
// яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
// вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
{ "defName": "GeneEggLaying", "parent": "BaseNumericGene", "label": "gene.eggLaying",
"default": 0.0, "min": 0.0, "max": 1.0, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
"effects": { "eggLaying": "value" } },
// Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
"effects": { "breedingSeason": "value" } },
{ "defName": "GeneGestationDays", "parent": "BaseNumericGene", "label": "gene.gestationDays",
"default": 30, "min": 1, "max": 200, "tags": ["animal", "reproduction"],
"effects": { "gestationDays": "value" } },
{ "defName": "GeneLitterSize", "parent": "BaseNumericGene", "label": "gene.litterSize",
"default": 1, "min": 1, "max": 12, "tags": ["animal", "reproduction"],
"effects": { "litterSize": "value" } }
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"type": "Gene",
// Контент-гены (фаза G4+): плодоношение, добыча, цвет, производные и дискретные морфы.
"defs": [
{ "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" } },
// --- Защита растения (коэволюция с травоядными) ---
// Токсичность отравляет поедателя (hediff Poisoned, тем сильнее, чем ниже его устойчивость к яду);
// шипы наносят поедателю лёгкую травму; вкусность (palatability) — обратная привлекательность для
// умных травоядных (избегание появится в C2). У защиты ЕСТЬ ЦЕНА: токсичность/шипы тормозят рост
// (см. PlantGrowthSystem), иначе все растения дошли бы до максимума и коэволюция бы встала.
{ "defName": "GeneToxicity", "parent": "BaseNumericGene", "label": "gene.toxicity",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
"effects": { "toxicity": "value" } },
{ "defName": "GeneThorns", "parent": "BaseNumericGene", "label": "gene.thorns",
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
"effects": { "thorns": "value" } },
{ "defName": "GenePalatability", "parent": "BaseNumericGene", "label": "gene.palatability",
"default": 1.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
"effects": { "palatability": "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" } }
]
}
+34
View File
@@ -0,0 +1,34 @@
{
"type": "Gene",
// Гены среды: свет, температура, почва. Покрывают экологические измерения генома растения,
// которые читают системы роста/жизненного цикла.
"defs": [
{ "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" } },
// Жёсткие края диапазона роста (модель RimWorld): на сколько °C ниже/выше плато оптимума рост
// спадает до нуля. Ниже (optimal-tolerance-coldHardiness) или выше (optimal+tolerance+heatHardiness)
// растение дормантно и копит температурный стресс. Асимметрично: вид может терпеть жару лучше холода.
{ "defName": "GeneColdHardiness", "parent": "BaseNumericGene", "label": "gene.coldHardiness",
"default": 8, "min": 0, "max": 40, "tags": ["environment", "temperature"],
"effects": { "coldHardiness": "value" } },
{ "defName": "GeneHeatHardiness", "parent": "BaseNumericGene", "label": "gene.heatHardiness",
"default": 10, "min": 0, "max": 40, "tags": ["environment", "temperature"],
"effects": { "heatHardiness": "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" } }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"type": "Gene",
// Гены роста и размножения: бодрость, продолжительность жизни, расселение, репродукция.
"defs": [
{ "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" } }
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"type": "Gene",
// Общие (абстрактные) гены: база наследования для конкретных генов. Каждый ген задаёт, как
// генерируются/мутируют аллели и как ген вкладывается в признаки (effects: имя признака →
// формула; value — выраженное значение гена). Конкретные гены лежат рядом по подтемам.
"defs": [
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 }
]
}
+60
View File
@@ -0,0 +1,60 @@
{
"type": "Hediff",
// Хедифы — состояния организма (фаза A4: лёгкий каркас). Гон (Rut) навешивается сезонно на взрослых
// и поднимает половое влечение. Раны/болезни/возрастные эффекты со стадиями придут в A5/A6.
"defs": [
{ "defName": "Rut", "label": "hediff.rut" },
// Болезнь (фаза A6): прогрессирует, параллельно растёт иммунитет (гонка). Обычно иммунитет
// успевает победить (~3 дня), но больной зверь медленнее и слабее; слабые/невезучие гибнут.
{
"defName": "Fever", "label": "hediff.fever",
"initialSeverity": 0.05, "severityPerDay": 0.25, "immunityPerDay": 0.32,
"lethalSeverity": 1.0, "pain": 0.3, "ambientPerDay": 0.03,
"capMods": { "Moving": 0.6, "Consciousness": 0.85, "Digestion": 0.8 }
},
// Рана/кровотечение (предатор-кластер): первый травматический урон. Не прогрессирует сама
// (severityPerDay 0), но immunityPerDay её «сворачивает» (рана заживает ~за 2 дня). Пока активна —
// теряется кровь (bloodLossPerDay × тяжесть); кровь на нуле = смерть. Боль и капмоды от тяжёлых ран.
// Каждый укус усиливает тяжесть и сбрасывает заживление (см. HealthState.Intensify).
{
"defName": "Bleeding", "label": "hediff.bleeding",
"initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8,
"pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 }
},
// Отравление растительным ядом (коэволюция): сама не прогрессирует (severityPerDay 0) — тяжесть
// копится при поедании токсичных растений (HealthState.Intensify в AnimalActionSystem). Иммунитет
// её рассасывает (~1.5 дня), но если зверь ест яд быстрее, чем выводит, тяжесть доходит до 1 → смерть.
// Это селективное давление: ядовитый корм опасен для травоядных с низкой устойчивостью.
{
"defName": "Poisoned", "label": "hediff.poisoned",
"initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2,
"capMods": { "Moving": 0.85, "Consciousness": 0.9, "Digestion": 0.7 }
},
// Терморегуляция (как Poisoned-паттерн): сами не прогрессируют — тяжесть копится, пока зверю
// холодно/жарко (см. AnimalHealthSystem, зависит от GeneInsulation + размера тела), а в комфорте
// immunityPerDay их рассасывает. Тяжесть 1 = смерть от переохлаждения/перегрева.
{
"defName": "Hypothermia", "label": "hediff.hypothermia",
"initialSeverity": 0.0, "immunityPerDay": 0.5, "lethalSeverity": 1.0, "pain": 0.15,
"capMods": { "Moving": 0.7, "Consciousness": 0.8 }
},
{
"defName": "Heatstroke", "label": "hediff.heatstroke",
"initialSeverity": 0.0, "immunityPerDay": 0.5, "lethalSeverity": 1.0, "pain": 0.15,
"capMods": { "Moving": 0.75, "Consciousness": 0.8 }
},
// Заражение раны (предатор/шипы → кровотечение → инфекция): прогрессирует, иммунитет (× фильтрация
// крови) душит. Слабая фильтрация/много ран → выше риск и хуже исход.
{
"defName": "Infection", "label": "hediff.infection",
"initialSeverity": 0.08, "severityPerDay": 0.2, "immunityPerDay": 0.28,
"lethalSeverity": 1.0, "pain": 0.25,
"capMods": { "Moving": 0.8, "Consciousness": 0.85, "BloodFiltration": 0.8 }
}
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"type": "Need",
// Нужды как данные (фаза A2, рефактор расширяемости): добавить нужду виду = добавить запись здесь.
// kind=deplete убывает и утоляется действием; kind=drive растёт под хедифом (гон) и сбрасывается им.
// action — id поведения-исполнителя (реестр в коде). minBrain — гейт интеллектом (активен с A7):
// нужда, чей minBrain выше эффективного интеллекта особи (brainSize × Consciousness), в выбор ИИ не
// входит. Пороги — тиры мозга: 0.0 голод-рефлекс, 0.25 жажда (низший позвоночный), 0.40 сон/секс
// (зверь). У оленя мозг ~0.45: здоров — весь набор; повреждение мозга/болезнь роняют сознание →
// эффективный интеллект падает → первыми отключаются сон/спаривание (обратная связь A7).
"defs": [
{ "defName": "Hunger", "label": "need.hunger", "kind": "deplete", "action": "Eat",
"decayPerDay": 0.55, "feedPerDay": 4, "lethal": true, "minBrain": 0.0 },
{ "defName": "Thirst", "label": "need.thirst", "kind": "deplete", "action": "Drink",
"decayPerDay": 0.9, "feedPerDay": 8, "lethal": true, "minBrain": 0.25 },
{ "defName": "Rest", "label": "need.rest", "kind": "deplete", "action": "Sleep",
"decayPerDay": 0.7, "feedPerDay": 3, "lethal": false, "minBrain": 0.40 },
{ "defName": "Mating", "label": "need.mating", "kind": "drive", "action": "Mate",
"decayPerDay": 1.0, "risePerDay": 1.2, "risesUnder": "Rut", "minBrain": 0.40 }
]
}
@@ -1,14 +1,7 @@
{ {
"type": "Pawn", "type": "Pawn",
// Животные.
"defs": [ "defs": [
{ "defName": "BaseBeing", "abstract": true, "kind": "being", "sizeCells": 1.1 },
{ "defName": "BeingMale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Male_south" },
{ "defName": "BeingFemale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Female_south" },
{ "defName": "BeingThin", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Thin_south" },
{ "defName": "BeingFat", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Fat_south" },
{ "defName": "BeingHulk", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Hulk_south" },
{ "defName": "BaseAnimal", "abstract": true, "kind": "animal" },
{ "defName": "Bear", "parent": "BaseAnimal", "label": "pawn.bear", "texture": "things/pawn/animal/bear/Bear_east", "sizeCells": 1.7 }, { "defName": "Bear", "parent": "BaseAnimal", "label": "pawn.bear", "texture": "things/pawn/animal/bear/Bear_east", "sizeCells": 1.7 },
{ "defName": "DeerDoe", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerFemale_east", "sizeCells": 1.4 }, { "defName": "DeerDoe", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerFemale_east", "sizeCells": 1.4 },
{ "defName": "DeerBuck", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerMale_east", "sizeCells": 1.6 }, { "defName": "DeerBuck", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerMale_east", "sizeCells": 1.6 },
+11
View File
@@ -0,0 +1,11 @@
{
"type": "Pawn",
// Жители: варианты тел.
"defs": [
{ "defName": "BeingMale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Male_south" },
{ "defName": "BeingFemale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Female_south" },
{ "defName": "BeingThin", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Thin_south" },
{ "defName": "BeingFat", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Fat_south" },
{ "defName": "BeingHulk", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Hulk_south" }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"type": "Pawn",
// Общие (абстрактные) существа: база для жителей и животных.
"defs": [
{ "defName": "BaseBeing", "abstract": true, "kind": "being", "sizeCells": 1.1 },
{ "defName": "BaseAnimal", "abstract": true, "kind": "animal" }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"type": "Plant",
// Ягодный почвопокров луга (еда).
"defs": [
{ "defName": "Strawberry", "parent": "BaseBerryBush", "label": "plant.strawberry", "sizeCells": 1.0, "texture": "things/plant/strawberryplant/StrawberryPlant" },
{ "defName": "Raspberry", "parent": "BaseBerryBush", "label": "plant.raspberry", "texture": "things/plant/raspberryplant/raspberrybusha" }
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"type": "Plant",
// Кусты: ягодный подлесок. Патч даёт им древесину при сборе (веточки) — см. Patches/Patches.json.
"defs": [
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"type": "Plant",
// Пустыня: кактусы и агава на песке. Большинство без плодов; сагуаро/агава плодоносят.
"defs": [
{ "defName": "SaguaroCactusA", "parent": "BaseCactus", "label": "plant.saguaro", "sizeCells": 2.2, "trunkRadiusCells": 0.18,
"fruitProduct": "ProductBerry", "texture": "things/plant/saguarocactus/SaguaroCactusA" },
{ "defName": "SaguaroCactusB", "parent": "BaseCactus", "label": "plant.saguaro", "sizeCells": 2.2, "trunkRadiusCells": 0.18,
"fruitProduct": "ProductBerry", "texture": "things/plant/saguarocactus/SaguaroCactusB" },
{ "defName": "PebbleCactusA", "parent": "BaseCactus", "sizeCells": 1.0, "texture": "things/plant/pebblecactus/PebbleCactusA" },
{ "defName": "PebbleCactusB", "parent": "BaseCactus", "sizeCells": 1.0, "texture": "things/plant/pebblecactus/PebbleCactusB" },
{ "defName": "PincushionCactusA", "parent": "BaseCactus", "sizeCells": 0.9, "texture": "things/plant/pincushioncactus/PincushionCactusA" },
{ "defName": "AgaveA", "parent": "BaseCactus", "label": "plant.agave", "sizeCells": 1.3, "fruitProduct": "ProductBerry", "texture": "things/plant/agave/AgaveA" },
{ "defName": "AgaveB", "parent": "BaseCactus", "label": "plant.agave", "sizeCells": 1.3, "fruitProduct": "ProductBerry", "texture": "things/plant/agave/AgaveB" }
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"type": "Plant",
// Луг: декоративные цветы с быстрым расселением.
"defs": [
{ "defName": "DandelionA", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/Dandelion" },
{ "defName": "DandelionB", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/DandelionB" },
{ "defName": "DandelionC", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/DandelionC" },
{ "defName": "DaylilyA", "parent": "BaseFlower", "label": "plant.daylily", "texture": "things/plant/daylily/DaylilyA" },
{ "defName": "DaylilyB", "parent": "BaseFlower", "label": "plant.daylily", "texture": "things/plant/daylily/DaylilyB" },
{ "defName": "RoseA", "parent": "BaseFlower", "label": "plant.rose", "sizeCells": 1.1, "texture": "things/plant/rose/RoseA" }
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"type": "Plant",
// Трава: быстрый почвопокров, основной скаттер лугов и леса.
"defs": [
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5,
"baseMassKg": 0.4,
"harvestProduct": "ProductGrass",
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8,
"coldHardiness": 9, "heatHardiness": 6,
"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.5, "label": "plant.stage.mature" }
] }
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"type": "Plant",
// Грибы: подлесок леса, растут в тени крон.
"defs": [
{ "defName": "GlowstoolA", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/glowstool/GlowstoolA" },
{ "defName": "GlowstoolB", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/glowstool/GlowstoolB" },
{ "defName": "TimbershroomA", "parent": "BaseMushroom", "sizeCells": 1.1, "texture": "things/plant/timbershroom/TimbershroomA" },
{ "defName": "TimbershroomB", "parent": "BaseMushroom", "sizeCells": 1.1, "texture": "things/plant/timbershroom/TimbershroomB" },
{ "defName": "NutrifungusA", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/nutrifungus/NutrifungusA" }
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"type": "Plant",
// Деревья: умеренные (BaseTree), тёплые (BaseWarmTree) и холодные (BaseColdTree) ниши.
"defs": [
{ "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": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
{ "defName": "TreeMapleA", "parent": "BaseTree", "label": "plant.maple", "texture": "things/plant/treemaple/TreeMapleA" },
{ "defName": "TreeMapleB", "parent": "BaseTree", "label": "plant.maple", "texture": "things/plant/treemaple/TreeMapleB" },
{ "defName": "TreePoplarA", "parent": "BaseTree", "label": "plant.poplar", "texture": "things/plant/treepoplar/TreePoplarA" },
{ "defName": "TreeTeakA", "parent": "BaseWarmTree", "label": "plant.teak", "texture": "things/plant/treeteak/TreeTeakA" },
{ "defName": "TreeTeakB", "parent": "BaseWarmTree", "label": "plant.teak", "texture": "things/plant/treeteak/TreeTeakB" },
{ "defName": "TreeCypressA", "parent": "BaseColdTree", "label": "plant.cypress", "texture": "things/plant/treecypress/TreeCypressA" },
{ "defName": "TreeCypressB", "parent": "BaseColdTree", "label": "plant.cypress", "texture": "things/plant/treecypress/TreeCypressB" }
]
}
+126
View File
@@ -0,0 +1,126 @@
{
"type": "Plant",
// Общие (абстрактные) растения: базовые геномы по типам ниш. Конкретные виды наследуют их
// через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …).
"defs": [
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"baseMassKg": 450,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10,
"coldHardiness": 14, "heatHardiness": 12,
"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" }
] },
// Деревья тёплой и холодной температурных ниш (showcase температурных генов).
{ "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"baseMassKg": 420,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9,
"coldHardiness": 8, "heatHardiness": 16,
"optimalFertility": 1.5, "fertilityTolerance": 0.9, "vigor": 1.1,
"lifespan": 150, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.22 },
"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": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26,
"baseMassKg": 500,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12,
"coldHardiness": 18, "heatHardiness": 8,
"optimalFertility": 1.2, "fertilityTolerance": 1.0, "vigor": 0.9,
"lifespan": 220, "dispersalRange": 2, "reproduceInterval": 16, "selfPollination": 0.2,
"mutationRate": 0.05, "variantChance": 0.12, "spread": 0.08,
"fruitYield": 4, "fruitSeason": 2, "harvestAmount": 12, "leafHue": 0.45 },
"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": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
"baseMassKg": 6,
"fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9,
"coldHardiness": 10, "heatHardiness": 8,
"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": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0,
"baseMassKg": 35,
"harvestProduct": "ProductFiber",
"genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8,
"coldHardiness": 20, "heatHardiness": 18,
"optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5,
"lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
"thorns": 0.6, "palatability": 0.5 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" }
] },
// Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно
// (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение.
{ "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0,
"baseMassKg": 0.3,
"harvestProduct": "ProductMushroom",
"genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11,
"coldHardiness": 12, "heatHardiness": 8,
"optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2,
"lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9,
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
"fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
"toxicity": 0.5, "palatability": 0.4 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" }
] },
// Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда).
{ "defName": "BaseFlower", "abstract": true, "label": "plant.flower", "sizeCells": 1.0, "trunkRadiusCells": 0,
"baseMassKg": 0.15,
"genome": { "optimalLight": 0.9, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 9,
"coldHardiness": 8, "heatHardiness": 8,
"optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4,
"lifespan": 18, "dispersalRange": 4, "reproduceInterval": 3, "selfPollination": 0.85,
"mutationRate": 0.08, "variantChance": 0.3, "spread": 0.1,
"fruitYield": 0, "fruitSeason": 0, "harvestAmount": 0, "leafHue": 0.2 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" }
] },
{ "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0,
"baseMassKg": 5,
"fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9,
"coldHardiness": 10, "heatHardiness": 7,
"optimalFertility": 1.2, "fertilityTolerance": 1.0, "vigor": 1.1,
"lifespan": 40, "dispersalRange": 2, "reproduceInterval": 6, "selfPollination": 0.6,
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
"fruitYield": 4, "fruitSeason": 2, "harvestAmount": 0, "leafHue": 0.33 },
"stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 3, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" }
] }
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"type": "Product",
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
// количество задаётся генами (harvestAmount / fruitYield).
"defs": [
{ "defName": "ProductWood", "label": "product.wood", "kind": "material", "massKg": 1.0 },
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material", "massKg": 0.1 },
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material", "massKg": 0.2 },
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food", "massKg": 0.05 },
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food", "massKg": 0.05 },
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food", "massKg": 0.1 },
// Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже.
{ "defName": "ProductMeat", "label": "product.meat", "kind": "food", "massKg": 0.5 },
{ "defName": "ProductBone", "label": "product.bone", "kind": "material", "massKg": 0.3 },
{ "defName": "ProductLeather", "label": "product.leather", "kind": "material", "massKg": 0.4 }
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"type": "Skill",
// Навыки как данные: уровень особи [0..1] растёт от практики (XP при соответствующем действии,
// ускоряется страстью) и медленно угасает к floorLevel. Эффект навыка — код по его id (добыча→выпас,
// охота→урон, уклонение→бегство). Сейчас навыки у животных; те же дефы пригодятся будущему человеку.
"defs": [
{ "defName": "Foraging", "label": "skill.foraging", "category": "survival",
"learnRate": 0.6, "decayPerDay": 0.015, "floorLevel": 0.0 },
{ "defName": "Hunting", "label": "skill.hunting", "category": "survival",
"learnRate": 0.5, "decayPerDay": 0.02, "floorLevel": 0.0 },
{ "defName": "Evasion", "label": "skill.evasion", "category": "survival",
"learnRate": 0.7, "decayPerDay": 0.02, "floorLevel": 0.0 }
]
}
+33
View File
@@ -0,0 +1,33 @@
{
"type": "Terrain",
"defs": [
{ "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": "Sand", "label": "terrain.sand", "maxHeight": 0.46, "color": [237, 201, 120],
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4,
"scatter": [
{ "chance": 0.05, "options": ["SaguaroCactusA", "SaguaroCactusB"] },
{ "chance": 0.10, "options": ["PebbleCactusA", "PebbleCactusB", "PincushionCactusA"] },
{ "chance": 0.04, "options": ["AgaveA", "AgaveB"] }
] },
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.72, "color": [90, 160, 70],
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
"scatter": [
{ "chance": 1.0, "options": ["GrassA"] },
{ "chance": 0.5, "options": ["GrassA"] },
{ "chance": 0.22, "options": ["BushA", "BushB", "BushC"] },
{ "chance": 0.20, "options": ["Strawberry", "Raspberry"] },
{ "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] }
] },
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 1.01, "color": [44, 110, 50],
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
"scatter": [
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA",
"TreeMapleA", "TreeMapleB", "TreePoplarA", "TreeCypressA", "TreeCypressB", "TreeTeakA", "TreeTeakB"] },
{ "chance": 0.6, "options": ["GrassA"] },
{ "chance": 0.16, "options": ["BushA", "BushB", "BushC"] },
{ "chance": 0.14, "options": ["Strawberry", "Raspberry"] },
{ "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] }
] }
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"type": "Thought",
// Мысли настроения (фаза A8): событие вешает на зверя временный сдвиг настроения, затухающий за
// durationDays. Триггер события — код (реестр AnimalThoughts), сила/длительность/метка — данные.
// Настроение есть только у тира 4+ (мозг ≥ 0.40); непрерывный фон (голод/жажда/усталость/боль)
// считается отдельно, мысли — поверх него. Негативные событийные мысли (потеря детёныша) придут с
// родительской опекой (горизонт) — пока горе идёт через непрерывный фон.
"defs": [
{ "defName": "GaveBirth", "label": "thought.gaveBirth", "moodOffset": 0.25, "durationDays": 3 },
{ "defName": "Mated", "label": "thought.mated", "moodOffset": 0.15, "durationDays": 1.5 },
{ "defName": "QuenchedThirst", "label": "thought.quenchedThirst", "moodOffset": 0.1, "durationDays": 0.5 }
]
}
@@ -1,8 +1,8 @@
{ {
"type": "WorldPreset", "type": "WorldPreset",
"defs": [ "defs": [
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 80, "height": 50, "population": 40 }, { "defName": "Small", "label": "preset.small", "order": 0, "width": 128, "height": 80, "population": 60 },
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 120, "height": 70, "population": 80 }, { "defName": "Medium", "label": "preset.medium", "order": 1, "width": 200, "height": 120, "population": 110 },
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 180, "height": 110, "population": 140 } { "defName": "Large", "label": "preset.large", "order": 2, "width": 300, "height": 180, "population": 180 }
] ]
} }
-73
View File
@@ -1,73 +0,0 @@
{
"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" } }
]
}
-48
View File
@@ -1,48 +0,0 @@
{
"type": "Plant",
"defs": [
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
"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": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
{ "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": "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,
"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": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
]
}
-11
View File
@@ -1,11 +0,0 @@
{
"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" }
]
}
-22
View File
@@ -1,22 +0,0 @@
{
"type": "Terrain",
"defs": [
{ "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": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120],
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4 },
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70],
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
"scatter": [
{ "chance": 0.35, "options": ["GrassA"] },
{ "chance": 0.07, "options": ["BushA", "BushB", "BushC"] }
] },
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
"scatter": [
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
] },
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
]
}
+160 -4
View File
@@ -6,7 +6,91 @@
"season.summer": "summer", "season.summer": "summer",
"season.autumn": "autumn", "season.autumn": "autumn",
"season.winter": "winter", "season.winter": "winter",
"hud.controls": "WASD — camera, wheel — zoom, ` — console, F1 — inspector", "hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
"hud.inspector": "F1 — inspector",
"hud.spawner": "F9 — spawner",
"inspect.tab.overview": "Overview",
"inspect.tab.genes": "Genes",
"inspect.tab.products": "Products",
"inspect.tab.needs": "Needs",
"inspect.tab.skills": "Skills",
"inspect.tab.health": "Health",
"inspect.tab.mood": "Mood",
"inspect.stage": "Stage: {0} ({1}/{2})",
"inspect.growing": "Growth: {0:0}% to next stage",
"inspect.mature": "Fully grown",
"inspect.age": "Age: {0:0.0} d · lives to {1:0}",
"inspect.mass": "Weight: {0:0.#} kg · carries up to {1:0.#} kg",
"inspect.plantmass": "Weight: {0:0.##} kg",
"inspect.state": "State: {0}",
"inspect.state.growing": "growing",
"inspect.state.mature": "mature",
"inspect.state.dormantcold": "dormant — too cold",
"inspect.state.dormanthot": "dormant — too hot",
"inspect.stress": "Stress: {0:0.0} d to death",
"inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
"inspect.genes": "Genes",
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d",
"inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}",
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
"inspect.gene.defense": "Defense: toxin {0:0.00} · thorns {1:0.00}",
"inspect.gene.variant": "Morph: recessive variant",
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
"inspect.barren": "Bears no fruit",
"inspect.sex.male": "male",
"inspect.sex.female": "female",
"inspect.animal.sex": "Sex: {0} · generation {1}",
"inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth",
"inspect.needline": "{0}: {1:0}%",
"inspect.skillline": "{0}: {1:0}% {2}",
"skill.foraging": "foraging",
"skill.hunting": "hunting",
"skill.evasion": "evasion",
"inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%",
"inspect.health.vitals": "Vitals",
"inspect.vital.blood": "blood",
"inspect.vital.pain": "pain",
"inspect.health.caps": "Capacities",
"inspect.health.cap": "{0}: {1:0}%",
"inspect.health.hediffs": "Conditions",
"inspect.health.hediffline": "{0}: severity {1:0}%",
"inspect.health.none": "Healthy, no wounds or illness",
"inspect.mood.value": "Mood: {0:0}%",
"inspect.mood.none": "No mood (primitive brain)",
"inspect.mood.thoughts": "Thoughts",
"inspect.mood.thoughtline": "{0}: {1}",
"inspect.mood.calm": "Calm, nothing on its mind",
"animalstage.baby": "baby",
"animalstage.juvenile": "juvenile",
"animalstage.adult": "adult",
"animalstage.senior": "senior",
"need.hunger": "hunger",
"need.thirst": "thirst",
"need.rest": "rest",
"need.mating": "mating",
"cap.consciousness": "consciousness",
"cap.moving": "moving",
"cap.sight": "sight",
"cap.hearing": "hearing",
"cap.smell": "smell",
"cap.touch": "touch",
"cap.talking": "talking",
"cap.eating": "eating",
"cap.breathing": "breathing",
"cap.bloodpumping": "blood pumping",
"cap.bloodfiltration": "blood filtration",
"cap.digestion": "digestion",
"hediff.rut": "rut",
"hediff.fever": "fever",
"hediff.bleeding": "bleeding",
"hediff.poisoned": "poisoned",
"hediff.hypothermia": "hypothermia",
"hediff.heatstroke": "heatstroke",
"hediff.infection": "infection",
"inspect.hint": "LMB — select · RMB/Esc — clear",
"hud.paused": "PAUSED", "hud.paused": "PAUSED",
"menu.title": "LittleSim", "menu.title": "LittleSim",
"menu.subtitle": "a god-game: minimal graphics, deep simulation", "menu.subtitle": "a god-game: minimal graphics, deep simulation",
@@ -15,6 +99,7 @@
"menu.settings": "Settings", "menu.settings": "Settings",
"menu.credits": "Credits", "menu.credits": "Credits",
"menu.quit": "Quit", "menu.quit": "Quit",
"loading.world": "Generating world",
"newworld.title": "New World", "newworld.title": "New World",
"newworld.name": "Name", "newworld.name": "Name",
"newworld.defaultname": "New World", "newworld.defaultname": "New World",
@@ -35,6 +120,19 @@
"settings.vsync": "Vertical sync", "settings.vsync": "Vertical sync",
"settings.resolution": "Resolution", "settings.resolution": "Resolution",
"settings.volume": "Volume", "settings.volume": "Volume",
"settings.uiscale": "UI scale",
"settings.devmode": "Developer mode",
"settings.on": "On",
"settings.off": "Off",
"dev.title": "Dev spawner",
"dev.hint": "Pick an entry · F9 to hide",
"dev.armed": "In hand: {0} · LMB to spawn, RMB to clear",
"dev.tools": "Tools",
"dev.damage": "Injure selected",
"dev.kill": "Kill selected",
"dev.infect": "Infect selected",
"dev.animals": "Animals",
"dev.plants": "Plants",
"settings.apply": "Apply", "settings.apply": "Apply",
"settings.back": "Back", "settings.back": "Back",
"pause.title": "Paused", "pause.title": "Paused",
@@ -44,7 +142,7 @@
"pause.mainmenu": "Main menu", "pause.mainmenu": "Main menu",
"pause.quit": "Quit", "pause.quit": "Quit",
"pause.saved": "Saved: {0}", "pause.saved": "Saved: {0}",
"speed.pause": "", "speed.pause": "Pause",
"credits.title": "Credits", "credits.title": "Credits",
"credits.author": "mrleo1nid", "credits.author": "mrleo1nid",
"credits.role": "Concept, code, mrgameeng engine", "credits.role": "Concept, code, mrgameeng engine",
@@ -57,20 +155,36 @@
"terrain.sand": "sand", "terrain.sand": "sand",
"terrain.grass": "grass", "terrain.grass": "grass",
"terrain.forest": "forest", "terrain.forest": "forest",
"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.maple": "maple",
"plant.poplar": "poplar",
"plant.teak": "teak",
"plant.cypress": "cypress",
"plant.cactus": "cactus",
"plant.saguaro": "saguaro",
"plant.agave": "agave",
"plant.mushroom": "mushroom",
"plant.flower": "flower",
"plant.dandelion": "dandelion",
"plant.daylily": "daylily",
"plant.rose": "rose",
"plant.berrybush": "berry bush",
"plant.strawberry": "strawberry",
"plant.raspberry": "raspberry",
"plant.stage.seedling": "seedling", "plant.stage.seedling": "seedling",
"plant.stage.sprout": "sprout", "plant.stage.sprout": "sprout",
"plant.stage.sapling": "sapling", "plant.stage.sapling": "sapling",
"plant.stage.mature": "mature", "plant.stage.mature": "mature",
"product.wood": "wood", "product.wood": "wood",
"product.grass": "grass", "product.grass": "grass",
"product.fiber": "fiber",
"product.berry": "berries", "product.berry": "berries",
"product.acorn": "acorn", "product.acorn": "acorn",
"product.mushroom": "mushrooms",
"pawn.being": "being", "pawn.being": "being",
"pawn.bear": "bear", "pawn.bear": "bear",
"pawn.deer": "deer", "pawn.deer": "deer",
@@ -78,11 +192,53 @@
"pawn.hare": "hare", "pawn.hare": "hare",
"pawn.boar": "boar", "pawn.boar": "boar",
"pawn.wolf": "wolf", "pawn.wolf": "wolf",
"pawn.chicken": "chicken",
"pawn.muffalo": "muffalo", "pawn.muffalo": "muffalo",
"pawn.squirrel": "squirrel", "pawn.squirrel": "squirrel",
"thought.gaveBirth": "gave birth",
"thought.mated": "mated",
"thought.quenchedThirst": "drank its fill",
"gene.feedingStyle": "feeding style",
"inspect.animal.feeding": "Feeding: {0}",
"feeding.gorge": "gorger (rare, to the full)",
"feeding.graze": "grazer (often, little by little)",
"feeding.mixed": "moderate",
"inspect.tab.log": "Log",
"log.empty": "No history yet",
"log.stamp": "D{0} {1:00}:{2:00}",
"hud.events": "Events",
"hud.perf": "Perf",
"events.title": "Event log",
"events.empty": "Nothing has happened yet",
"events.hint": "Double-click — jump to the creature",
"perf.title": "Performance",
"perf.fps": "{0} FPS · {1:0.0} ms/frame",
"perf.root": "{0}: {1:0.000} ms · systems {2}",
"perf.systemline": "{0} {1:0.000} ms {2:0}%",
"perf.hint": "Refreshes ~5/s · costliest systems shown",
"event.mated": "Pair: {0} + {1}",
"event.pregnant": "{0}: pregnant",
"event.birth": "{0}: birth ×{1}",
"event.hatch": "{0}: hatched ×{1}",
"event.hunt": "Hunt: {0} → {1}",
"event.injured": "Wound: {0} → {1}",
"event.struck": "Hit: {0} → {1} ({2:0})",
"event.tookhit": "Hit: {1} → {0} ({2:0})",
"event.killed": "Kill: {0} → {1}",
"event.ate": "{0} is feeding",
"event.drank": "{0} is drinking",
"event.rest": "{0} is resting",
"event.dig": "{0} digs for food",
"event.wallow": "{0} wallows in mud",
"event.fled": "{0} is fleeing",
"event.grew": "{0}: new stage — {1}",
"event.death.starve": "Death: {0} (hunger/thirst)",
"event.death.age": "Death: {0} (old age)",
"event.death.disease": "Death: {0} (disease)",
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu", "net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
"net.connecting": "connecting…", "net.connecting": "connecting…",
"net.connected": "connected", "net.connected": "connected",
"net.failed": "connection failed", "net.failed": "connection failed",
"net.lost": "connection lost" "net.lost": "connection lost",
"net.reconnecting": "reconnecting…"
} }
+160 -4
View File
@@ -6,7 +6,91 @@
"season.summer": "лето", "season.summer": "лето",
"season.autumn": "осень", "season.autumn": "осень",
"season.winter": "зима", "season.winter": "зима",
"hud.controls": "WASD — камера, колесо — зум, ` — консоль, F1 — инспектор", "hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
"hud.inspector": "F1 — инспектор",
"hud.spawner": "F9 — спавнер",
"inspect.tab.overview": "Обзор",
"inspect.tab.genes": "Гены",
"inspect.tab.products": "Продукты",
"inspect.tab.needs": "Нужды",
"inspect.tab.skills": "Навыки",
"inspect.tab.health": "Здоровье",
"inspect.tab.mood": "Настроение",
"inspect.stage": "Стадия: {0} ({1}/{2})",
"inspect.growing": "Рост: {0:0}% до следующей стадии",
"inspect.mature": "Полностью выросло",
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
"inspect.mass": "Вес: {0:0.#} кг · несёт до {1:0.#} кг",
"inspect.plantmass": "Вес: {0:0.##} кг",
"inspect.state": "Состояние: {0}",
"inspect.state.growing": "растёт",
"inspect.state.mature": "созрело",
"inspect.state.dormantcold": "покой — слишком холодно",
"inspect.state.dormanthot": "покой — слишком жарко",
"inspect.stress": "Стресс: {0:0.0} дн до гибели",
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
"inspect.genes": "Гены",
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
"inspect.gene.defense": "Защита: яд {0:0.00} · шипы {1:0.00}",
"inspect.gene.variant": "Морфа: рецессивный вариант",
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
"inspect.barren": "Не плодоносит",
"inspect.sex.male": "самец",
"inspect.sex.female": "самка",
"inspect.animal.sex": "Пол: {0} · поколение {1}",
"inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов",
"inspect.needline": "{0}: {1:0}%",
"inspect.skillline": "{0}: {1:0}% {2}",
"skill.foraging": "добыча корма",
"skill.hunting": "охота",
"skill.evasion": "уклонение",
"inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%",
"inspect.health.vitals": "Жизненные показатели",
"inspect.vital.blood": "кровь",
"inspect.vital.pain": "боль",
"inspect.health.caps": "Способности",
"inspect.health.cap": "{0}: {1:0}%",
"inspect.health.hediffs": "Состояния",
"inspect.health.hediffline": "{0}: тяжесть {1:0}%",
"inspect.health.none": "Здоров, ран и болезней нет",
"inspect.mood.value": "Настроение: {0:0}%",
"inspect.mood.none": "Настроения нет (примитивный мозг)",
"inspect.mood.thoughts": "Мысли",
"inspect.mood.thoughtline": "{0}: {1}",
"inspect.mood.calm": "Спокоен, особых мыслей нет",
"animalstage.baby": "детёныш",
"animalstage.juvenile": "подросток",
"animalstage.adult": "взрослый",
"animalstage.senior": "старый",
"need.hunger": "голод",
"need.thirst": "жажда",
"need.rest": "отдых",
"need.mating": "влечение",
"cap.consciousness": "сознание",
"cap.moving": "движение",
"cap.sight": "зрение",
"cap.hearing": "слух",
"cap.smell": "обоняние",
"cap.touch": "осязание",
"cap.talking": "речь",
"cap.eating": "питание",
"cap.breathing": "дыхание",
"cap.bloodpumping": "кровоснабжение",
"cap.bloodfiltration": "фильтрация крови",
"cap.digestion": "пищеварение",
"hediff.rut": "гон",
"hediff.fever": "лихорадка",
"hediff.bleeding": "кровотечение",
"hediff.poisoned": "отравление",
"hediff.hypothermia": "переохлаждение",
"hediff.heatstroke": "тепловой удар",
"hediff.infection": "заражение",
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
"hud.paused": "ПАУЗА", "hud.paused": "ПАУЗА",
"menu.title": "LittleSim", "menu.title": "LittleSim",
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции", "menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
@@ -15,6 +99,7 @@
"menu.settings": "Настройки", "menu.settings": "Настройки",
"menu.credits": "Авторы", "menu.credits": "Авторы",
"menu.quit": "Выход", "menu.quit": "Выход",
"loading.world": "Создание мира",
"newworld.title": "Новый мир", "newworld.title": "Новый мир",
"newworld.name": "Название", "newworld.name": "Название",
"newworld.defaultname": "Новый мир", "newworld.defaultname": "Новый мир",
@@ -35,6 +120,19 @@
"settings.vsync": "Вертикальная синхронизация", "settings.vsync": "Вертикальная синхронизация",
"settings.resolution": "Разрешение", "settings.resolution": "Разрешение",
"settings.volume": "Громкость", "settings.volume": "Громкость",
"settings.uiscale": "Масштаб UI",
"settings.devmode": "Режим разработчика",
"settings.on": "Вкл",
"settings.off": "Откл",
"dev.title": "Дев-спавнер",
"dev.hint": "Выбери пункт · F9 — скрыть",
"dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
"dev.tools": "Инструменты",
"dev.damage": "Ранить выбранного",
"dev.kill": "Убить выбранного",
"dev.infect": "Заразить выбранного",
"dev.animals": "Животные",
"dev.plants": "Растения",
"settings.apply": "Применить", "settings.apply": "Применить",
"settings.back": "Назад", "settings.back": "Назад",
"pause.title": "Пауза", "pause.title": "Пауза",
@@ -44,7 +142,7 @@
"pause.mainmenu": "Главное меню", "pause.mainmenu": "Главное меню",
"pause.quit": "Выход", "pause.quit": "Выход",
"pause.saved": "Сохранено: {0}", "pause.saved": "Сохранено: {0}",
"speed.pause": "", "speed.pause": "Пауза",
"credits.title": "Авторы", "credits.title": "Авторы",
"credits.author": "mrleo1nid", "credits.author": "mrleo1nid",
"credits.role": "Идея, код, движок mrgameeng", "credits.role": "Идея, код, движок mrgameeng",
@@ -57,20 +155,36 @@
"terrain.sand": "песок", "terrain.sand": "песок",
"terrain.grass": "трава", "terrain.grass": "трава",
"terrain.forest": "лес", "terrain.forest": "лес",
"terrain.mountain": "горы",
"plant.oak": "дуб", "plant.oak": "дуб",
"plant.birch": "берёза", "plant.birch": "берёза",
"plant.pine": "сосна", "plant.pine": "сосна",
"plant.grass": "пучок травы", "plant.grass": "пучок травы",
"plant.bush": "куст", "plant.bush": "куст",
"plant.maple": "клён",
"plant.poplar": "тополь",
"plant.teak": "тик",
"plant.cypress": "кипарис",
"plant.cactus": "кактус",
"plant.saguaro": "сагуаро",
"plant.agave": "агава",
"plant.mushroom": "гриб",
"plant.flower": "цветок",
"plant.dandelion": "одуванчик",
"plant.daylily": "лилейник",
"plant.rose": "роза",
"plant.berrybush": "ягодный куст",
"plant.strawberry": "земляника",
"plant.raspberry": "малина",
"plant.stage.seedling": "росток", "plant.stage.seedling": "росток",
"plant.stage.sprout": "всходы", "plant.stage.sprout": "всходы",
"plant.stage.sapling": "саженец", "plant.stage.sapling": "саженец",
"plant.stage.mature": "взрослое", "plant.stage.mature": "взрослое",
"product.wood": "древесина", "product.wood": "древесина",
"product.grass": "трава", "product.grass": "трава",
"product.fiber": "волокно",
"product.berry": "ягоды", "product.berry": "ягоды",
"product.acorn": "жёлудь", "product.acorn": "жёлудь",
"product.mushroom": "грибы",
"pawn.being": "житель", "pawn.being": "житель",
"pawn.bear": "медведь", "pawn.bear": "медведь",
"pawn.deer": "олень", "pawn.deer": "олень",
@@ -78,11 +192,53 @@
"pawn.hare": "заяц", "pawn.hare": "заяц",
"pawn.boar": "кабан", "pawn.boar": "кабан",
"pawn.wolf": "волк", "pawn.wolf": "волк",
"pawn.chicken": "курица",
"pawn.muffalo": "муффало", "pawn.muffalo": "муффало",
"pawn.squirrel": "белка", "pawn.squirrel": "белка",
"thought.gaveBirth": "родила потомство",
"thought.mated": "спарилась",
"thought.quenchedThirst": "напилась вволю",
"gene.feedingStyle": "стиль питания",
"inspect.animal.feeding": "Питание: {0}",
"feeding.gorge": "обжора (редко, до отвала)",
"feeding.graze": "пастьба (часто, понемногу)",
"feeding.mixed": "умеренное",
"inspect.tab.log": "История",
"log.empty": "История пуста",
"log.stamp": "Д{0} {1:00}:{2:00}",
"hud.events": "События",
"hud.perf": "Произв.",
"events.title": "Журнал событий",
"events.empty": "Пока ничего не произошло",
"events.hint": "Двойной клик — перейти к существу",
"perf.title": "Производительность",
"perf.fps": "{0} FPS · {1:0.0} мс/кадр",
"perf.root": "{0}: {1:0.000} мс · систем {2}",
"perf.systemline": "{0} {1:0.000} мс {2:0}%",
"perf.hint": "Обновляется ~5 раз/с · показаны самые дорогие системы",
"event.mated": "Пара: {0} + {1}",
"event.pregnant": "{0}: беременность",
"event.birth": "{0}: роды ×{1}",
"event.hatch": "{0}: вылупление ×{1}",
"event.hunt": "Охота: {0} → {1}",
"event.injured": "Ранение: {0} → {1}",
"event.struck": "Удар: {0} → {1} ({2:0})",
"event.tookhit": "Удар: {1} → {0} ({2:0})",
"event.killed": "Добыча: {0} → {1}",
"event.ate": "{0} кормится",
"event.drank": "{0} пьёт воду",
"event.rest": "{0} отдыхает",
"event.dig": "{0} роет землю в поисках корма",
"event.wallow": "{0} валяется в грязи",
"event.fled": "{0} спасается бегством",
"event.grew": "{0}: новая стадия — {1}",
"event.death.starve": "Гибель: {0} (голод/жажда)",
"event.death.age": "Гибель: {0} (старость)",
"event.death.disease": "Гибель: {0} (болезнь)",
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню", "net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
"net.connecting": "подключение…", "net.connecting": "подключение…",
"net.connected": "подключено", "net.connected": "подключено",
"net.failed": "не удалось подключиться", "net.failed": "не удалось подключиться",
"net.lost": "соединение потеряно" "net.lost": "соединение потеряно",
"net.reconnecting": "переподключение…"
} }
+20
View File
@@ -60,6 +60,26 @@ Mods/<Имя>/
вид — `being`/`animal`). CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип вид — `being`/`animal`). CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип
дефа = новый класс + `RegisterType` в `GameContent.Load`. дефа = новый класс + `RegisterType` в `GameContent.Load`.
`Defs/` сканируется рекурсивно (`Defs/**/*.json`), и имя файла ни на что не влияет —
важны только `type` и `defName`. Поэтому контент Core разложен по подпапкам для удобства
поиска: одна папка на тип дефа, внутри — общие (абстрактные) дефы в `_Bases.json` и
конкретные, разбитые по семействам:
```
Defs/
Genes/ _Bases.json, Environment.json, Growth.json, Content.json
Plants/ _Bases.json, Trees.json, Bushes.json, Grass.json, Cacti.json,
Mushrooms.json, Flowers.json, Berries.json
Pawns/ _Bases.json, Beings.json, Animals.json
Terrain/ Terrain.json
Products/ Products.json
Patches/ Patches.json
WorldPresets/ WorldPresets.json
```
`parent` разрешается после слияния всех файлов, так что ссылка на родителя из другого
файла/папки (`TreeOakA``BaseTree`) работает независимо от расположения.
## Локализация ## Локализация
`Languages/<код>/*.json` — плоские словари «ключ → строка»: `Languages/<код>/*.json` — плоские словари «ключ → строка»:
-36
View File
@@ -1,36 +0,0 @@
# Roadmap LittleSim
Этап закрыт, когда механика работает, наблюдаема (HUD/консоль/визуал)
и управляется командой консоли.
## Этап 0 — Каркас ✅
- [x] Репозиторий с движком-сабмодулем, общий solution
- [x] Генерация рельефа по сиду (вода/песок/трава/лес/горы)
- [x] Жители-заглушки (блуждание), камера бога, HUD, консоль (`regen`)
## Этап 1 — Жители как агенты
- [~] Потребности: голод, усталость (тикают со временем) — усталость/энергия готова (`PawnNeeds`), голод впереди
- [~] Utility-выбор действия: искать еду / отдыхать / бродить — каркас на движке (`MrGameEng.AI`), выбор «отдыхать/бродить» работает; «искать еду» впереди
- [ ] Еда на траве/в лесу: растёт, собирается, истощается
- [ ] Смерть от голода; визуальное состояние жителя (цвет/прозрачность)
- [ ] Консоль: `spawn <n>`, `feed`, `starve` для тестов
## Этап 2 — Общество
- [ ] Дома и поселения (житель несёт еду домой, спит дома)
- [ ] Размножение при сытости; рост населения
- [ ] Простые профессии: собиратель, строитель
## Этап 3 — Бог
- [ ] Вера как ресурс (генерируется жителями)
- [ ] Первые силы: дождь (ускоряет рост еды), молния (поджигает лес)
- [ ] Терраформинг клеток
## Этап 4 — Экология и время
- [ ] День/ночь, сезоны
- [ ] Восстановление/деградация ресурсов, пожары
- [ ] Миграции при нехватке ресурсов
## Бэклог
- Сохранение/загрузка мира (сериализация Friflo)
- Фиксированный тик симуляции, независимый от рендера
- Исторические графики (население, еда) в дев-оверлее
+572
View File
@@ -0,0 +1,572 @@
# Животные (зоология)
> Статус: **A1–A8 реализованы целиком + сериализация животных** — базовый план зоологии и его сквозное
> требование сейв/лоада закрыты; дальше экологический горизонт (хищники, опека, эпидемии, миграция) и
> бэклог реализма. Это живой план — стиль и правила как в [концепте](concept.md) и [модах](mods.md).
>
> **Сериализация животных (готово): сейв/лоад популяции и трупов.** `WorldSave` получил `Animals`
> (`AnimalSave`: геном→признаки+пол, поколение, стадия/возраст, нужды+истощение, здоровье — кровь/HP
> частей/хедифы тяжесть+иммунитет, настроение+мысли, беременность+геном отца) и `Corpses` (`CorpseSave`:
> вид/позиция/таймер-стадия разложения/мясо). `SaveAnimals` снимает популяцию (Health/Mood/Pregnant — через
> entity, т.к. запрос ≤5 типов); `RestoreAnimals`/`RestoreAnimalState` собирают особь фабрикой из генома и
> накладывают динамическое состояние (ref-правки до структурного `AddComponent(Pregnant)`; труп
> пере-тинтится через `Stage=-1`). `Corpse` получил поле `Species` (для спрайта/разделки); `CreateCorpse`
> теперь принимает индекс вида. На загрузке стадо НЕ пересыпается, если в сейве есть животные; старый/пустой
> сейв — регенерируется из сида (не крашит). Эволюция/болезни/беременности переживают сейв/лоад. Сборка +
> `--check-content` чистые.
>
> **A8 часть 2 (готово): инспектор животных.** `InspectPanel` теперь осматривает и животных (не только
> растения): вкладки **Обзор** (стадия/возраст/пол/поколение/беременность), **Нужды** (значения нужд %),
> **Здоровье** (кровь/боль, способности Consciousness/Moving/Sight, активные хедифы с тяжестью),
> **Настроение** (значение % + список мыслей с их сдвигом). Набор вкладок зависит от типа выбранного
> (растение vs животное). `SelectionSystem` теперь пикает и животных (запрос расширен с `PlantGrowth` на
> `AnimalOrganism`). Локализация `inspect.*`/`need.*`/`hediff.*`/`cap.*`/`animalstage.*` (ru/en). Сборка +
> `--check-content` чистые.
>
> **A8 часть 1 (готово): настроение — ядро.** Тип дефа `ThoughtDef` + `thoughts.json` (мысли: роды/
> спаривание/утоление жажды — сила и длительность данными); компонент `Mood` (значение 0..1, активность по
> тиру мозга, список затухающих мыслей) на всех животных; `AnimalMoodSystem` (тир 4+: настроение из
> непрерывного фона — средняя удовлетворённость убывающих нужд минус боль из здоровья — плюс сумма мыслей;
> мысли угасают). Реестр `ThoughtSet` + id-константы `AnimalThoughts`; событийные триггеры: роды
> (`AnimalPregnancySystem`), спаривание и утоление сильной жажды (`AnimalActionSystem`). **Эффект на
> поведение**: низкое настроение (стресс/истощение) подавляет спаривание в `AnimalDecisionSystem`. Команда
> `mood [species]`. Сборка + `--check-content` чистые (11 типов дефов). Осталось A8.2: вкладки инспектора
> (needs/health/mood) — сейчас животные в инспекторе не осматриваются (он только растительный).
>
> **A7 (готово): мозг → интеллект → гейт нужд/действий.** Эффективный интеллект = `brainSize` (ген) ×
> `Consciousness` (capacity из здоровья) — `AnimalFactory.Intelligence`. `AnimalContext` гейтит выбор ИИ:
> нужда с `minBrain` выше интеллекта особи в выбор не входит (`AnimalDecisionSystem` читает `Health`
> поэлементно). Пороги в `needs.json` — тиры мозга (голод 0.0, жажда 0.25, сон/секс 0.40; у оленя мозг
> ~0.45 → здоров весь набор). **Обратная связь от повреждения мозга**: болезнь/боль/кровопотеря роняют
> сознание → эффективный интеллект падает → первыми отключаются сон/спаривание (лихорадящий олень
> сводится к еде/питью). Команда `intel <species> [consciousness]`. Сборка + `--check-content` чистые.
>
> **A5 часть 1 (готово): смерть → труп → разложение.** `Corpse`-компонент + `CorpseSystem`
> (Fresh→Rotting→Skeleton→исчезает, скорость от температуры, тинт по стадии); смерть теперь оставляет
> труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` +
> `corpseTexture` оленя (`Dessicated_*`, скелет — бледный тинт, своего спрайта пока нет). Продукты
> `ProductMeat/Bone/Leather` в `products.json` (разделка/падальщики — позже). Сборка/линт чистые.
> **A5 часть 2 (готово): части тела, кровь, capacities.** `BodyDef`/`BodyPartDef` + `bodies.json`
> (`Quadruped` — данные: иерархия органов, вклады в способности); `AnimalDef.Body`; `HealthState`
> расширен `Parts`/`BloodLevel`/`Pain`/`Capacities`; `CapacityCalc` (обобщённый расчёт по зависимостям
> кровь→сердце/лёгкие→сознание→движение/зрение, модовые способности — сырыми); фабрика строит части
> (HP × размер тела) и считает способности. Источников урона ещё нет → всё на полном HP, способности =
> 1 (каркас под болезни A6/хищников). Команда `body <species>`. Сборка/линт чистые (10 типов дефов).
> Осталось в A5: смерть через здоровье (vital-часть/кровь) + истощение как hediff — когда появятся раны.
>
> **A6 (готово): болезни и иммунитет.** `HediffDef` расширён (severity/immunity/lethal/pain/capMods/
> ambient); хедифы теперь инстансы (`Hediff` с тяжестью/иммунитетом). `AnimalHealthSystem` (медленный
> тик ~час): фоновое заражение, гонка тяжесть↔иммунитет (выздоровление или смерть), пересчёт способностей
> с учётом боли и capMods; смерть по здоровью (летальная болезнь/кровь/потеря сознания) → труп. **Первый
> реальный урон** — `Moving` подключён к скорости (больные звери медленнее). Болезнь `Fever` в
> `hediffs.json`; команда `infect [hediff]`. Сборка/линт чистые.
>
> **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`,
> компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`;
> `AnimalRutSystem` (сезонный гон-хедиф поднимает влечение — вне сезона зачатия нет); действие
> `Mate` (поиск партнёра, сближение); `Pregnant` + `AnimalPregnancySystem` (вынашивание → роды
> через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка
> чистая, `--check-content` (33 гена).
>
> **Рефактор расширяемости (готово):** устранены хардкод-блокеры — данными стали: **геном вида**
> (`AnimalDef.Genome` = словарь `geneId→base`, шаблон строится generically); **нужды**
> (`NeedDef`/`needs.json` + `NeedSet` + динамический `AnimalNeeds.Values` + реестр поведений по id);
> **диета** (`AnimalDef.Diet`); **стадии роста** (`AnimalStageDef`/`AnimalDef.Stages` — возраст входа,
> масштаб, текстуры, флаг «взрослая»; пусто → встроенный дефолт); **спавн** (`AnimalDef.SpawnPer1000Cells`
> — все виды рассыпаются по суше из данных, не только олень). Добавить ген/нужду/рацион/стадию/спавн
> виду = правка JSON без кода. Остаются как известные: реестр действий-кода (новое *поведение* = код,
> сознательная граница), часть глобальных баланс-констант (скорость/выпас/потолок).
>
> **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY,
> генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`;
> стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена
> спрайта детёныш/самец/самка и размера по стадии) + `AnimalMortalitySystem` (смерть от
> старости и от истощения → деспавн, замыкает контур ёмкости среды). Спавн стада —
> разновозрастный; команда `animal` показывает пол/созревание. Сборка чистая, `--check-content`
> (30 генов).
>
> **A1 (готово):** тип дефа `Animal` (`AnimalDef : PawnDef`) + `animals.json` (олень);
> 8 животных генов в `genes.json`; `AnimalPhenotype`/`AnimalOrganism`; `AnimalSet`
> (шаблон генома вида) + `AnimalFactory`; детерминированный спавн стада оленей по суше в
> `WorldScene` с рендером и тинтом меха по гену; консольная команда `animal <species>`.
>
> **A2 (готово):** нужды `AnimalNeeds` (голод/жажда/отдых) с падением по гену метаболизма;
> два слоя ИИ — `AnimalDecisionSystem` (движковый `UtilityAi`, выбор + поиск ближайшей
> еды/воды) и `AnimalActionSystem` (движение к цели + утоление: выедание растений, питьё у
> кромки воды, сон); `AnimalAppearanceSystem` (яркость по острой нужде); кромка воды из
> террейна; команда `popstats [species]` (средние признаков + размах поколений). Контур
> ёмкости среды частичный (трава выедается, но смерть от голода — в A3). Сборка чистая;
> визуальная проверка (олени ищут корм/воду, пасутся) — вручную: «Новый мир».
## Видение
Животные — следующая большая система после генов. Главный принцип: животное живёт по
**тем же законам, что и растение** — гены → признаки (фенотип) → поведение систем, —
но разворачивает их в полноценную зоологию: двуполое размножение с вынашиванием,
здоровье по модели RimWorld (части тела, органы, кровь, болезни), мозг и интеллект,
нужды и настроение. Цель прежняя: **эмерджентность** (динамика популяций, отбор,
пищевые цепи складываются из правил, а не скриптуются) и **наблюдаемость** (всё видно
в мире, HUD, инспекторе, консоли).
Первое показательное животное — **олень** (богатый набор текстур: ♂/♀/детёныш/труп).
## Опора на существующий фундамент
Ничего из перечисленного не строим заново — переиспользуем:
- организм-агностичный `GeneDef`, управляемый `Genome` (диплоидные аллели),
`Genome.Breed(мать, отец, mutationChance)`, `Phenotype.Compute`, `GenomeTemplate`
(шаблон вида, ср. `PlantSet.BuildTemplate`);
- формульный движок и regex-группы (`gsum('Gene.*')`) для производных признаков;
- движковый `UtilityAi<TContext>`, `ResponseCurve`, `Suitability.Trapezoid`;
- день/ночь, сезоны, климат, террейн `water`, `ProductDef`;
- паттерн компонентов растений: `…Organism { Genome; Phenotype Traits }`,
`…Growth { species, stage, ageDays }`, `…Needs`, `…Brain`, `AppearanceSystem` (sim→`Sprite`).
- Friflo допускает managed-ссылку в `IComponent` (доказано `Genome`/`Tilemap`) —
поэтому `Health`/`Pregnant` несут managed-объекты.
## Принцип моддинга: всё расширяется данными
Главное требование — **система должна целиком модифицироваться через моды без правки
кода**. Для этого:
1. **Никаких C#-`enum` для видового контента.** Виды, части тела, органы, способности
(capacities), болезни, нужды, мысли — это **дефы со строковыми id**, интернируемые
в int при загрузке (ровно как уже сделано с `geneId`). Enum нельзя расширить модом —
строковый id можно.
2. **Формулы вместо констант** везде, где можно (как `effects` у генов): модер меняет
баланс формулой в дефе, а не пересборкой.
3. **Реестр поведенческих действий** (`actionId → исполнитель-код`). Деф нужды/ИИ
ссылается на `actionId`. Граница честная и в духе проекта (движок без сэмпла,
фичи демонстрируются игрой): **комбинировать готовые кирпичи — мод; принципиально
новое поведение — код + деф.**
4. **Patch-система (фаза G5)** уже позволяет модеру менять чужие дефы по regex
(например, «всем хищникам +агрессия»).
5. Любой деф переопределяется одноимённым из позднего мода; локализация — в `Languages`.
Итог: модер добавляет новый вид зверя, орган, болезнь, нужду или мысль одними JSON-файлами.
## Геном животного
Животные гены живут в `genes.json` рядом с растительными (один организм-агностичный
тип `Gene`). Предлагаемый набор (числовые, если не сказано иное):
| Группа | Гены |
|---|---|
| Тело/метаболизм | `GeneMaxBodySize`, `GeneMetabolism`, `GeneMoveSpeed`, `GeneBloodVolume`, `GeneVision`, `GeneInsulation` (мех/терморегуляция) |
| Жизненный цикл | `GeneMaturityAge`, `GeneLifespan` (есть), `GeneGestationDays`, `GeneLitterSize` |
| Органы | `GeneHeartEfficiency`, `GeneLungCapacity`, `GeneImmunityGain`, `GeneHealRate`, `GenePainTolerance` |
| Мозг | `GeneBrainSize` → признак `brainSize` (см. «Мозг и интеллект») |
| Поведение | `GeneAggression`, `GeneSociability`, `GeneActivityPattern` (дневной/ночной/сумеречный) |
| Среда | переиспользуем `GeneOptimalTemperature/Tolerance/Cold/HeatHardiness` — звери тоже мёрзнут/перегреваются через ту же трапецию |
| Морфология | `GeneFurColor` (тинт спрайта, как `leafHue`), `GeneSex` (дискретный, см. ниже) |
**Пол выводится из диплоидности бесплатно.** Локус с аллелями {X=0, Y=1}: самка = XX,
самец = XY. При `Genome.Breed` мать может отдать только X (у неё нет Y), отец — X/Y
поровну → потомок XX/XY ровно 50/50, **без спец-кода в скрещивании**. У гена пола
`mutationChance: 0` (как у `GeneFruitSeason`), чтобы X не мутировал в Y.
> Тонкость генерации (не скрещивания): обычный `Generate` дискретного гена выбрал бы
> два аллеля независимо и мог бы выдать невозможный **YY**. Поэтому пол особи-основателя
> назначается явно — XX или XY (по 50/50) — а дальнейшее наследование корректно само.
Признаки собираются в `AnimalPhenotype.FromTraits(traits)` — зеркало `PlantPhenotype`.
Организм: `AnimalOrganism { Genome; AnimalPhenotype Traits; int Generation }` (поколение =
max(род.) + 1, у основателей 0 — для `popstats` и видимого дрейфа).
## Жизненный цикл и стадии роста
Зеркало `PlantGrowth`: `AnimalGrowth { species, stage, ageDays }`. Стадии задаются
данными в дефе вида: `Embryo` (в утробе) → `Baby``Juvenile``Adult``Senior`,
пороги из генов (`MaturityAge`, `Lifespan`). `AnimalGrowthSystem` двигает стадию,
масштабирует `sizeCells`, меняет спрайт/набор текстур (Baby ↔ ♀/♂). Стадия `Senior`
повышает шанс возрастных hediff и смерти — стык со здоровьем.
> **Путь смерти по фазам.** Чтобы на A4 (размножение) популяция не взрывалась без
> убыли, простая **смерть по старости и от голода/жажды (деспавн)** появляется уже в A3.
> На A5 она «апгрейдится»: смерть идёт через здоровье (возрастные hediff, истощение, отказ
> органов) и оставляет **труп** вместо деспавна.
## Нужды и ИИ
Обобщаем `PawnNeeds` (одна энергия) → `AnimalNeeds`: голод, жажда, сон, секс (0..1).
- скорости падения из `GeneMetabolism` × размер (масштаб по Клайберу: ∝ size^0.75);
- **голод** — ест растения/продукты. **Поедание реально воздействует на сущность
растения** (важно для контура ёмкости среды): выедает накопленные плоды
(`Fruiting.RipeFruit`) и/или убавляет биомассу/стадию растения; перевыпас может убить
растение. **Диета** (`AnimalDef.diet`) — список съедобного: теги растений/`ProductDef`
(для будущих хищников — живая добыча/мясо/трупы), плюс способ (выедание плодов vs выпас);
- **жажда** — пьёт на клетках террейна `water`;
- **сон** — спит в неактивную фазу суток (`GeneActivityPattern`): дневные ночью и т.д.;
- **секс** — удовлетворяется размножением.
ИИ разбит на **два слоя — выбор и исполнение**:
1. **Выбор**`AnimalDecisionSystem` на движковом `UtilityAi`: из активных нужд выбирает
действие `FindFood / FindWater / Sleep / SeekMate / Wander(default)` (готовые
`ResponseCurve`) и пишет его + цель в `AnimalBrain { Action; Target; DecideIn }`.
Действия и нужды описаны дефами `NeedDef` (decay-формула, `minBrain`).
2. **Исполнение** — отдельный слой систем (реестр `actionId → исполнитель`, см. «Принцип
моддинга») доводит выбранное действие до результата по схеме **найти цель → дойти →
выполнить → удовлетворить нужду**:
- *Eat*: ближайшее съедобное (плод/биомасса) → подойти → съесть (убавить растение) → ↑ сытость;
- *Drink*: ближайшая клетка `water` → подойти → попить → ↓ жажда;
- *Sleep*: остановиться → спать → ↓ усталость;
- *Mate*: подходящий партнёр → сблизиться → акт → `Pregnant` + сброс `Mating`;
- *Wander*: движение по умолчанию.
Без этого слоя нужда только падает, но не исполняется.
**Передвижение и поиск цели.** Движение к цели — рулевое смещение к точке (реюз
`Transform2D` + `WanderSystem`/`SeparationSystem` для обхода препятствий: стволы деревьев,
другие звери), без полноценного A* на старте. Вода непроходима для сухопутных → `Drink`
целит клетку суши у кромки воды. «Ближайшее съедобное/вода/партнёр» — **пространственный
запрос по сетке/тайлмапу** с радиусом из `Vision`/`Sight` (не O(n²)); ничьи в «ближайшем»
разрешаются детерминированно (по id сущности).
> Порядок фаз: формальный **гейтинг нужд интеллектом — фаза A7**. В A2 у оленя просто
> полный набор нужд зверя (тир 4); поле `minBrain` уже пишем в дефы, но активируется оно
> на A7 (см. «Мозг и интеллект»).
## Размножение (млекопитающие)
Главное отличие от растений — двуполость и отложенное во времени вынашивание:
1. **Влечение и гон (через хедиф).** Нужда `Mating` есть **круглый год** на низком фоне.
В брачный сезон вида на взрослых вешается **хедиф `Rut` (гон)**, который поднимает
влечение (множитель на рост `Mating` / снижение порога действия). **Только под `Rut`
влечение переходит порог `SeekMate` → вне сезона зачатия фактически нет**, хотя нужда
формально существует. Хедиф снимается с концом сезона и **виден в инспекторе** (как
любой hediff). Это унифицирует сезонность с hediff-механизмом, а не городит спец-флаг.
2. **Спаривание → беременность.** Два взрослых разного пола сближаются (действие `Mate`) →
акт → у самки появляется `Pregnant : IComponent { Genome FatherGenome; float
ConceivedDay; float GestationDays; int LitterSize }` — managed-ссылка на геном отца
(оплодотворение = «слепок» отца). Беременность повышает метаболизм (голод) и снижает
`Moving`. (Можно реализовать и как hediff, но держим компонентом ради managed-ссылки
на геном отца.)
3. **Роды.** По достижении срока спавним `LitterSize` детёнышей, каждому
`Genome.Breed(мать, отец, mutationRate)`, стадия `Baby`, рядом с матерью. Лактация/
опека — опционально после.
Стратегии r/K выводятся из генов: мелкие (заяц) — большой помёт, короткая жизнь;
крупные (олень/медведь) — мало потомства, долгая жизнь.
**Решённые параметры (A4):**
- **Гон = хедиф `Rut`** (а не «нужда растёт в сезон»): навешивается в брачный сезон
`GeneBreedingSeason` (как `fruitSeason`, 0..3) — у **оленя осень** (→ роды весной к
обилию корма); у r-стратегов (заяц) — круглогодично/частый гон. Под `Rut` влечение
переходит порог действия; вне `Rut` зачатия нет.
- **Hediff-каркас рождается здесь (A4).** Гон — первый хедиф, поэтому в A4 появляется
лёгкий каркас: контейнер `Health`/`HealthState` со списком хедифов, `HediffDef` со
стадиями и модификаторами, навешивание/снятие по триггеру. A5/A6 расширяют его
частями тела, кровью, болезнями.
- **Триггер акта** — взрослые, разный пол, у обоих `Mating > 0.6`, рядом (1–2 клетки),
самка не беременна, оба не спят/не убегают.
- **Послеродовый кулдаун** — самка не зачинает снова до родов + восстановления;
самец восстанавливается быстро → **полигиния** (один самец кроет нескольких самок).
- **Выбор партнёра (половой отбор)** — НЕ в A4: на старте берётся ближайший подходящий;
предпочтение по размеру/здоровью/настроению добавим позже отдельной итерацией.
## Здоровье — полная модель RimWorld
### Данные (дефы)
- **`BodyDef`** — дерево частей. У части: `coverage` (доля тела → вес попадания),
`parent`, `maxHp` (масштаб от размера тела), флаги (`vital`, `solid`, `skinCovered`),
**вклады в capacities** `{ breathing: 0.5, consciousness: 1.0, moving: 0.25, … }`.
Пример (олень): `body → torso → (heart, lungs×2, liver, stomach, kidneys×2, spine),
head → (brain, eyes×2, ears×2, jaw), legs×4 → hooves`.
- **`HediffDef`** — *всё* есть hediff (рана, потеря части, болезнь, имплант, можно и
беременность): `stages` с порогами по severity; стадия задаёт `capMods`
(offset/factor/setMax по capacity), `painOffset/painFactor`, `bleedRate`, для болезней —
параметры иммунитета.
- **`CapacityDef`** — общий каталог способностей строковыми id: `Consciousness, Moving,
Manipulation, Sight, Hearing, Breathing, BloodPumping, BloodFiltration, Digestion,
Metabolism` (расширяемо модом). Это **каталог**, а не обязательный набор: вид имеет
лишь те способности, в которые вкладываются части его `BodyDef` (у копытного оленя нет
`Manipulation` — некому вкладываться).
### Компонент
`Health : IComponent { HealthState State }` — managed-класс (как `Genome`):
`List<PartInstance>` (деф + текущий hp), `List<Hediff>` (severity, привязка к части,
иммунитет), кэш `Capacities`, `Pain`, `BloodLevel`, `Dead`.
> Контейнер растёт по фазам: **рождается в A4** с одним лишь `List<Hediff>` (под гон-хедиф
> `Rut`); части тела, кровь, capacities и `Dead` добавляются в A5, болезни/иммунитет — A6.
### Расчёт capacities (обобщённая дата-модель)
Способности зависят друг от друга, считаем в порядке зависимостей:
1. `BloodLevel` (0..1) ← запас минус кровопотеря;
2. `BloodPumping` ← сердце × BloodLevel; `Breathing` ← лёгкие × BloodLevel;
`BloodFiltration` ← печень/почки;
3. `Consciousness` ← мозг × BloodPumping × Breathing × (1 болевой штраф);
4. `Moving` ← ноги/позвоночник × Consciousness × BloodPumping; `Sight` ← глаза ×
Consciousness; `Digestion` и пр. — домножаются на вышестоящие.
Формула одна и общая: `capacity = Σ(вклады работающих частей × partHp/maxHp) ×
вышестоящие capacity`. **Без хардкода на каждую способность** — новую способность
вводит деф. Каждая capacity — множитель на действия: `Moving`→скорость, `Sight`→радиус
поиска, `Digestion`→скорость насыщения, `Consciousness`→интеллект и сам факт сознания.
### Кровь — как hediff (реалистичная модель RW)
Кровопотеря = hediff `BloodLoss` (severity 0..1). Кровоточащие раны поднимают её через
`bleedRate`; без кровотечения медленно рассасывается; severity = 1 → смерть; влияет на
`BloodPumping`/`Consciousness` через `capMods`. `GeneBloodVolume` масштабирует скорость
роста severity (больше крови → медленнее теряет).
### Болезни и иммунитет (гонка)
severity болезни растёт за день; параллельно растёт иммунитет (0..1) со скоростью из
стата (зависит от `BloodFiltration`, отдыха, сытости, `GeneImmunityGain`). Иммунитет→1
раньше = выздоровел; severity→летальный порог раньше = смерть. Голодный/раненый зверь
борется с болезнью медленнее.
### Смерть
Триггеры: разрушена `vital`-часть (мозг/сердце), `Consciousness ≤ порога`,
`BloodLevel = 0`, летальная severity болезни, старость (возрастные hediff: катаракта,
больная спина, деменция, закупорка артерий + шанс смерти от старости). На смерти:
стоп-ИИ, переход в труп.
### Тик
Медленный «health tick» раз в игровой час + событийный пересчёт при уроне.
Детерминизм: привязка к игровому времени, не к FPS.
### Источники урона в v1
**Почти никаких.** Строим весь каркас (части, кровь, capacities, hediff-framework, условия
смерти), но не вводим *травмирующих* источников. На старте (A1–A2) смерть только **от
старости**.
Порядок источников урона по фазам: **истощение/обезвоживание** — уже в **A3** (простое
правило «нужда=0 дольше N дней → смерть», замыкает контур ёмкости среды; в A5
переоформляется в hediff истощения) → **температура** (гипотермия/тепловой удар, hediff на
той же трапеции, что у растений) → **болезни** (A6) → **хищничество** (горизонт).
## Труп и разложение
Труп — отдельная сущность с `Corpse { RotDays; Stage }`. Конвейер (скорость зависит от
температуры/сезона — реюз климата):
```
Fresh ──→ Rotting ──→ Skeleton ──→ (исчезает)
│ │ │
│ разделка │ разделка └─ остаётся ProductBone
└──────────┴─→ ProductMeat (кол-во ∝ GeneBodySize) + опц. ProductLeather
```
- свежий/гниющий можно **разделать** → мясо (объём от размера тела) + шкура;
спрайт `Dessicated_*` (для оленя уже есть) на стадии Rotting;
- **скелет** — финальная стадия разложения (как просили, скелет — часть процесса):
мясо уже не получить, остаются кости (`ProductBone`);
- после скелета труп **исчезает в ничто**. На будущее — возврат плодородия в почву
(ниточка к растениям, опционально).
- мясо/кости/падаль → кормовая база будущих хищников и падальщиков.
Новые продукты: `ProductMeat`, `ProductBone`, `ProductLeather`.
**Спрайт скелета — нужен настоящий (решено).** В проекте костяных/скелетных текстур
нет (проверено) → это **ассет-задача**: создать/найти спрайт скелета (желательно
универсальный + по-видовой при наличии), путь вида `things/pawn/animal/deer/Skeleton_*`.
До появления ассета стадию Skeleton можно временно рисовать обработкой `Dessicated_*`,
но финальная версия требует своего спрайта.
## Мозг и интеллект
`GeneBrainSize` → признак `brainSize`. **Эффективный интеллект** = `brainSize ×
Consciousness` — т.е. повреждение/болезнь мозга, боль, кровопотеря динамически снижают
интеллект (сильная связка со здоровьем).
Интеллект **гейтит набор нужд и действий**. У `NeedDef`/действия поле `minBrain`:
активный набор особи = те, у кого `minBrain ≤ brainSize`. **Без enum** — шкала
непрерывная, тиры ниже лишь ярлыки для авторов контента (числа порогов — контент,
правятся модом). `brainSize` нормирован 0..1.
| Тир | `minBrain` | Кто | Что открывается |
|---|---|---|---|
| 1 — рефлекс | 0.00 | губки, простейшие | пассивный голод, реактивность |
| 2 — примитив | ~0.10 | насекомые, черви | голод; реактивное поведение |
| 3 — низший позвоночный | ~0.25 | рыбы, рептилии, амфибии | +жажда; простое избегание |
| 4 — зверь | ~0.45 | млекопитающие, птицы (**олень**, заяц, волк) | +сон, секс, Flee, SeekMate; **настроение**; факультативно стадность/память |
| 5 — умный (на будущее) | ~0.75 | приматы, врановые, люди | +социальные нужды, игра, любопытство, память о ресурсах, планирование, обучение, сложное настроение/отношения |
Олень = **тир 4**. Настроение, сон и брачное поведение включаются с тира 4.
## Настроение
`Mood` (0..1) собирается **из двух источников** (решено):
1. **Непрерывные состояния → в настроение напрямую** по кривой `ResponseCurve`:
уровни голода, жажды, усталости, боли (из здоровья), температуры вне комфорта.
Это не дискретные мысли, а текущий вклад нужд/состояния.
2. **События → `ThoughtDef { moodOffset, durationDays, trigger }`** (модель thoughts
RimWorld, данными): «дала потомство», «спарилась», «напилась после долгой жажды»,
«потеряла детёныша» — временный сдвиг настроения с затуханием.
Настроение есть только с тира 4 (зверь) — гейт интеллектом; у примитивных его нет.
Влияет на поведение (низкое → паника/бегство, не спаривается) и видно в инспекторе.
## Ориентировочные числа для оленя (старт)
Шкала времени мира: **год = 60 игровых дней**, сезон = 15 дней (для справки растения
живут ~160 дней). Числа — стартовая точка, балансируются при реализации; это значения
по умолчанию генома вида (`GenomeTemplate`), вокруг которых идёт разброс и мутации.
| Параметр | Ген/поле | Значение | Заметка |
|---|---|---|---|
| Размер тела | `GeneMaxBodySize` | ♀ 1.4 / ♂ 1.6 sizeCells | как в текущих `DeerDoe`/`DeerBuck` |
| Продолжительность жизни | `GeneLifespan` | ~360 дн (~6 лет) | дольше растений, видно несколько поколений |
| Взросление (→Adult) | `GeneMaturityAge` | ~90 дн (~1.5 года) | Baby 020, Juvenile 2090, Adult 90~290, Senior 290+ |
| Срок беременности | `GeneGestationDays` | ~30 дн (2 сезона) | гон осенью → роды весной |
| Помёт | `GeneLitterSize` | 1 (изредка 2) | K-стратег |
| Сезон гона | `GeneBreedingSeason` | осень | у зайца — круглый год |
| Размер мозга | `GeneBrainSize` | ~0.45 (тир 4) | открывает сон/секс/настроение |
| Объём крови | `GeneBloodVolume` | ~1.0 (×размер) | масштабирует скорость кровопотери |
| Скорость | `GeneMoveSpeed` | ~1.1–1.3 | олень быстрый |
| Голод | `NeedDef` decay | полный голод за ~1.5 дн | по Клайберу ∝ size^0.75 |
| Жажда | `NeedDef` decay | полная жажда за ~0.75 дн | быстрее голода |
| Сон | `NeedDef` decay | суточный цикл | спит в неактивную фазу |
## Идеи для реализма (бэклог)
Опционально, по мере фаз — каждая ложится на дата-модель:
- **Гон/брачный сезон** — хедиф `Rut`, навешиваемый в сезон, поднимает влечение (реализовано в A4).
- **Терморегуляция мехом** — `GeneInsulation` сдвигает температурную трапецию;
новорождённые мёрзнут сильнее.
- **Метаболизм по Клайберу** — расход ∝ size^0.75 (крупные едят больше абсолютно,
меньше на кг).
- **Циркадные ритмы** — дневные/ночные/сумеречные виды (`GeneActivityPattern`).
- **Стадность** — `GeneSociability`: стадные держатся группой (реюз `SeparationSystem`
+ когезия) → эмерджентные стада.
- **Сезонный окрас** — арктические лиса/заяц/волк белеют зимой (текстуры `*_arctic`
есть) — сезонный свитч спрайта/тинта.
- **Половой диморфизм** — рога/размер у самцов (ген, проявляется при XY).
- **r/K-стратегии** — выводятся из `litterSize/gestation/lifespan`.
- **Инбридинг-депрессия** (продвинутое) — родственное скрещивание снижает fitness
через накопление рецессивных аллелей.
- **Память о ресурсах/территория** (для умных) — водопои, кормовые места.
- **Возврат нутриентов** — полное разложение трупа повышает плодородие клетки.
## Новые типы дефов (для модеров)
| Тип | Назначение |
|---|---|
| `AnimalDef : PawnDef` (подтип) | вид: геном-шаблон (ссылки на гены), `BodyDef`, диета, текстуры по стадиям/полу, размеры. Наследник `PawnDef` — существующие 9 животных-`PawnDef` остаются валидными; поведение на компонентах, не на типе дефа |
| `BodyDef` | дерево частей тела с coverage/parent/maxHp/флагами/вкладами в capacities |
| `HediffDef` | раны/болезни/состояния: стадии, capMods, боль, кровотечение, иммунитет |
| `CapacityDef` | именованная способность (строковый id) |
| `NeedDef` | нужда: формула расхода, `minBrain`, привязка к действию |
| `ThoughtDef` | мысль настроения: offset, длительность, условие-триггер |
| `ProductDef` (есть) | + `ProductMeat`, `ProductBone`, `ProductLeather` |
CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип = класс + `RegisterType` в
`GameContent.Load` (см. [mods.md](mods.md)).
## Отбор, экология и наблюдаемость
Смысл генетики — чтобы за поколениями популяция **реально адаптировалась к климату
сида** (дрейф признаков под отбором). Чтобы это было видно (столп «наблюдаемость»):
- **Команда `popstats <species>`** — среднее и разброс ключевых признаков популяции +
счётчик поколений. Без неё отбор «есть, но невидим». Появляется, как только в мире
есть популяция (≈A1–A2), расширяется по мере добавления признаков.
- **Контур ёмкости среды (Лотка–Вольтерра через корм).** Даже без хищников травоядные
могут **выесть растения → голод → крах популяции → восстановление флоры → новый рост**.
Это бесплатная эмерджентность уже на A2, **если поедание реально уменьшает растения**.
Явно проверяем этот контур на A2 (поедание убавляет плод/растение, голод растёт при
нехватке корма). **Важно:** полный «крах» популяции требует **смерти от голода** — она
появляется в **A3** (простое правило). На A2 контур наблюдается частично (звери голодают
и тормозят, но не вымирают), полностью замыкается с A3.
## Сквозные требования (каждая фаза)
- **Детерминизм.** Любая случайность (спавн, выбор партнёра, мутации, выбор части под
урон, шанс инфекции) — из seeded `Random`, выведенного из сида мира (+ id сущности),
никогда `Random.Shared`. Один сид = воспроизводимый мир. Ничьи в пространственных
запросах («ближайший корм/вода/партнёр») разрешаются по id сущности, не по порядку обхода.
- **Сериализация — чек-лист по каждой фазе.** Каждая фаза сразу делает сейв/лоад своих
данных: A1 геном (уже умеет, geneId→Allele) → A3 стадия/возраст/пол → A4 беременность
(срок + **геном отца**) + список хедифов (гон `Rut`) → A5 здоровье (части тела, кровь,
capacities) + труп (стадия/таймер разложения) → A8 настроение/мысли. Пустой/старый
сейв — регенерируется, не крашит.
- **Sim/presentation split.** Системы симуляции пишут только компоненты; спрайт/тинт
(пол, стадия, мех, гниль) ставит `AppearanceSystem`. Никаких draw/UI из симуляции.
## План реализации (фазы A1–A8)
По аналогии с фазами генов G1–G5. Каждая фаза — отдельный коммит, дев-команда для
проверки, проверка через `--check-content` (где можно), и пункты сквозных требований выше.
**Вкладки инспектора добавляются по мере появления данных:** needs@A2, health@A5, mood@A8.
1. **A1 — фундамент.** `AnimalDef` + животные гены + `AnimalPhenotype`/`AnimalOrganism`
+ per-species `GenomeTemplate` + спавн оленя в `WorldScene` + рендер + команда
`animal <species>`.
2. **A2 — нужды + ИИ (выбор + исполнение).** `AnimalNeeds` + `AnimalDecisionSystem`
(выбор) + **слой исполнения** (Eat/Drink/Sleep: найти→дойти→выполнить→удовлетворить) +
еда-растения / вода-террейн / сон по фазе суток + `AppearanceSystem`. **Проверить
контур ёмкости среды** (поедание убавляет растения → колебания). Команда `popstats`.
3. **A3 — жизненный цикл.** Стадии Baby→Adult→Senior, ген пола (XX/XY, генерация без
YY), спрайт по стадии/полу, **простая смерть по старости и от голода/жажды (деспавн)**
— последняя замыкает контур ёмкости среды.
4. **A4 — размножение.** Нужда `Mating` + **лёгкий hediff-каркас** + гон-хедиф `Rut`
(сезон) + исполнение `Mate` → `Pregnant` → роды через `Genome.Breed`.
5. **A5 — здоровье I.** `BodyDef`, части тела, кровь (hediff), capacities, **апгрейд
смерти**: через здоровье + **труп** с разложением до скелета (вместо деспавна A3).
6. **A6 — здоровье II.** Болезни/`HediffDef` с иммунитетом, боль.
7. **A7 — мозг/интеллект.** Ген→интеллект→гейт нужд/действий + обратная связь от
повреждения мозга.
8. **A8 — настроение.** `Mood` + `ThoughtDef` + вкладки инспектора (needs/health/mood —
`InspectPanel` уже умеет вкладки overview/genes/products).
## После A8 — экологический горизонт
То, ради чего вся глубина: превращает «зоопарк» в живую экосистему. Каждая фича опирается
на уже построенные системы.
> **ГОТОВО — хищный кластер (часть 1): диета-гены + охота + урон + бегство.** Диета теперь
> ДАННЫЕ-через-ГЕНЫ: `GeneHerbivory`/`GeneCarnivory`/`GeneOmnivory` → признаки
> `herbivory/carnivory/omnivory`; рацион выводит `AnimalFactory.Diet` (ест растения, если
> max(herb,omni) ≥ 0.5; ест мясо, если max(carn,omni) ≥ 0.5 — всеядность включает оба; откат
> на `AnimalDef.Diet` у видов без генов диеты). Новые виды: **Wolf** (хищник, охотится) и
> **WildBoar** (всеядный); у оленя добавлен `GeneHerbivory`. Действие `Hunt`
> (`AnimalDecisionSystem` ищет ближайший труп-падаль ИЛИ живую добычу не крупнее себя;
> `AnimalActionSystem.Hunt` ведёт к цели и при контакте кусает или ест труп). **Первый
> травматический урон**: `HealthState.ApplyInjury` (урон части тела по весу попадания + острая
> кровопотеря + хедиф `Bleeding`); `Bleeding` (`bloodLossPerDay`) точит кровь в
> `AnimalHealthSystem` (+восстановление крови в покое), кровь на нуле → смерть → труп → падаль.
> Боевые параметры — данные на `AnimalDef` (`attackDamage/attackBleed/attackBloodLoss/
> attackRangeCells`). **Бегство**: `AnimalActions.Flee` — жертва, заметившая хищника не мельче
> себя в радиусе восприятия, убегает (приоритет над нуждами); сам хищник бежит лишь от заметно
> более крупного мясоеда. Общий путь смерти — `AnimalFactory.Die`. Консоль: `diet [species]`;
> `--check-content` печатает роль каждого вида. **Не покрыто (горизонт):** падальщики как
> отдельная роль, удар по настроению от страха, снижение страха стадом, спрайт скелета.
- **Хищный кластер.** Хищничество (волк→олень): диета = живая добыча; действие `Hunt`
(выследить → преследовать → атака → **урон через здоровье A5** → добить → съесть труп).
Это **первый «активный» (травмирующий) источник урона** для системы здоровья и
полноценная Лотка–Вольтерра с хищником. Рядом: **падальщики** (едят трупы Fresh/Rotting)
и **угроза/бегство** (хищник в радиусе `Sight` → страх → действие `Flee`, удар по
настроению; стадо снижает страх).
- **Родительская опека / импринтинг.** Детёныш ходит за матерью, голод гасится близостью
к лактирующей матери до ювенильной стадии; мать ведёт/защищает. Реализм + детёныши не
мрут от голода в одиночку.
- **Эпидемии.** Болезни (A6) распространяются по контакту/плотности → плотностно-зависимый
регулятор популяции (вспышки-крахи). Исход решает иммунитет (`GeneImmunityGain`).
- **Миграция.** При локальном истощении корма / некомфортном климате — уход к лучшим
клеткам (концепт: «голод+неурожай→миграция»). Эмерджентно из нужд + пространственного
поиска с большим радиусом.
Сюда же тяготеют пункты бэклога: половой отбор (выбор партнёра), сезонный окрас,
инбридинг-депрессия, возврат нутриентов в почву.
## Открытые вопросы
- Конкретный список `ThoughtDef`-событий для оленя (набор и величины offset/длительности)
— детализируется при реализации A8.
- Ассет: создать/найти спрайт скелета (`things/pawn/animal/deer/Skeleton_*`) к фазе A5.
-84
View File
@@ -1,84 +0,0 @@
# Растения как экосистема
Дизайн системы растений: рост зависит от среды, среда описана факторами, реакция растения на
факторы задаётся **генами**, а размножение передаёт гены потомкам с мутацией — появляется
естественный отбор. Документ — единый ориентир для фаз 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: d044cafad9...46f3931f85
+3 -1
View File
@@ -9,7 +9,9 @@ $Project = Join-Path $Root "src\LittleSim\LittleSim.csproj"
Push-Location $Root Push-Location $Root
try { try {
dotnet build LittleSim.sln -c $Configuration # Только игровой проект (тянет за собой нужные движковые библиотеки) — не вся солюшн с
# тестами, сервером и WASM-клиентом (его emscripten-сборка занимает минуты).
dotnet build $Project -c $Configuration
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE exit $LASTEXITCODE
} }
+3 -1
View File
@@ -18,5 +18,7 @@ done
cd "$ROOT" cd "$ROOT"
dotnet build LittleSim.sln -c "$CONFIGURATION" # Только игровой проект (тянет за собой нужные движковые библиотеки) — не вся солюшн с
# тестами, сервером и WASM-клиентом (его emscripten-сборка занимает минуты).
dotnet build src/LittleSim/LittleSim.csproj -c "$CONFIGURATION"
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@" dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
+17
View File
@@ -0,0 +1,17 @@
namespace LittleSim.Web;
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
public enum ConnectionStatus
{
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
Idle,
/// <summary>Идёт первая попытка подключения.</summary>
Connecting,
/// <summary>Соединение установлено, снапшоты приходят.</summary>
Connected,
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
Reconnecting,
}
+1
View File
@@ -18,6 +18,7 @@
<Compile Include="Pages\Index.razor.cs" /> <Compile Include="Pages\Index.razor.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="LittleSimWebGame.cs" /> <Compile Include="LittleSimWebGame.cs" />
<Compile Include="ConnectionStatus.cs" />
<Compile Include="NetContract.cs" /> <Compile Include="NetContract.cs" />
<Compile Include="WorldViewScene.cs" /> <Compile Include="WorldViewScene.cs" />
</ItemGroup> </ItemGroup>
+14 -5
View File
@@ -16,24 +16,33 @@ public class LittleSimWebGame : Game
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary> /// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
public EngineContext Context { get; } = new EngineContext(); public EngineContext Context { get; } = new EngineContext();
private readonly Uri _server;
private GraphicsDeviceManager _graphics; private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch = null!; private SpriteBatch _spriteBatch = null!;
private Texture2D _pixel = null!; private Texture2D _pixel = null!;
private WorldViewScene _scene = null!;
/// <summary>Игра, подключающаяся к серверу <paramref name="server"/>.</summary> /// <summary>Игра-наблюдатель: мира не создаёт, ждёт <see cref="Connect"/> из UI.</summary>
public LittleSimWebGame(Uri server) public LittleSimWebGame()
{ {
_server = server;
_graphics = new GraphicsDeviceManager(this); _graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content"; Content.RootDirectory = "Content";
} }
/// <summary>Состояние соединения со сценой-наблюдателем — читает оверлей подключения.</summary>
public ConnectionStatus Status => _scene?.Status ?? ConnectionStatus.Idle;
/// <summary>Число реплицированных жителей — для статуса в оверлее.</summary>
public int EntityCount => _scene?.EntityCount ?? 0;
/// <summary>Подключиться к серверу <paramref name="server"/> (вызывается из Blazor-оверлея).</summary>
public void Connect(Uri server) => _scene?.Connect(server);
/// <inheritdoc /> /// <inheritdoc />
protected override void Initialize() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
Context.Scenes.Switch(new WorldViewScene(_server, () => _spriteBatch, () => _pixel)); _scene = new WorldViewScene(() => _spriteBatch, () => _pixel);
Context.Scenes.Switch(_scene);
} }
/// <inheritdoc /> /// <inheritdoc />
+31 -2
View File
@@ -1,3 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using Friflo.Engine.ECS; using Friflo.Engine.ECS;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Net; using MrGameEng.Net;
@@ -36,7 +38,34 @@ public static class WebNetSchema
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary> /// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
public const int DefaultPort = 9050; public const int DefaultPort = 9050;
// Те же wire-размеры, что и в десктопном NetSchema.cs — проверяем, что зеркало не разошлось по раскладке.
private const int TransformWireSize = 20; // Vector2(8) + float(4) + Vector2(8)
private const int PawnNeedsWireSize = 4; // float(4)
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary> /// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
public static ReplicationSchema Create() => public static ReplicationSchema Create()
new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>(); {
VerifyLayout();
return new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
}
/// <summary>Сверяет байт-раскладку зеркальных структур с десктопным контрактом (NetSchema.cs).</summary>
private static void VerifyLayout()
{
Require<NetTransform>(TransformWireSize);
Require<NetPawnNeeds>(PawnNeedsWireSize);
static void Require<T>(int expected)
where T : struct
{
var actual = Unsafe.SizeOf<T>();
if (actual != expected)
{
throw new InvalidOperationException(
$"Размер зеркального {typeof(T).Name} = {actual} Б, ожидался {expected} Б: "
+ "wire-контракт разошёлся — синхронизируй NetContract.cs и LittleSim/Net/NetSchema.cs."
);
}
}
}
} }
+10 -1
View File
@@ -1,4 +1,4 @@
@page "/" @page "/"
@page "/index.html" @page "/index.html"
@inject IJSRuntime JsRuntime @inject IJSRuntime JsRuntime
@using nkast.Wasm.Canvas @using nkast.Wasm.Canvas
@@ -18,3 +18,12 @@
"> ">
<canvas id="theCanvas" style="touch-action:none;"></canvas> <canvas id="theCanvas" style="touch-action:none;"></canvas>
</div> </div>
<div class="connect-bar">
<span class="title">LittleSim</span>
<input class="server" placeholder="ws://host:9050"
@bind="_serverAddress" @bind:event="oninput"
@onkeydown="OnAddressKey" />
<button class="connect" @onclick="Connect">Подключиться</button>
<span class="status @StatusClass">@StatusText</span>
</div>
+80 -8
View File
@@ -1,7 +1,7 @@
using System; using System;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Microsoft.Xna.Framework;
namespace LittleSim.Web.Pages namespace LittleSim.Web.Pages
{ {
@@ -10,7 +10,12 @@ namespace LittleSim.Web.Pages
[Inject] [Inject]
private NavigationManager Navigation { get; set; } = null!; private NavigationManager Navigation { get; set; } = null!;
private Game? _game; private LittleSimWebGame? _game;
private string _serverAddress = "";
private Uri? _pendingServer; // запрошен Connect до создания игры — подключим в первом тике
private ConnectionStatus _lastStatus = ConnectionStatus.Idle;
private int _lastCount = -1;
private int _frame;
protected override void OnAfterRender(bool firstRender) protected override void OnAfterRender(bool firstRender)
{ {
@@ -18,25 +23,92 @@ namespace LittleSim.Web.Pages
if (firstRender) if (firstRender)
{ {
_serverAddress = DefaultServerAddress();
StateHasChanged();
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this)); JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
} }
} }
// Гонится из requestAnimationFrame (index.html). Игру создаём лениво в первом тике —
// мира она не создаёт, лишь ждёт адрес сервера из формы.
[JSInvokable] [JSInvokable]
public void TickDotNet() public void TickDotNet()
{ {
if (_game == null) if (_game == null)
{ {
_game = new LittleSimWebGame(ResolveServerUri()); _game = new LittleSimWebGame();
_game.Run(); _game.Run();
} }
_game.Tick(); _game.Tick();
if (_pendingServer is not null)
{
_game.Connect(_pendingServer);
_pendingServer = null;
}
// Перерисовываем оверлей только при смене статуса (или изредка — чтобы освежить счётчик),
// а не каждый кадр rAF.
var status = _game.Status;
var count = _game.EntityCount;
if (
status != _lastStatus
|| (status == ConnectionStatus.Connected && count != _lastCount && _frame % 15 == 0)
)
{
_lastStatus = status;
_lastCount = count;
InvokeAsync(StateHasChanged);
}
_frame++;
} }
// Адрес сервера: ?server=ws://host:port в URL страницы; по умолчанию — private void OnAddressKey(KeyboardEventArgs e)
// хост самой страницы на порту LittleSim.Server. {
private Uri ResolveServerUri() if (e.Key == "Enter")
{
Connect();
}
}
private void Connect()
{
if (!Uri.TryCreate(_serverAddress?.Trim(), UriKind.Absolute, out var uri))
{
return; // адрес не похож на ws://… — игнорируем
}
if (_game is not null)
{
_game.Connect(uri);
}
else
{
_pendingServer = uri;
}
}
private string StatusText =>
_lastStatus switch
{
ConnectionStatus.Connecting => "подключение…",
ConnectionStatus.Connected => $"подключено · {_lastCount} жит.",
ConnectionStatus.Reconnecting => "переподключение…",
_ => "не подключено",
};
private string StatusClass =>
_lastStatus switch
{
ConnectionStatus.Connected => "ok",
ConnectionStatus.Connecting or ConnectionStatus.Reconnecting => "warn",
_ => "idle",
};
// Адрес по умолчанию: ?server=ws://host:port из URL страницы, иначе хост страницы на порту сервера.
private string DefaultServerAddress()
{ {
var page = new Uri(Navigation.Uri); var page = new Uri(Navigation.Uri);
var query = page.Query.TrimStart('?'); var query = page.Query.TrimStart('?');
@@ -45,7 +117,7 @@ namespace LittleSim.Web.Pages
var separator = pair.IndexOf('='); var separator = pair.IndexOf('=');
if (separator > 0 && pair[..separator] == "server") if (separator > 0 && pair[..separator] == "server")
{ {
return new Uri(Uri.UnescapeDataString(pair[(separator + 1)..])); return Uri.UnescapeDataString(pair[(separator + 1)..]);
} }
} }
@@ -54,7 +126,7 @@ namespace LittleSim.Web.Pages
Scheme = "ws", Scheme = "ws",
Host = page.Host, Host = page.Host,
Port = WebNetSchema.DefaultPort, Port = WebNetSchema.DefaultPort,
}.Uri; }.Uri.ToString();
} }
} }
} }
+72
View File
@@ -0,0 +1,72 @@
.connect-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
box-sizing: border-box;
background: rgba(12, 16, 24, 0.82);
border-bottom: 1px solid rgba(108, 198, 255, 0.25);
color: #d8e0ea;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
z-index: 10;
}
.connect-bar .title {
font-weight: 600;
color: #6cc6ff;
letter-spacing: 0.02em;
}
.connect-bar .server {
flex: 0 1 320px;
min-width: 0;
padding: 5px 9px;
background: #0c1018;
border: 1px solid #2a3850;
border-radius: 4px;
color: #e6edf6;
font-family: ui-monospace, 'Cascadia Code', monospace;
font-size: 13px;
}
.connect-bar .server:focus {
outline: none;
border-color: #6cc6ff;
}
.connect-bar .connect {
padding: 5px 14px;
background: #1c2c44;
border: 1px solid #3a567f;
border-radius: 4px;
color: #d8e0ea;
font-size: 13px;
cursor: pointer;
}
.connect-bar .connect:hover {
background: #25395a;
border-color: #6cc6ff;
}
.connect-bar .status {
margin-left: auto;
font-variant-numeric: tabular-nums;
}
.connect-bar .status.ok {
color: #7fdca0;
}
.connect-bar .status.warn {
color: #f0c060;
}
.connect-bar .status.idle {
color: #8a96a6;
}
+77 -17
View File
@@ -10,30 +10,57 @@ using MrGameEng.Net;
namespace LittleSim.Web; namespace LittleSim.Web;
/// <summary> /// <summary>
/// Браузерный клиент мира LittleSim: подключается к дедикейтед-серверу /// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
/// (LittleSim.Server --listen), применяет дельта-снапшоты в свой EntityStore и рисует /// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
/// жителей через KNI SpriteBatch (WebGL). Симуляция целиком на сервере — сюда приезжают /// EntityStore и рисует жителей через KNI SpriteBatch (WebGL). Мира не создаёт — только
/// только компоненты схемы (позиция + потребности); позиции сглаживаются до частоты /// наблюдает: симуляция целиком на сервере, сюда приезжают лишь компоненты схемы (позиция +
/// кадра, усталость затемняет квадратик жителя. /// потребности). Позиции сглаживаются до частоты кадра, усталость затемняет квадратик. При
/// обрыве переподключается сам с экспоненциальным бэкоффом, очищая устаревшие сущности.
/// </summary> /// </summary>
public sealed class WorldViewScene : Scene public sealed class WorldViewScene : Scene
{ {
private readonly Uri _server; // Бэкофф переподключения: задержка удваивается с каждой неудачей до потолка.
private const float MaxBackoffSeconds = 8f;
private readonly Func<SpriteBatch> _spriteBatch; private readonly Func<SpriteBatch> _spriteBatch;
private readonly Func<Texture2D> _pixel; private readonly Func<Texture2D> _pixel;
private ReplicationClient _replication = null!; private ReplicationClient _replication = null!;
private Task<WebSocketClient>? _connecting; private Task<WebSocketClient>? _connecting;
private WebSocketClient? _connection; private WebSocketClient? _connection;
private Uri? _server; // целевой сервер; null — пользователь ещё не подключался
private double _nextAttemptAt;
private int _attempt;
/// <summary>Сцена, подключающаяся к <paramref name="server"/>.</summary> /// <summary>Текущее состояние соединения — для оверлея подключения.</summary>
public WorldViewScene(Uri server, Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel) public ConnectionStatus Status { get; private set; } = ConnectionStatus.Idle;
/// <summary>Сколько реплицированных жителей сейчас в мире клиента.</summary>
public int EntityCount => _replication?.EntityCount ?? 0;
/// <summary>Сцена-наблюдатель; сервер задаётся позже через <see cref="Connect"/>.</summary>
public WorldViewScene(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
{ {
_server = server;
_spriteBatch = spriteBatch; _spriteBatch = spriteBatch;
_pixel = pixel; _pixel = pixel;
} }
/// <summary>
/// Подключиться к <paramref name="server"/> (ws:// или wss://). Сбрасывает прежнее
/// соединение и счётчик попыток — подключение начнётся в ближайшем тике. Дальше клиент сам
/// переподключается к этому адресу при обрывах.
/// </summary>
public void Connect(Uri server)
{
_connection?.Close();
_connection = null;
_replication?.Clear();
_server = server;
_attempt = 0;
_nextAttemptAt = 0f;
Status = ConnectionStatus.Connecting;
}
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary> /// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
private struct NetLerp : IComponent private struct NetLerp : IComponent
{ {
@@ -51,35 +78,68 @@ public sealed class WorldViewScene : Scene
UpdateSystems.Add(new NetSmoothingSystem()); UpdateSystems.Add(new NetSmoothingSystem());
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel)); DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
Log.Info($"LittleSim.Web: connecting to {_server}…"); Log.Info("LittleSim.Web ready — awaiting connect");
_connecting = WebSocketClient.ConnectAsync(_server);
} }
protected override void OnUnload() => _connection?.Close(); protected override void OnUnload() => _connection?.Close();
private void Pump() private void Pump()
{ {
if (_server is null)
{
return; // адрес ещё не задан — ждём команды Connect из оверлея
}
var now = Context.Clock.UnscaledTotalTime;
// Завершилась попытка подключения: успех — берём соединение; провал — планируем повтор.
if (_connecting is { IsCompleted: true } finished) if (_connecting is { IsCompleted: true } finished)
{ {
_connecting = null; _connecting = null;
if (finished.IsFaulted) if (finished.IsCompletedSuccessfully)
{
_connection = finished.Result;
_replication.Clear(); // сбрасываем устаревшие сущности перед свежим полным снапшотом
_attempt = 0;
Status = ConnectionStatus.Connected;
Log.Info($"Connected to {_server}");
}
else
{ {
Log.Error( Log.Error(
$"Connect to {_server} failed: " $"Connect to {_server} failed: "
+ finished.Exception?.GetBaseException().Message + finished.Exception?.GetBaseException().Message
); );
ScheduleReconnect(now);
} }
else }
{
_connection = finished.Result; // Обрыв установленного соединения: чистим и уходим в переподключение.
Log.Info($"Connected to {_server}"); if (_connection is { IsOpen: false })
} {
_connection = null;
_replication.Clear();
ScheduleReconnect(now);
} }
if (_connection is not null) if (_connection is not null)
{ {
_replication.Pump(_connection); _replication.Pump(_connection);
} }
else if (_connecting is null && now >= _nextAttemptAt)
{
Status = _attempt == 0 ? ConnectionStatus.Connecting : ConnectionStatus.Reconnecting;
_connecting = WebSocketClient.ConnectAsync(_server);
}
}
// Экспоненциальный бэкофф: 1, 2, 4, 8, 8, … секунд между попытками.
private void ScheduleReconnect(double now)
{
var delay = MathF.Min(MaxBackoffSeconds, 1f * (1 << Math.Min(_attempt, 3)));
_attempt++;
_nextAttemptAt = now + delay;
Status = ConnectionStatus.Reconnecting;
} }
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary> /// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
+4 -62
View File
@@ -1,8 +1,3 @@
using System.Text.Json;
using Microsoft.Xna.Framework;
using MrGameEng.Audio;
using MrGameEng.Mods;
namespace LittleSim.App; namespace LittleSim.App;
/// <summary> /// <summary>
@@ -28,63 +23,10 @@ public sealed class GameSettings
/// <summary>Общая громкость 0..1.</summary> /// <summary>Общая громкость 0..1.</summary>
public float Volume { get; set; } = 1f; public float Volume { get; set; } = 1f;
}
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary> /// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
public static class GameSettingsStore public float UiScale { get; set; } = 1f;
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true,
};
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary> /// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
public static GameSettings Load() public bool DeveloperMode { get; set; } = true;
{
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();
}
} }
+65
View File
@@ -0,0 +1,65 @@
using System.Text.Json;
using Microsoft.Xna.Framework;
using MrGameEng.Audio;
using MrGameEng.Mods;
namespace LittleSim.App;
/// <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();
}
}
+107
View File
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using Friflo.Engine.ECS.Systems;
using MrGameEng.DevConsole;
namespace LittleSim.App;
/// <summary>
/// Дев-инструмент: per-system тайминги Friflo. Включает мониторинг (<c>SetMonitorPerf</c>) на корнях
/// систем и печатает в дев-консоль командой <c>perf</c> отсортированную по времени таблицу систем
/// (среднее мс/кадр по последним кадрам + доля от корня). Доступно только в режиме разработчика.
/// </summary>
public static class PerfReport
{
private const int DefaultTop = 15; // сколько самых дорогих систем печатать по умолчанию
/// <summary>Окно усреднения времени (последние N обновлений) — используют и консоль, и perf-окно.</summary>
public const int AvgFrames = 30;
private static readonly List<(string Name, double AvgMs, int Updates)> Scratch = []; // без аллокаций на вызов
/// <summary>Включает мониторинг на корнях и регистрирует команду <c>perf [N]</c> (топ-N систем).</summary>
public static void Register(DevConsole console, params SystemRoot[] roots)
{
EnableMonitoring(roots);
console.Register(
"perf",
"perf [N] — тайминги систем (среднее мс/кадр, топ-N по времени)",
(c, args) =>
{
// Мониторинг пере-утверждаем на случай систем, добавленных после Register (идемпотентно).
EnableMonitoring(roots);
var top =
args.Length > 0 && int.TryParse(args[0], out var n) && n > 0 ? n : DefaultTop;
foreach (var root in roots)
{
WriteRoot(c, root, top);
}
}
);
}
/// <summary>Включает per-system мониторинг Friflo на корнях (идемпотентно; можно звать повторно для
/// систем, добавленных позже). Публично — чтобы perf-окно тоже могло его утвердить.</summary>
public static void EnableMonitoring(params SystemRoot[] roots)
{
foreach (var root in roots)
{
root.SetMonitorPerf(true);
}
}
/// <summary>
/// Заполняет <paramref name="into"/> строками (имя, среднее мс/кадр, число обновлений) по листовым
/// системам корня, сортированными по времени убыв.; возвращает суммарное среднее мс/кадр корня
/// (0, если данных ещё нет). Общий сбор для консольной команды и perf-окна — без дублирования.
/// </summary>
public static double Snapshot(
SystemRoot root,
List<(string Name, double AvgMs, int Updates)> into
)
{
into.Clear();
Collect(root, into);
into.Sort((a, b) => b.AvgMs.CompareTo(a.AvgMs));
return root.Perf.UpdateCount == 0 ? 0.0 : root.Perf.LastAvgMs(AvgFrames);
}
private static void WriteRoot(DevConsole console, SystemRoot root, int top)
{
var total = Snapshot(root, Scratch);
if (root.Perf.UpdateCount == 0)
{
console.WriteLine($"[{root.Name}] нет данных — подожди кадр и повтори 'perf'.");
return;
}
console.WriteLine($"[{root.Name}] {total:0.000} мс/кадр · систем: {Scratch.Count}");
var shown = System.Math.Min(top, Scratch.Count);
for (var i = 0; i < shown; i++)
{
var (name, avg, _) = Scratch[i];
var pct = total > 0.0001 ? avg / total * 100.0 : 0.0;
console.WriteLine($" {name, -30} {avg, 8:0.000} мс {pct, 3:0}%");
}
if (Scratch.Count > shown)
{
console.WriteLine($" … ещё {Scratch.Count - shown} систем(ы)");
}
}
// Рекурсивно собирает листовые системы (группы разворачиваются — интересны конкретные системы).
private static void Collect(SystemGroup group, List<(string, double, int)> into)
{
foreach (var child in group.ChildSystems)
{
if (child is SystemGroup sub)
{
Collect(sub, into);
}
else
{
into.Add((child.Name, child.Perf.LastAvgMs(AvgFrames), child.Perf.UpdateCount));
}
}
}
}
+75
View File
@@ -0,0 +1,75 @@
using MrGameEng.Genetics;
namespace LittleSim.App;
/// <summary>
/// Сериализуемое состояние одного животного (фазы A1–A8): вид, позиция, геном (источник признаков и
/// пола), поколение, стадия/возраст, нужды, здоровье (кровь, HP частей, хедифы), настроение и
/// беременность. Спрайт пересобирается фабрикой из дефа. Чтобы популяция (эволюция/состояние)
/// переживала сейв/лоад, а не пересыпалась заново из сида.
/// </summary>
public sealed class AnimalSave
{
/// <summary>Имя дефа вида — по нему берётся индекс/стадии/текстуры/тело.</summary>
public string Species { get; set; } = "";
/// <summary>Позиция X в мировых координатах.</summary>
public float X { get; set; }
/// <summary>Позиция Y в мировых координатах.</summary>
public float Y { get; set; }
/// <summary>Номер поколения (для popstats/дрейфа).</summary>
public int Generation { get; set; }
/// <summary>Текущая стадия роста.</summary>
public int Stage { get; set; }
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays { get; set; }
/// <summary>Геном особи: аллели по id генов (переменного состава; несёт и пол через GeneSex).</summary>
public Dictionary<string, Allele> Genome { get; set; } = new();
/// <summary>Значения нужд по индексам NeedSet.</summary>
public float[] Needs { get; set; } = [];
/// <summary>Накоплено дней истощения.</summary>
public float Starve { get; set; }
/// <summary>Уровень крови [0,1].</summary>
public float BloodLevel { get; set; } = 1f;
/// <summary>HP частей тела по порядку анатомии вида (пусто — у вида нет тела).</summary>
public float[] PartHp { get; set; } = [];
/// <summary>Активные хедифы (гон/болезни).</summary>
public List<HediffSave> Hediffs { get; set; } = [];
/// <summary>Текущее настроение [0,1].</summary>
public float Mood { get; set; }
/// <summary>Активно ли настроение (тир 4+).</summary>
public bool MoodActive { get; set; }
/// <summary>Активные событийные мысли.</summary>
public List<ThoughtSave> Thoughts { get; set; } = [];
/// <summary>Беременна ли самка.</summary>
public bool Pregnant { get; set; }
/// <summary>Геном отца (если беременна) — «слепок» на момент зачатия.</summary>
public Dictionary<string, Allele> FatherGenome { get; set; } = new();
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
public float DueInDays { get; set; }
/// <summary>Размер помёта.</summary>
public int Litter { get; set; }
/// <summary>Уровни навыков [0..1] по индексам SkillSet.</summary>
public float[] SkillLevels { get; set; } = [];
/// <summary>Страсть к навыкам (0/1/2) по индексам SkillSet.</summary>
public int[] SkillPassions { get; set; } = [];
}
+23
View File
@@ -0,0 +1,23 @@
namespace LittleSim.App;
/// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary>
public sealed class CorpseSave
{
/// <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 RotDays { get; set; }
/// <summary>Размер тела (для масштаба спрайта).</summary>
public float BodySize { get; set; }
/// <summary>Доступное мясо.</summary>
public float Meat { get; set; }
}
+25
View File
@@ -0,0 +1,25 @@
using MrGameEng.Genetics;
namespace LittleSim.App;
/// <summary>Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.</summary>
public sealed class EggSave
{
/// <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 int Generation { get; set; }
/// <summary>Остаток инкубации в игровых днях.</summary>
public float IncubateDays { get; set; }
/// <summary>Геном будущего детёныша (уже скрещён на момент кладки).</summary>
public Dictionary<string, Allele> Genome { get; set; } = new();
}
+14
View File
@@ -0,0 +1,14 @@
namespace LittleSim.App;
/// <summary>Сериализуемый хедиф особи (фаза A6): деф (по имени), тяжесть и набранный иммунитет.</summary>
public sealed class HediffSave
{
/// <summary>Имя дефа хедифа (гон/болезнь).</summary>
public string DefName { get; set; } = "";
/// <summary>Текущая тяжесть.</summary>
public float Severity { get; set; }
/// <summary>Набранный иммунитет [0,1].</summary>
public float Immunity { get; set; }
}
+28
View File
@@ -0,0 +1,28 @@
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();
}
+92
View File
@@ -0,0 +1,92 @@
using System.Text.Json;
namespace LittleSim.App;
/// <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 && save.Version <= WorldSave.CurrentVersion)
{
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 readonly HashSet<char> InvalidFileNameChars = new(
Path.GetInvalidFileNameChars()
);
private static string Sanitize(string name)
{
var trimmed = name.Trim();
var safe = string.Concat(trimmed.Select(c => InvalidFileNameChars.Contains(c) ? '_' : c));
return string.IsNullOrEmpty(safe) ? "world" : safe;
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace LittleSim.App;
/// <summary>Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания.</summary>
public sealed class ThoughtSave
{
/// <summary>Имя дефа мысли.</summary>
public string DefName { get; set; } = "";
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
public float RemainingDays { get; set; }
}
+64
View File
@@ -0,0 +1,64 @@
namespace LittleSim.App;
/// <summary>
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
/// детерминированно регенерируется из сида при загрузке.
/// </summary>
public sealed class WorldSave
{
/// <summary>Текущая версия схемы сейва (поднимать при несовместимых изменениях полей).</summary>
public const int CurrentVersion = 1;
/// <summary>Версия схемы файла. Старые сейвы без поля совместимы (схема аддитивна) и читаются как
/// текущая версия; проверка нужна, чтобы отклонить файл из БУДУЩЕЙ (несовместимой) версии.</summary>
public int Version { get; set; } = CurrentVersion;
/// <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<PlantSave> Plants { get; set; } = [];
/// <summary>Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность).</summary>
public List<AnimalSave> Animals { get; set; } = [];
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
public List<CorpseSave> Corpses { get; set; } = [];
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
public List<EggSave> Eggs { get; set; } = [];
/// <summary>Конфиг мира для пересоздания сцены.</summary>
public WorldConfig ToConfig() =>
new()
{
Name = Name,
Width = Width,
Height = Height,
Seed = Seed,
SmoothPasses = SmoothPasses,
Population = Population,
};
}
-196
View File
@@ -1,196 +0,0 @@
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;
}
}
+253
View File
@@ -0,0 +1,253 @@
using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Content;
/// <summary>
/// Готовая (resolved) таблица животных для сцены: по каждому <see cref="AnimalDef"/> — загруженный
/// регион атласа и набор генов вида (<see cref="GenomeTemplate"/>) для генерации особей. Строится
/// один раз при загрузке сцены (нужны атлас и устройство), как <see cref="PlantSet"/>. Системы и
/// фабрика индексируются в неё без словарей. Несёт направленные спрайты (восток/север/юг + флип) по
/// полу/стадии, спрайты трупа/яйца, анатомию, шаблон генома и предпосчёты для горячих путей.
/// </summary>
public sealed class AnimalSet
{
/// <summary>Направленные спрайты одной «внешности» (пол/стадия): восток, север, юг. Запад = зеркало
/// востока (флип по X). Север/юг берутся по конвенции имени (_east→_north/_south), иначе = восток.</summary>
public readonly record struct Directional(
Texture2DRegion East,
Texture2DRegion North,
Texture2DRegion South
);
/// <summary>Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи.</summary>
public sealed class Species
{
/// <summary>Исходный деф животного.</summary>
public required AnimalDef Def { get; init; }
/// <summary>Направленные спрайты взрослой самки (она же общий/fallback).</summary>
public required Directional FemaleDirs { get; init; }
/// <summary>Направленные спрайты взрослого самца (fallback — самка).</summary>
public required Directional MaleDirs { get; init; }
/// <summary>Направленные спрайты детёныша/подростка (fallback — самка).</summary>
public required Directional BabyDirs { get; init; }
/// <summary>Спрайт трупа (fallback — самка, восток).</summary>
public required Texture2DRegion Corpse { get; init; }
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
public required Texture2DRegion Egg { get; init; }
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
public required AnimalStageDef[] Stages { get; init; }
/// <summary>Анатомия вида (части тела) или null, если вид без тела.</summary>
public required BodyDef? Body { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
public required GenomeTemplate Template { get; init; }
/// <summary>Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей
/// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле.</summary>
public required float RefBodySize { get; init; }
/// <summary>Направленные спрайты для стадии/пола: детёныш → BabyDirs; иначе по полу (самец/самка).</summary>
public Directional LookDirs(int stage, bool isMale) =>
string.Equals(Stages[stage].Texture, "baby", StringComparison.Ordinal) ? BabyDirs
: isMale ? MaleDirs
: FemaleDirs;
}
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
private static readonly AnimalStageDef[] DefaultStages =
[
new()
{
Name = "Baby",
EnterAt = 0f,
RelativeTo = "maturity",
Scale = 0.45f,
Texture = "baby",
},
new()
{
Name = "Juvenile",
EnterAt = 0.22f,
RelativeTo = "maturity",
Scale = 0.7f,
Texture = "baby",
},
new()
{
Name = "Adult",
EnterAt = 1f,
RelativeTo = "maturity",
Scale = 1f,
Texture = "adult",
Adult = true,
},
new()
{
Name = "Senior",
EnterAt = 0.8f,
RelativeTo = "lifespan",
Scale = 0.92f,
Texture = "adult",
Adult = true,
},
];
private readonly Species[] _species;
private readonly Dictionary<AnimalDef, int> _index = new();
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков животных.</summary>
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
/// <summary>Строит таблицу из всех (не-abstract) <see cref="AnimalDef"/> мода.</summary>
public AnimalSet(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<AnimalDef>().ToArray();
_species = new Species[defs.Length];
for (var i = 0; i < defs.Length; i++)
{
var def = defs[i];
var femaleKey = def.Texture;
var maleKey = string.IsNullOrEmpty(def.MaleTexture) ? femaleKey : def.MaleTexture;
var babyKey = string.IsNullOrEmpty(def.BabyTexture) ? femaleKey : def.BabyTexture;
_species[i] = new Species
{
Def = def,
FemaleDirs = LoadDirs(atlases, device, femaleKey),
MaleDirs = LoadDirs(atlases, device, maleKey),
BabyDirs = LoadDirs(atlases, device, babyKey),
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
? atlases.GetRegion(device, femaleKey)
: atlases.GetRegion(device, def.CorpseTexture),
Egg = string.IsNullOrEmpty(def.EggTexture)
? atlases.GetRegion(device, femaleKey)
: atlases.GetRegion(device, def.EggTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
Template = BuildTemplate(def.Genome, genes),
RefBodySize =
def.Genome.TryGetValue("GeneMaxBodySize", out var refSize) && refSize > 0f
? refSize
: 1f,
};
_index[def] = i;
}
}
// Грузит направленный набор по восточному ключу: север/юг — по конвенции имени (_east→_north/_south),
// если такой регион есть в атласе; иначе откат на восток (вид с одним спрайтом просто не разворачивается
// вверх/вниз, но влево/вправо — флипом востока). Запад всегда = зеркало востока.
private static Directional LoadDirs(ModAtlases atlases, GraphicsDevice device, string eastKey)
{
var east = atlases.GetRegion(device, eastKey);
return new Directional(
east,
TryDir(atlases, device, eastKey, "_north", east),
TryDir(atlases, device, eastKey, "_south", east)
);
}
private static Texture2DRegion TryDir(
ModAtlases atlases,
GraphicsDevice device,
string eastKey,
string suffix,
Texture2DRegion fallback
) =>
eastKey.Contains("_east", StringComparison.Ordinal)
&& atlases.TryGetRegion(
device,
eastKey.Replace("_east", suffix, StringComparison.Ordinal),
out var region
)
? region
: fallback;
/// <summary>Число видов в таблице.</summary>
public int Count => _species.Length;
/// <summary>Вид по индексу.</summary>
public Species this[int index] => _species[index];
/// <summary>Индекс вида по дефу.</summary>
public int IndexOf(AnimalDef def) => _index[def];
/// <summary>
/// Генерирует геном новой особи-основателя вида: аллели из шаблона + явно назначенный пол
/// (XX самка / XY самец, 50/50) — чтобы не возник невозможный YY (см. Defs/Genes/Animal.json GeneSex).
/// Скрещивание (A4) наследует пол само (мать XX отдаёт только X).
/// </summary>
public Genome GenerateGenome(int species, Random random)
{
var genome = _species[species].Template.Generate(random);
genome["GeneSex"] = random.NextSingle() < 0.5f ? new Allele(0f, 0f) : new Allele(0f, 1f);
return genome;
}
/// <summary>
/// ОСНОВА создания гибрида (полноценная команда/UI — позже): геном помеси, привязанный к базовому виду
/// <paramref name="baseSpecies"/> (его тело/спрайт/набор генов), но с общими генами, скрещёнными с
/// <paramref name="otherSpecies"/> (по одной аллели-гамете от каждого родителя, как при размножении).
/// Пол берётся от базового вида; гены, которых нет у базового, игнорируются (его тело их не читает).
/// Сущность создаётся обычной <see cref="Sim.AnimalFactory.Create"/> с этим геномом и базовым видом —
/// то есть «создать любой вид» = Create(вид, GenerateGenome), «любой гибрид» = Create(вид, HybridGenome).
/// </summary>
public Genome HybridGenome(int baseSpecies, int otherSpecies, Random random)
{
var hybrid = GenerateGenome(baseSpecies, random);
var other = _species[otherSpecies].Template.Generate(random);
foreach (var (geneId, allele) in other.ToDictionary())
{
if (string.Equals(geneId, "GeneSex", StringComparison.Ordinal) || !hybrid.Has(geneId))
{
continue; // пол — от базового вида; чужие гены базовое тело не читает
}
var mine = hybrid[geneId];
hybrid[geneId] = new Allele(
random.NextSingle() < 0.5f ? mine.A : mine.B,
random.NextSingle() < 0.5f ? allele.A : allele.B
);
}
return hybrid;
}
// Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
private static GenomeTemplate BuildTemplate(
IReadOnlyDictionary<string, float> genome,
IReadOnlyDictionary<string, GeneDef> genes
)
{
var entries = new List<GenomeTemplate.Entry>(genome.Count);
foreach (var (geneId, baseValue) in genome)
{
if (!genes.TryGetValue(geneId, out var gene))
{
throw new KeyNotFoundException(
$"Animal genome references unknown gene '{geneId}' — check Defs/Genes/."
);
}
var spread = gene.Kind == GeneKind.Discrete ? 0f : gene.Spread;
entries.Add(new GenomeTemplate.Entry(gene, baseValue, spread));
}
return new GenomeTemplate(entries);
}
}
+73
View File
@@ -0,0 +1,73 @@
namespace LittleSim.Content;
/// <summary>
/// Вид-животное (Defs/Animals/): подтип <see cref="PawnDef"/> (текстура/размер/вид) плюс геном
/// вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе дефа —
/// существующие 9 животных-<see cref="PawnDef"/> остаются валидными.
/// </summary>
public sealed class AnimalDef : PawnDef
{
/// <summary>Текстура взрослого самца (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/> (самка/общая).</summary>
public string MaleTexture { get; init; } = "";
/// <summary>Текстура детёныша/подростка (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string BabyTexture { get; init; } = "";
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string CorpseTexture { get; init; } = "";
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
public string EggTexture { get; init; } = "things/plant/seed_default";
/// <summary>Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³.</summary>
public float BaseMassKg { get; init; } = 40f;
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
public string Body { get; init; } = "";
/// <summary>
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
/// кода; разброс берётся из самого гена (<c>GeneDef.Spread</c>). Дискретные гены (напр. пол) сюда
/// не пишутся — назначаются генерацией.
/// </summary>
public Dictionary<string, float> Genome { get; init; } = new();
/// <summary>
/// Диета как ДАННЫЕ: что вид ест ("plant" — травоядное; "meat" — хищник/падальщик; оба — всеядное).
/// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода.
/// </summary>
public string[] Diet { get; init; } = [];
/// <summary>
/// Стадии роста (данные); пусто — встроенный набор Baby→Juvenile→Adult→Senior по умолчанию.
/// </summary>
public AnimalStageDef[] Stages { get; init; } = [];
/// <summary>Плотность стартового спавна: голов на 1000 клеток суши (0 — вид не спавнится сам).</summary>
public float SpawnPer1000Cells { get; init; }
/// <summary>Базовая скорость передвижения (мировых единиц/сек при гене moveSpeed = 1).</summary>
public float BaseSpeed { get; init; } = 28f;
/// <summary>Базовый радиус восприятия в клетках (× ген vision) — поиск корма/воды/партнёра.</summary>
public float VisionCells { get; init; } = 14f;
/// <summary>Сила выедания: на сколько игровых дней роста убавляется растение за день кормёжки.</summary>
public float ForageBiteDays { get; init; } = 40f;
/// <summary>Дальность атаки в клетках: на этом расстоянии хищник кусает добычу (контакт).</summary>
public float AttackRangeCells { get; init; } = 1.2f;
/// <summary>Игровые секунды между ударами (кулдаун дискретной атаки, как в RimWorld).</summary>
public float AttackIntervalSec { get; init; } = 1.8f;
/// <summary>Урон части тела добычи ЗА ОДИН УДАР (×силу: размер×навык). 0 — вид не атакует.</summary>
public float AttackDamage { get; init; }
/// <summary>Прирост тяжести раны-кровотечения у добычи за один удар (×силу).</summary>
public float AttackBleed { get; init; }
/// <summary>Острая кровопотеря добычи за один удар (доля объёма крови, ×силу) — основной путь к смерти.</summary>
public float AttackBloodLoss { get; init; }
}
@@ -0,0 +1,28 @@
namespace LittleSim.Content;
/// <summary>
/// Стадия роста животного как ДАННЫЕ (вложенный объект <see cref="AnimalDef.Stages"/>): возраст входа =
/// <see cref="EnterAt"/> × якорь (<see cref="RelativeTo"/> — созревание или продолжительность жизни),
/// масштаб тела и набор текстур ("baby"/"adult"). Стадии перечисляются по возрастанию возраста входа.
/// Модер задаёт любой набор стадий (личинка/куколка/имаго и т.п.) без правки кода.
/// </summary>
public sealed class AnimalStageDef
{
/// <summary>Имя стадии (для тултипов/отладки).</summary>
public string Name { get; init; } = "";
/// <summary>Множитель к якорю: возраст входа = EnterAt × (созревание | продолжительность жизни).</summary>
public float EnterAt { get; init; }
/// <summary>Якорь возраста входа: "maturity" (созревание) или "lifespan" (продолжительность жизни).</summary>
public string RelativeTo { get; init; } = "maturity";
/// <summary>Масштаб размера тела на этой стадии (× ген размера).</summary>
public float Scale { get; init; } = 1f;
/// <summary>Набор текстур: "baby" (детёныш) или "adult" (по полу — самец/самка).</summary>
public string Texture { get; init; } = "adult";
/// <summary>Взрослая стадия — способна размножаться.</summary>
public bool Adult { get; init; }
}
+13
View File
@@ -0,0 +1,13 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Анатомия вида (Defs/Bodies/): дерево частей тела/органов. Используется системой здоровья (фаза
/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities).
/// </summary>
public sealed class BodyDef : Def
{
/// <summary>Части тела/органы вида.</summary>
public BodyPartDef[] Parts { get; init; } = [];
}
+27
View File
@@ -0,0 +1,27 @@
namespace LittleSim.Content;
/// <summary>
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
/// способности (capacity id → доля). Модер задаёт любую анатомию (8 ног, два сердца) без правки кода.
/// </summary>
public sealed class BodyPartDef
{
/// <summary>Имя части (уникально в теле); на него ссылается <see cref="Parent"/> детей.</summary>
public string Name { get; init; } = "";
/// <summary>Имя родительской части ("" — корневая, напр. торс).</summary>
public string Parent { get; init; } = "";
/// <summary>Вес попадания (доля тела) — для будущего выбора задетой части.</summary>
public float Coverage { get; init; } = 0.1f;
/// <summary>Базовый максимум HP (× размер тела при создании особи).</summary>
public float MaxHp { get; init; } = 10f;
/// <summary>Жизненно важная: уничтожение → смерть.</summary>
public bool Vital { get; init; }
/// <summary>Вклады в способности: capacity id (см. <c>AnimalCapacities</c>) → доля (сумма по телу ≈ 1).</summary>
public Dictionary<string, float> Capacities { get; init; } = new();
}
+21
View File
@@ -0,0 +1,21 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Пункт дев-инструмента (Defs/DevTools/) — ЧТОБЫ МОДЫ ДОБАВЛЯЛИ кнопки в дев-спавнер без правки кода.
/// <see cref="Kind"/> — id обработчика из реестра (spawn-animal/spawn-plant/infect/damage/kill/…),
/// <see cref="Target"/> — параметр (defName вида/хедифа). Виды животных/растений спавнер берёт из их
/// дефов сам (модовый контент появляется автоматически); DevToolDef добавляет ПРОЧИЕ действия.
/// </summary>
public sealed class DevToolDef : Def
{
/// <summary>Категория для группировки в панели (например "tool").</summary>
public string Category { get; init; } = "tool";
/// <summary>Id обработчика-инструмента из реестра кода (spawn-animal/spawn-plant/infect/damage/kill).</summary>
public string Kind { get; init; } = "";
/// <summary>Параметр инструмента: defName вида/хедифа и т.п. (по смыслу Kind).</summary>
public string Target { get; init; } = "";
}
+78
View File
@@ -0,0 +1,78 @@
namespace LittleSim.Content;
/// <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>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
public float ColdHardiness { get; init; } = 8f;
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
public float HeatHardiness { get; init; } = 10f;
/// <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>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
public float Toxicity { get; init; }
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
public float Thorns { get; init; }
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
public float Palatability { get; init; } = 1f;
}
+39
View File
@@ -0,0 +1,39 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Состояние/хедиф организма (Defs/Hediffs/): гон, болезни, позже раны/возрастные эффекты. Фаза A6:
/// болезнь прогрессирует (<see cref="SeverityPerDay"/>), параллельно растёт иммунитет
/// (<see cref="ImmunityPerDay"/>) — гонка: иммунитет→1 раньше = выздоровление, severity→
/// <see cref="LethalSeverity"/> раньше = смерть. Снижает способности (<see cref="CapMods"/>) и даёт боль.
/// </summary>
public sealed class HediffDef : Def
{
/// <summary>Начальная тяжесть при навешивании (гон — 1; болезнь — малая, напр. 0.05).</summary>
public float InitialSeverity { get; init; } = 1f;
/// <summary>Прирост тяжести за игровой день (0 — статичный хедиф вроде гона).</summary>
public float SeverityPerDay { get; init; }
/// <summary>Прирост иммунитета за день (0 — иммунитета нет, не выздоравливает сам).</summary>
public float ImmunityPerDay { get; init; }
/// <summary>Тяжесть, при которой наступает смерть (0 — не летально).</summary>
public float LethalSeverity { get; init; }
/// <summary>Боль при тяжести 1 (масштабируется тяжестью) — вход в сознание/настроение.</summary>
public float Pain { get; init; }
/// <summary>Шанс подхватить фоном за день (0 — не фоновая болезнь).</summary>
public float AmbientPerDay { get; init; }
/// <summary>
/// Кровопотеря за игровой день при тяжести 1 (масштабируется тяжестью): доля объёма крови, которую
/// рана теряет в сутки. 0 — хедиф не вызывает кровотечения. Кровь на нуле → смерть (см. система здоровья).
/// </summary>
public float BloodLossPerDay { get; init; }
/// <summary>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
public Dictionary<string, float> CapMods { get; init; } = new();
}
+36
View File
@@ -0,0 +1,36 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Нужда (Defs/Needs/) как ДАННЫЕ: имя, тип (deplete — убывает, низкое = срочно; drive — растёт под
/// условием, высокое = срочно), действие-исполнитель (по id, см. реестр поведений), темпы и порог
/// интеллекта. Модер добавляет нужду виду/всем одной записью; если она ссылается на существующее
/// действие — без правки кода. Хранилище нужд особи — динамическое (см. <c>NeedSet</c>/<c>AnimalNeeds</c>).
/// </summary>
public sealed class NeedDef : Def
{
/// <summary>Тип: "deplete" (убывает, утоляется действием) или "drive" (растёт под условием).</summary>
public string Kind { get; init; } = "deplete";
/// <summary>Id действия-исполнителя, удовлетворяющего нужду (Eat/Drink/Sleep/Mate/…).</summary>
public string Action { get; init; } = "";
/// <summary>Темп убывания за игровой день (drive: темп спада, когда условие отсутствует).</summary>
public float DecayPerDay { get; init; } = 0.5f;
/// <summary>drive: темп роста за день, пока активно условие <see cref="RisesUnder"/>.</summary>
public float RisePerDay { get; init; } = 1f;
/// <summary>deplete: насколько за день восполняет нужду исполнение действия.</summary>
public float FeedPerDay { get; init; } = 4f;
/// <summary>drive: id хедифа, под которым нужда растёт (напр. "Rut"); null — растёт всегда.</summary>
public string? RisesUnder { get; init; }
/// <summary>deplete: на нуле копит счётчик голодной смерти (A3).</summary>
public bool Lethal { get; init; }
/// <summary>Порог размера мозга, с которого нужда активна (гейт интеллектом — фаза A7).</summary>
public float MinBrain { get; init; }
}
+22
View File
@@ -0,0 +1,22 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное). Не sealed — <see cref="AnimalDef"/> наследует.</summary>
public class PawnDef : Def
{
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
public const string KindBeing = "being";
/// <summary>Вид животного.</summary>
public const string KindAnimal = "animal";
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
public string Texture { get; init; } = "";
/// <summary>Размер спрайта в клетках.</summary>
public float SizeCells { get; init; } = 1f;
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
public string Kind { get; init; } = "";
}
+31
View File
@@ -0,0 +1,31 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
public sealed class PlantDef : Def
{
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
public string Texture { get; init; } = "";
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
public float SizeCells { get; init; } = 1f;
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
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>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
public float BaseMassKg { get; init; } = 20f;
}
@@ -0,0 +1,21 @@
namespace LittleSim.Content;
/// <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; }
}
+17
View File
@@ -0,0 +1,17 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
/// </summary>
public sealed class ProductDef : Def
{
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
public string Kind { get; init; } = "";
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
public float MassKg { get; init; } = 1f;
}
@@ -0,0 +1,11 @@
namespace LittleSim.Content;
/// <summary>Запись скаттера: с вероятностью <see cref="Chance"/> на клетку ставится одно растение из <see cref="Options"/>.</summary>
public sealed class ScatterEntry
{
/// <summary>Вероятность на клетку (0..1); записи бросаются независимо.</summary>
public float Chance { get; init; }
/// <summary>Имена <see cref="PlantDef"/>, из которых выбирается равновероятно.</summary>
public List<string> Options { get; init; } = [];
}
+24
View File
@@ -0,0 +1,24 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
/// код по его id (как у нужд/действий): добыча→выпас, охота→урон, уклонение→бегство. Модер добавляет
/// навык строкой JSON; новый эффект — код. <see cref="Def.Label"/> — ключ локализации (skill.*).
/// </summary>
public sealed class SkillDef : Def
{
/// <summary>Категория (для группировки в UI), напр. "survival".</summary>
public string Category { get; init; } = "";
/// <summary>Скорость обучения за игровой день непрерывной практики (при страсти ×1).</summary>
public float LearnRate { get; init; } = 0.5f;
/// <summary>Угасание уровня за игровой день без практики.</summary>
public float DecayPerDay { get; init; } = 0.02f;
/// <summary>Пол угасания (ниже не падает) — «не забывается совсем».</summary>
public float FloorLevel { get; init; }
}
+37
View File
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Microsoft.Xna.Framework;
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
public sealed class TerrainDef : Def
{
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
public float MaxHeight { get; init; }
/// <summary>Цвет клетки [r, g, b] для сцен с тонированными квадратами и тайлов без поверхности.</summary>
[JsonPropertyName("color")]
public List<int> Rgb { get; init; } = [];
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
public bool IsLand { get; init; }
/// <summary>Светонепроницаемый террейн — базовый окклюдер лайтмапа/теней (сейчас такого нет).</summary>
public bool BlocksLight { get; init; }
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
public string? Surface { get; init; }
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
public List<ScatterEntry> Scatter { get; init; } = [];
/// <summary>
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
/// </summary>
public float Fertility { get; init; } = 1f;
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
}
+18
View File
@@ -0,0 +1,18 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Мысль настроения (Defs/Thoughts/) как ДАННЫЕ (фаза A8, модель thoughts RimWorld): событие
/// (роды/спаривание/утоление жажды/…) вешает на особь временный сдвиг настроения, затухающий за
/// <see cref="DurationDays"/>. Триггеры событий — код (реестр <c>AnimalThoughts</c>), а величины/метка —
/// данные: модер правит силу и длительность мысли одной записью JSON, без пересборки.
/// </summary>
public sealed class ThoughtDef : Def
{
/// <summary>Сдвиг настроения [-1,1] (положительный — радость, отрицательный — горе).</summary>
public float MoodOffset { get; init; }
/// <summary>Сколько игровых дней мысль живёт, прежде чем угаснуть.</summary>
public float DurationDays { get; init; } = 1f;
}
@@ -0,0 +1,23 @@
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
/// хардкодом. <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; }
}
+7
View File
@@ -63,6 +63,13 @@ public sealed class GameContent
defs.RegisterType<ProductDef>("Product"); defs.RegisterType<ProductDef>("Product");
defs.RegisterType<PlantDef>("Plant"); defs.RegisterType<PlantDef>("Plant");
defs.RegisterType<PawnDef>("Pawn"); defs.RegisterType<PawnDef>("Pawn");
defs.RegisterType<AnimalDef>("Animal");
defs.RegisterType<HediffDef>("Hediff");
defs.RegisterType<NeedDef>("Need");
defs.RegisterType<ThoughtDef>("Thought");
defs.RegisterType<BodyDef>("Body");
defs.RegisterType<SkillDef>("Skill");
defs.RegisterType<DevToolDef>("DevTool");
defs.RegisterType<WorldPresetDef>("WorldPreset"); defs.RegisterType<WorldPresetDef>("WorldPreset");
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа. // Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'"); defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
-204
View File
@@ -1,204 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.Xna.Framework;
using MrGameEng.Mods;
namespace LittleSim.Content;
/// <summary>Запись скаттера: с вероятностью <see cref="Chance"/> на клетку ставится одно растение из <see cref="Options"/>.</summary>
public sealed class ScatterEntry
{
/// <summary>Вероятность на клетку (0..1); записи бросаются независимо.</summary>
public float Chance { get; init; }
/// <summary>Имена <see cref="PlantDef"/>, из которых выбирается равновероятно.</summary>
public List<string> Options { get; init; } = [];
}
/// <summary>Тип клетки рельефа (Defs/terrain.json): порог высоты, цвет, поверхность, скаттер растений.</summary>
public sealed class TerrainDef : Def
{
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
public float MaxHeight { get; init; }
/// <summary>Цвет клетки [r, g, b] для сцен с тонированными квадратами и тайлов без поверхности.</summary>
[JsonPropertyName("color")]
public List<int> Rgb { get; init; } = [];
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
public bool IsLand { get; init; }
/// <summary>Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.</summary>
public bool BlocksLight { get; init; }
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
public string? Surface { get; init; }
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
public List<ScatterEntry> Scatter { get; init; } = [];
/// <summary>
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
/// </summary>
public float Fertility { get; init; } = 1f;
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
}
/// <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
{
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
public string Texture { get; init; } = "";
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
public float SizeCells { get; init; } = 1f;
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
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>
public sealed class PawnDef : Def
{
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
public const string KindBeing = "being";
/// <summary>Вид животного.</summary>
public const string KindAnimal = "animal";
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
public string Texture { get; init; } = "";
/// <summary>Размер спрайта в клетках.</summary>
public float SizeCells { get; init; } = 1f;
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
public string Kind { get; init; } = "";
}
+9
View File
@@ -48,6 +48,15 @@ public sealed class ModAtlases : IDisposable
return Get(device, name).GetRegion(textureKey); return Get(device, name).GetRegion(textureKey);
} }
/// <summary>Регион по ключу, если он есть (для опциональных вариантов, напр. направленных спрайтов).</summary>
public bool TryGetRegion(GraphicsDevice device, string textureKey, out Texture2DRegion region)
{
var segments = textureKey.Split('/');
var depth = Math.Min(GroupDepth, segments.Length - 1);
var name = depth == 0 ? "Atlas" : string.Join('.', segments[..depth]);
return Get(device, name).TryGetRegion(textureKey, out region);
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
+57
View File
@@ -0,0 +1,57 @@
namespace LittleSim.Content;
/// <summary>
/// Готовый реестр нужд из <see cref="NeedDef"/>: присваивает каждой нужде индекс (по нему системы
/// читают значения в <c>AnimalNeeds.Values</c> без словарей), знает тип (deplete/drive), начальное
/// значение и какое действие какую нужду утоляет. Делает набор нужд расширяемым данными: добавил
/// <see cref="NeedDef"/> — особи получают новую нужду, без правки кода (если действие уже есть).
/// </summary>
public sealed class NeedSet
{
private readonly NeedDef[] _defs;
private readonly bool[] _isDrive; // предрасчёт типа нужды — горячие циклы не сравнивают строки на тик
private readonly Dictionary<string, int> _byAction = new(StringComparer.Ordinal);
/// <summary>Строит реестр из всех <see cref="NeedDef"/> мода (порядок = индексы нужд).</summary>
public NeedSet(GameContent content)
{
_defs = content.Defs.All<NeedDef>().ToArray();
_isDrive = new bool[_defs.Length];
for (var i = 0; i < _defs.Length; i++)
{
_isDrive[i] = string.Equals(_defs[i].Kind, "drive", StringComparison.Ordinal);
if (!string.IsNullOrEmpty(_defs[i].Action))
{
_byAction[_defs[i].Action] = i;
}
}
}
/// <summary>Число нужд.</summary>
public int Count => _defs.Length;
/// <summary>Деф нужды по индексу.</summary>
public NeedDef this[int index] => _defs[index];
/// <summary>Нужда-влечение (растёт под условием), а не убывающая.</summary>
public bool IsDrive(int index) => _isDrive[index];
/// <summary>Убывающая нужда (голод/жажда/сон) — низкое значение срочно.</summary>
public bool IsDeplete(int index) => !IsDrive(index);
/// <summary>Индекс нужды, которую утоляет действие, или -1.</summary>
public int NeedForAction(string actionId) =>
_byAction.TryGetValue(actionId, out var index) ? index : -1;
/// <summary>Новый массив значений нужд особи: deplete стартуют полными (1), drive — пустыми (0).</summary>
public float[] NewValues()
{
var values = new float[_defs.Length];
for (var i = 0; i < values.Length; i++)
{
values[i] = IsDrive(i) ? 0f : 1f;
}
return values;
}
}
+8 -2
View File
@@ -81,7 +81,7 @@ public sealed class PlantSet
/// <summary>Индекс вида по дефу.</summary> /// <summary>Индекс вида по дефу.</summary>
public int IndexOf(PlantDef def) => _index[def]; public int IndexOf(PlantDef def) => _index[def];
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр // Переводит базовый геном вида (числа из Defs/Plants/) в набор генов: каждое число — центр
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида. // аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
private static GenomeTemplate BuildTemplate( private static GenomeTemplate BuildTemplate(
GenomeDef g, GenomeDef g,
@@ -92,7 +92,7 @@ public sealed class PlantSet
genes.TryGetValue(id, out var def) genes.TryGetValue(id, out var def)
? def ? def
: throw new KeyNotFoundException( : throw new KeyNotFoundException(
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?" $"Gene def '{id}' not found — is Mods/Core/Defs/Genes/ loaded?"
); );
GenomeTemplate.Entry Numeric(string id, float baseValue) => GenomeTemplate.Entry Numeric(string id, float baseValue) =>
@@ -103,6 +103,8 @@ public sealed class PlantSet
Numeric("GeneLightTolerance", g.LightTolerance), Numeric("GeneLightTolerance", g.LightTolerance),
Numeric("GeneOptimalTemperature", g.OptimalTemperature), Numeric("GeneOptimalTemperature", g.OptimalTemperature),
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance), Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
Numeric("GeneColdHardiness", g.ColdHardiness),
Numeric("GeneHeatHardiness", g.HeatHardiness),
Numeric("GeneOptimalFertility", g.OptimalFertility), Numeric("GeneOptimalFertility", g.OptimalFertility),
Numeric("GeneFertilityTolerance", g.FertilityTolerance), Numeric("GeneFertilityTolerance", g.FertilityTolerance),
Numeric("GeneVigor", g.Vigor), Numeric("GeneVigor", g.Vigor),
@@ -116,6 +118,10 @@ public sealed class PlantSet
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
Numeric("GeneHarvestAmount", g.HarvestAmount), Numeric("GeneHarvestAmount", g.HarvestAmount),
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f), new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
// Защита (коэволюция с травоядными): токсичность/шипы/вкусность.
Numeric("GeneToxicity", g.Toxicity),
Numeric("GeneThorns", g.Thorns),
Numeric("GenePalatability", g.Palatability),
new GenomeTemplate.Entry( new GenomeTemplate.Entry(
Gene("GeneMorph"), Gene("GeneMorph"),
0f, 0f,
+81
View File
@@ -0,0 +1,81 @@
namespace LittleSim.Content;
/// <summary>
/// Готовый реестр навыков из <see cref="SkillDef"/>: каждому навыку — индекс (по нему системы читают
/// уровни в <c>Skills.Levels</c> без словарей), плюс генерация стартовых уровней и страсти особи.
/// Делает набор навыков расширяемым данными (как <see cref="NeedSet"/>): добавил <see cref="SkillDef"/> —
/// у особей появляется навык, без правки кода (если его эффект/начисление XP уже есть в коде).
/// </summary>
public sealed class SkillSet
{
// Веса страсти при генерации особи: нет / интерес / страсть (множители обучения см. PassionMultiplier).
private static readonly float[] PassionWeights = [0.6f, 0.3f, 0.1f];
private readonly SkillDef[] _defs;
private readonly Dictionary<string, int> _byId = new(StringComparer.Ordinal);
/// <summary>Строит реестр из всех <see cref="SkillDef"/> мода (порядок = индексы навыков).</summary>
public SkillSet(GameContent content)
{
_defs = content.Defs.All<SkillDef>().ToArray();
for (var i = 0; i < _defs.Length; i++)
{
_byId[_defs[i].DefName] = i;
}
}
/// <summary>Число навыков.</summary>
public int Count => _defs.Length;
/// <summary>Деф навыка по индексу.</summary>
public SkillDef this[int index] => _defs[index];
/// <summary>Индекс навыка по id, или -1.</summary>
public int IndexOf(string id) => _byId.TryGetValue(id, out var index) ? index : -1;
/// <summary>Стартовые уровни особи (с пола угасания каждого навыка).</summary>
public float[] NewLevels()
{
var levels = new float[_defs.Length];
for (var i = 0; i < levels.Length; i++)
{
levels[i] = _defs[i].FloorLevel;
}
return levels;
}
/// <summary>Бросает страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) детерминированно по rng.</summary>
public byte[] RollPassions(Random rng)
{
var passions = new byte[_defs.Length];
for (var i = 0; i < passions.Length; i++)
{
var roll = rng.NextSingle();
byte p = 0;
var acc = 0f;
for (byte k = 0; k < PassionWeights.Length; k++)
{
acc += PassionWeights[k];
if (roll < acc)
{
p = k;
break;
}
}
passions[i] = p;
}
return passions;
}
/// <summary>Множитель скорости обучения по уровню страсти (нет/интерес/страсть).</summary>
public static float PassionMultiplier(byte passion) =>
passion switch
{
0 => 0.35f,
1 => 1.0f,
_ => 1.7f,
};
}
+22
View File
@@ -0,0 +1,22 @@
namespace LittleSim.Content;
/// <summary>
/// Реестр мыслей настроения (<see cref="ThoughtDef"/>) с доступом по id (фаза A8): системы-источники
/// событий (роды/спаривание/жажда) берут нужную мысль по строковому id (см. <c>AnimalThoughts</c>) и
/// вешают её на особь. Метка/сила/длительность мысли — данные; модер правит их в thoughts.json. Зеркало
/// <see cref="NeedSet"/> по духу. Удалил модер мысль из дефов — <see cref="Get"/> вернёт null (no-op).
/// </summary>
public sealed class ThoughtSet
{
private readonly Dictionary<string, ThoughtDef> _byId;
/// <summary>Строит реестр из всех <see cref="ThoughtDef"/> мода.</summary>
public ThoughtSet(GameContent content) =>
_byId = content.Defs.All<ThoughtDef>().ToDictionary(t => t.DefName, StringComparer.Ordinal);
/// <summary>Число мыслей.</summary>
public int Count => _byId.Count;
/// <summary>Деф мысли по id или null, если такой нет (мод убрал).</summary>
public ThoughtDef? Get(string id) => _byId.GetValueOrDefault(id);
}
+25
View File
@@ -0,0 +1,25 @@
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;
}
+32 -2
View File
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using LittleSim.Sim; using LittleSim.Sim;
using MrGameEng.Graphics; using MrGameEng.Graphics;
using MrGameEng.Net; using MrGameEng.Net;
@@ -17,7 +18,36 @@ public static class NetSchema
/// <summary>Порт сервера по умолчанию.</summary> /// <summary>Порт сервера по умолчанию.</summary>
public const int DefaultPort = 9050; public const int DefaultPort = 9050;
// Задокументированный wire-размер реплицируемых структур (см. NetContract.cs веб-зеркала). Репликация
// блитит сырые байты структуры, поэтому добавленное/переставленное поле молча испортит десериализацию —
// проверяем размеры при построении схемы, чтобы дрейф упал на старте, а не выдал битый мир.
private const int TransformWireSize = 20; // Vector2(8) + float(4) + Vector2(8)
private const int PawnNeedsWireSize = 4; // float(4)
/// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary> /// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary>
public static ReplicationSchema Create() => public static ReplicationSchema Create()
new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>(); {
VerifyLayout();
return new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
}
/// <summary>Сверяет фактическую байт-раскладку реплицируемых структур с контрактом (см. NetContract.cs).</summary>
private static void VerifyLayout()
{
Require<Transform2D>(TransformWireSize);
Require<PawnNeeds>(PawnNeedsWireSize);
static void Require<T>(int expected)
where T : struct
{
var actual = Unsafe.SizeOf<T>();
if (actual != expected)
{
throw new InvalidOperationException(
$"Размер реплицируемого {typeof(T).Name} = {actual} Б, ожидался {expected} Б: "
+ "wire-контракт изменился — синхронизируй NetSchema и LittleSim.Web/NetContract.cs."
);
}
}
}
} }
@@ -5,25 +5,6 @@ using MrGameEng.Graphics;
namespace LittleSim.Net; 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> /// <summary>
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в /// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера. /// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
+31
View File
@@ -1,6 +1,7 @@
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Net; using LittleSim.Net;
using LittleSim.Scenes; using LittleSim.Scenes;
using LittleSim.Sim;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Genetics; using MrGameEng.Genetics;
using MrGameEng.Host; using MrGameEng.Host;
@@ -20,6 +21,36 @@ if (args.Contains("--check-content"))
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits " + $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})" + $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
); );
// Диета животных из генов (предатор-кластер): для каждого вида строим центральный геном из его
// баз, считаем признаки формулами и выводим рацион (травоядное/хищник/всеядное) — проверка без
// GPU, что хищники заданы и роль выводится из генов herbivory/carnivory/omnivory.
foreach (var def in content.Defs.All<AnimalDef>())
{
var entries = new List<GenomeTemplate.Entry>();
foreach (var (geneId, baseValue) in def.Genome)
{
if (registry.TryGetValue(geneId, out var gene))
{
var spread = gene.Kind == GeneKind.Discrete ? 0f : gene.Spread;
entries.Add(new GenomeTemplate.Entry(gene, baseValue, spread));
}
}
var genome = new GenomeTemplate(entries).Generate(new Random(7));
var t = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, registry));
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(t, def);
var role =
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
: eatsPlants ? "herbivore"
: "none";
var hunt = eatsMeat && def.AttackDamage > 0f ? $", attack {def.AttackDamage:0}/d" : "";
Console.WriteLine(
$" {def.DefName}: {role} (herb {t.Herbivory:0.##}/carn {t.Carnivory:0.##}/omni {t.Omnivory:0.##}),"
+ $" spawn {def.SpawnPer1000Cells:0.##}/1000{hunt}"
);
}
return; return;
} }
catch (Exception error) catch (Exception error)
+2 -2
View File
@@ -46,14 +46,14 @@ public sealed class BootScene : Scene
host.Graphics.ApplyChanges(); host.Graphics.ApplyChanges();
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume; Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
_label = new Label _label = new Label
{ {
TextColor = Ui.Accent, TextColor = Ui.Accent,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
}; };
desktop.Root = Ui.Screen(_label); desktop.Root = Ui.MenuScreen(_label);
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока. // Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
_load = Task.Run(() => GameContent.Load()); _load = Task.Run(() => GameContent.Load());
@@ -2,58 +2,14 @@ using Friflo.Engine.ECS;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Sim; using LittleSim.Sim;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.DevConsole; using MrGameEng.DevConsole;
using MrGameEng.Graphics; using MrGameEng.Graphics;
using MrGameEng.Lighting;
namespace LittleSim.Scenes; namespace LittleSim.Scenes;
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
internal static class ScatterSpawner
{
/// <summary>
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
/// разновозрастный. Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением
/// управляет <see cref="PlantGrowthSystem"/>.
/// </summary>
public static void Spawn(
Scene scene,
GameContent content,
PlantSet plants,
TerrainDef terrain,
Point cell,
int cellSize,
Random random
)
{
foreach (var entry in terrain.Scatter)
{
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
{
continue;
}
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
var index = plants.IndexOf(def);
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var age = random.NextSingle() * plants[index].MaturityDays;
PlantFactory.Create(
scene.Store,
plants,
index,
new Vector2(px, py),
age,
plants[index].Template.Generate(random),
terrain.Fertility,
cellSize
);
}
}
}
/// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary> /// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary>
internal static class ContentCommands internal static class ContentCommands
{ {
+5 -5
View File
@@ -15,17 +15,17 @@ public sealed class CreditsScene : Scene
{ {
var lang = Context.Services.Get<GameContent>().Languages; var lang = Context.Services.Get<GameContent>().Languages;
var input = this.UseInput(); var input = this.UseInput();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12); var column = Ui.Card(10);
column.Widgets.Add(Ui.Title(lang.Get("credits.title"))); column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(Ui.AccentRule());
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author"))); column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role"))); column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 12 });
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back)); column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
desktop.Root = Ui.Screen(column); desktop.Root = Ui.MenuScreen(column);
UpdateSystems.Add( UpdateSystems.Add(
new CallbackSystem(() => new CallbackSystem(() =>
{ {
+5 -1
View File
@@ -13,7 +13,10 @@ public static class GameLayers
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary> /// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
public static LayerId Beings { get; private set; } public static LayerId Beings { get; private set; }
/// <summary>Регистрирует слои при первом вызове.</summary> /// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
public static LayerId Selection { get; private set; }
/// <summary>Регистрирует слои при первом вызове (порядок = порядок отрисовки).</summary>
public static void EnsureRegistered(Renderer2D renderer) public static void EnsureRegistered(Renderer2D renderer)
{ {
if (_registered) if (_registered)
@@ -22,6 +25,7 @@ public static class GameLayers
} }
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort); Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
Selection = renderer.Layers.Register("Selection", LayerSpace.World, LayerSortMode.Depth);
_registered = true; _registered = true;
} }
} }
+5 -5
View File
@@ -24,7 +24,7 @@ public sealed class LoadGameScene : Scene
{ {
_content = Context.Services.Get<GameContent>(); _content = Context.Services.Get<GameContent>();
var input = this.UseInput(); var input = this.UseInput();
_desktop = this.UseUI(); _desktop = this.UseScaledUI();
Rebuild(); Rebuild();
UpdateSystems.Add( UpdateSystems.Add(
@@ -41,9 +41,9 @@ public sealed class LoadGameScene : Scene
private void Rebuild() private void Rebuild()
{ {
var lang = _content.Languages; var lang = _content.Languages;
var column = Ui.Column(8); var column = Ui.Card(8);
column.Widgets.Add(Ui.Title(lang.Get("load.title"))); column.Widgets.Add(Ui.Title(lang.Get("load.title")));
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.AccentRule());
var saves = _store.List(); var saves = _store.List();
if (saves.Count == 0) if (saves.Count == 0)
@@ -73,7 +73,7 @@ public sealed class LoadGameScene : Scene
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
{ {
Context.Scenes.Switch( Context.Scenes.Switch(
new WorldScene(save.ToConfig(), save), new WorldLoadingScene(save.ToConfig(), save),
Transitions.Fade(0.6f) Transitions.Fade(0.6f)
); );
} }
@@ -98,7 +98,7 @@ public sealed class LoadGameScene : Scene
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(new Label { Height = 8 });
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back)); column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
_desktop.Root = Ui.Screen(column); _desktop.Root = Ui.MenuScreen(column);
} }
private void Back() private void Back()
+11 -11
View File
@@ -17,21 +17,21 @@ public sealed class MainMenuScene : Scene
{ {
var content = Context.Services.Get<GameContent>(); var content = Context.Services.Get<GameContent>();
var lang = content.Languages; var lang = content.Languages;
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
var column = Ui.Column(12); var card = Ui.Card(8);
column.Widgets.Add(Ui.Title(lang.Get("menu.title"))); card.Widgets.Add(Ui.Title(lang.Get("menu.title")));
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle"))); card.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 }); card.Widgets.Add(Ui.AccentRule());
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene()))); card.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
column.Widgets.Add( card.Widgets.Add(
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit()) Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
); );
desktop.Root = Ui.Screen(column); desktop.Root = Ui.MenuScreen(card);
} }
private void Go(Scene scene) private void Go(Scene scene)
+42 -14
View File
@@ -30,6 +30,9 @@ public sealed class MultiplayerScene : Scene
{ {
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f); private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
// Бэкофф переподключения: задержка удваивается с каждой неудачей до потолка.
private const float MaxBackoffSeconds = 8f;
private readonly Uri _server; private readonly Uri _server;
private Task<WebSocketClient>? _connecting; private Task<WebSocketClient>? _connecting;
private WebSocketClient? _connection; private WebSocketClient? _connection;
@@ -38,6 +41,8 @@ public sealed class MultiplayerScene : Scene
private GameContent _content = null!; private GameContent _content = null!;
private Label _hud = null!; private Label _hud = null!;
private string _statusKey = "net.connecting"; private string _statusKey = "net.connecting";
private double _nextAttemptAt; // время (unscaled) следующей попытки подключения
private int _attempt; // 0 — первая попытка; растёт при обрывах, задаёт бэкофф
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary> /// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
public MultiplayerScene(Uri server) => _server = server; public MultiplayerScene(Uri server) => _server = server;
@@ -64,7 +69,7 @@ public sealed class MultiplayerScene : Scene
entity.AddComponent(new NetLerp()); entity.AddComponent(new NetLerp());
}; };
var desktop = this.UseUI(); var desktop = this.UseScaledUI();
_hud = new Label { Left = 10, Top = 8 }; _hud = new Label { Left = 10, Top = 8 };
desktop.Root = Ui.Screen(_hud); desktop.Root = Ui.Screen(_hud);
@@ -86,39 +91,53 @@ public sealed class MultiplayerScene : Scene
} }
); );
_connecting = WebSocketClient.ConnectAsync(_server); // Первую попытку запускает сам Pump (единый путь с переподключением): _nextAttemptAt = 0.
} }
protected override void OnUnload() => _connection?.Close(); protected override void OnUnload() => _connection?.Close();
private void Pump() private void Pump()
{ {
var now = Context.Clock.UnscaledTotalTime;
// Завершилась попытка подключения: успех — берём соединение; провал — планируем повтор.
if (_connecting is { IsCompleted: true } finished) if (_connecting is { IsCompleted: true } finished)
{ {
_connecting = null; _connecting = null;
if (finished.IsFaulted) if (finished.IsCompletedSuccessfully)
{
_connection = finished.Result;
_replication.Clear(); // сбрасываем устаревшие сущности перед свежим полным снапшотом
_attempt = 0;
_statusKey = "net.connected";
Log.Info($"Connected to {_server}");
}
else
{ {
_statusKey = "net.failed";
Log.Error( Log.Error(
$"Connect to {_server} failed: " $"Connect to {_server} failed: "
+ finished.Exception?.GetBaseException().Message + finished.Exception?.GetBaseException().Message
); );
ScheduleReconnect(now);
} }
else }
{
_connection = finished.Result; // Обрыв уже установленного соединения: чистим и уходим в переподключение.
_statusKey = "net.connected"; if (_connection is { IsOpen: false })
Log.Info($"Connected to {_server}"); {
} _connection = null;
_replication.Clear();
ScheduleReconnect(now);
} }
if (_connection is not null) if (_connection is not null)
{ {
_replication.Pump(_connection); _replication.Pump(_connection);
if (!_connection.IsOpen) }
{ else if (_connecting is null && now >= _nextAttemptAt)
_statusKey = "net.lost"; {
} _statusKey = _attempt == 0 ? "net.connecting" : "net.reconnecting";
_connecting = WebSocketClient.ConnectAsync(_server);
} }
_hud.Text = _content.Languages.Format( _hud.Text = _content.Languages.Format(
@@ -133,4 +152,13 @@ public sealed class MultiplayerScene : Scene
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
// Экспоненциальный бэкофф: 1, 2, 4, 8, 8, … секунд между попытками.
private void ScheduleReconnect(double now)
{
var delay = MathF.Min(MaxBackoffSeconds, 1f * (1 << Math.Min(_attempt, 3)));
_attempt++;
_nextAttemptAt = now + delay;
_statusKey = "net.reconnecting";
}
} }
+5 -5
View File
@@ -33,14 +33,14 @@ public sealed class NewWorldScene : Scene
var content = Context.Services.Get<GameContent>(); var content = Context.Services.Get<GameContent>();
var lang = content.Languages; var lang = content.Languages;
var input = this.UseInput(); var input = this.UseInput();
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList(); var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
_preset = presets[0]; _preset = presets[0];
var column = Ui.Column(10); var column = Ui.Card(10);
column.Widgets.Add(Ui.Title(lang.Get("newworld.title"))); column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(Ui.AccentRule());
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 }; _name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name)); column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
@@ -106,7 +106,7 @@ public sealed class NewWorldScene : Scene
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150)); buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
column.Widgets.Add(buttons); column.Widgets.Add(buttons);
desktop.Root = Ui.Screen(column); desktop.Root = Ui.MenuScreen(column);
Refresh(); Refresh();
UpdateSystems.Add( UpdateSystems.Add(
@@ -154,7 +154,7 @@ public sealed class NewWorldScene : Scene
Seed = seed, Seed = seed,
SmoothPasses = _smoothing, SmoothPasses = _smoothing,
}; };
Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f)); Context.Scenes.Switch(new WorldLoadingScene(config), Transitions.Fade(0.6f));
} }
private void Back() private void Back()
+4
View File
@@ -24,6 +24,7 @@ internal sealed class PauseMenu
private readonly GameSpeed _speed; private readonly GameSpeed _speed;
private readonly GameHost _host; private readonly GameHost _host;
private readonly AudioManager _audio; private readonly AudioManager _audio;
private readonly Desktop _desktop;
private readonly Func<string> _onSave; private readonly Func<string> _onSave;
private readonly Action _onMainMenu; private readonly Action _onMainMenu;
private readonly Action _onQuit; private readonly Action _onQuit;
@@ -36,6 +37,7 @@ internal sealed class PauseMenu
EngineContext context, EngineContext context,
GameContent content, GameContent content,
GameSpeed speed, GameSpeed speed,
Desktop desktop,
Func<string> onSave, Func<string> onSave,
Action onMainMenu, Action onMainMenu,
Action onQuit Action onQuit
@@ -44,6 +46,7 @@ internal sealed class PauseMenu
_context = context; _context = context;
_content = content; _content = content;
_speed = speed; _speed = speed;
_desktop = desktop;
_host = (GameHost)context.Services.Get<Game>(); _host = (GameHost)context.Services.Get<Game>();
_audio = context.Services.Get<AudioManager>(); _audio = context.Services.Get<AudioManager>();
_onSave = onSave; _onSave = onSave;
@@ -144,6 +147,7 @@ internal sealed class PauseMenu
{ {
GameSettingsStore.Save(_settings); GameSettingsStore.Save(_settings);
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio); GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
UiScaling.Apply(_context, _desktop, _settings.UiScale); // масштаб UI — сразу
ShowSettings(); // язык мог смениться ShowSettings(); // язык мог смениться
}, },
onBack: () => onBack: () =>
+74
View File
@@ -0,0 +1,74 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
using LittleSim.Sim;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Graphics;
using MrGameEng.Lighting;
namespace LittleSim.Scenes;
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
internal static class ScatterSpawner
{
// Ниже этой световой пригодности (при полном дневном свете клетки) растение не сеем: теневому виду
// (грибы, optimalLight≈0.15) в открытой клетке не место — без полога крон ему тут не выжить, поэтому
// он и не должен появляться, пока такой ниши нет. Солнечные виды (≥0.7 пригодности) проходят свободно.
private const float ShadeSpawnThreshold = 0.15f;
/// <summary>
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
/// разновозрастный. Теневые виды не сеются в ярко освещённые клетки (см. <see cref="ShadeSpawnThreshold"/>).
/// Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением управляет <see cref="PlantGrowthSystem"/>.
/// </summary>
public static void Spawn(
Scene scene,
GameContent content,
PlantSet plants,
TerrainDef terrain,
Point cell,
int cellSize,
Random random
)
{
// Статичная освещённость клетки в полдень: затенена только светонепроницаемым террейном (полог
// крон сейчас не учитывается). По ней решаем, годится ли клетка теневому виду.
var exposure = terrain.BlocksLight ? LightmapBuilder.OccluderShade : 1f;
foreach (var entry in terrain.Scatter)
{
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
{
continue;
}
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
if (
Suitability.Gaussian(exposure, def.Genome.OptimalLight, def.Genome.LightTolerance)
< ShadeSpawnThreshold
)
{
continue; // теневому виду в ярко освещённой клетке не место
}
var index = plants.IndexOf(def);
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
var age = random.NextSingle() * plants[index].MaturityDays;
PlantFactory.Create(
scene.Store,
plants,
index,
new Vector2(px, py),
age,
plants[index].Template.Generate(random),
terrain.Fertility,
cellSize
);
}
}
}
+5 -2
View File
@@ -32,7 +32,7 @@ public sealed class SettingsScene : Scene
_host = (GameHost)Context.Services.Get<Game>(); _host = (GameHost)Context.Services.Get<Game>();
var input = this.UseInput(); var input = this.UseInput();
_desktop = this.UseUI(); _desktop = this.UseScaledUI();
Rebuild(); Rebuild();
UpdateSystems.Add( UpdateSystems.Add(
@@ -47,12 +47,15 @@ public sealed class SettingsScene : Scene
} }
private void Rebuild() => private void Rebuild() =>
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back)); _desktop.Root = Ui.MenuScreen(
SettingsPanel.Build(_settings, _content.Languages, Apply, Back)
);
private void Apply() private void Apply()
{ {
GameSettingsStore.Save(_settings); GameSettingsStore.Save(_settings);
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio); GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
UiScaling.Apply(Context, _desktop, _settings.UiScale); // масштаб UI — сразу
Rebuild(); // язык мог смениться — перестраиваем подписи Rebuild(); // язык мог смениться — перестраиваем подписи
} }

Some files were not shown because too many files have changed in this diff Show More