63 Commits
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 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
164 changed files with 12175 additions and 1776 deletions
+4 -1
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" } }
]
}
+15
View File
@@ -15,6 +15,21 @@
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"], "default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
"effects": { "leafHue": "value" } }, "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). Собственное значение гена не используется. // (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness", { "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
+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
View File
@@ -3,6 +3,7 @@
// Трава: быстрый почвопокров, основной скаттер лугов и леса. // Трава: быстрый почвопокров, основной скаттер лугов и леса.
"defs": [ "defs": [
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5, { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5,
"baseMassKg": 0.4,
"harvestProduct": "ProductGrass", "harvestProduct": "ProductGrass",
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8, "genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8,
"coldHardiness": 9, "heatHardiness": 6, "coldHardiness": 9, "heatHardiness": 6,
+12 -2
View File
@@ -4,6 +4,7 @@
// через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …). // через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …).
"defs": [ "defs": [
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"baseMassKg": 450,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10,
"coldHardiness": 14, "heatHardiness": 12, "coldHardiness": 14, "heatHardiness": 12,
@@ -19,6 +20,7 @@
// Деревья тёплой и холодной температурных ниш (showcase температурных генов). // Деревья тёплой и холодной температурных ниш (showcase температурных генов).
{ "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28, { "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
"baseMassKg": 420,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9, "genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9,
"coldHardiness": 8, "heatHardiness": 16, "coldHardiness": 8, "heatHardiness": 16,
@@ -32,6 +34,7 @@
{ "sizeCells": 2.0, "label": "plant.stage.mature" } { "sizeCells": 2.0, "label": "plant.stage.mature" }
] }, ] },
{ "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26, { "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26,
"baseMassKg": 500,
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn", "harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
"genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12, "genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12,
"coldHardiness": 18, "heatHardiness": 8, "coldHardiness": 18, "heatHardiness": 8,
@@ -46,6 +49,7 @@
] }, ] },
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4, { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
"baseMassKg": 6,
"fruitProduct": "ProductBerry", "fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9, "genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9,
"coldHardiness": 10, "heatHardiness": 8, "coldHardiness": 10, "heatHardiness": 8,
@@ -60,13 +64,15 @@
// Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы. // Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы.
{ "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0, { "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0,
"baseMassKg": 35,
"harvestProduct": "ProductFiber", "harvestProduct": "ProductFiber",
"genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8, "genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8,
"coldHardiness": 20, "heatHardiness": 18, "coldHardiness": 20, "heatHardiness": 18,
"optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5, "optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5,
"lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6, "lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6,
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08, "mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 }, "fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
"thorns": 0.6, "palatability": 0.5 },
"stages": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" } { "label": "plant.stage.mature" }
@@ -75,13 +81,15 @@
// Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно // Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно
// (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение. // (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение.
{ "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0, { "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0,
"baseMassKg": 0.3,
"harvestProduct": "ProductMushroom", "harvestProduct": "ProductMushroom",
"genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11, "genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11,
"coldHardiness": 12, "heatHardiness": 8, "coldHardiness": 12, "heatHardiness": 8,
"optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2, "optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2,
"lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9, "lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9,
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1, "mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
"fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 }, "fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
"toxicity": 0.5, "palatability": 0.4 },
"stages": [ "stages": [
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" }, { "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
{ "label": "plant.stage.mature" } { "label": "plant.stage.mature" }
@@ -89,6 +97,7 @@
// Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда). // Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда).
{ "defName": "BaseFlower", "abstract": true, "label": "plant.flower", "sizeCells": 1.0, "trunkRadiusCells": 0, { "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, "genome": { "optimalLight": 0.9, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 9,
"coldHardiness": 8, "heatHardiness": 8, "coldHardiness": 8, "heatHardiness": 8,
"optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4, "optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4,
@@ -101,6 +110,7 @@
] }, ] },
{ "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0, { "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0,
"baseMassKg": 5,
"fruitProduct": "ProductBerry", "fruitProduct": "ProductBerry",
"genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9, "genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9,
"coldHardiness": 10, "heatHardiness": 7, "coldHardiness": 10, "heatHardiness": 7,
+11 -6
View File
@@ -3,11 +3,16 @@
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct; // Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
// количество задаётся генами (harvestAmount / fruitYield). // количество задаётся генами (harvestAmount / fruitYield).
"defs": [ "defs": [
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" }, { "defName": "ProductWood", "label": "product.wood", "kind": "material", "massKg": 1.0 },
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" }, { "defName": "ProductGrass", "label": "product.grass", "kind": "material", "massKg": 0.1 },
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material" }, { "defName": "ProductFiber", "label": "product.fiber", "kind": "material", "massKg": 0.2 },
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" }, { "defName": "ProductBerry", "label": "product.berry", "kind": "food", "massKg": 0.05 },
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }, { "defName": "ProductAcorn", "label": "product.acorn", "kind": "food", "massKg": 0.05 },
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" } { "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 }
]
}
+2 -4
View File
@@ -19,7 +19,7 @@
{ "chance": 0.20, "options": ["Strawberry", "Raspberry"] }, { "chance": 0.20, "options": ["Strawberry", "Raspberry"] },
{ "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] } { "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] }
] }, ] },
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50], { "defName": "Forest", "label": "terrain.forest", "maxHeight": 1.01, "color": [44, 110, 50],
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6, "isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
"scatter": [ "scatter": [
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA", { "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA",
@@ -28,8 +28,6 @@
{ "chance": 0.16, "options": ["BushA", "BushB", "BushC"] }, { "chance": 0.16, "options": ["BushA", "BushB", "BushC"] },
{ "chance": 0.14, "options": ["Strawberry", "Raspberry"] }, { "chance": 0.14, "options": ["Strawberry", "Raspberry"] },
{ "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] } { "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] }
] }, ] }
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
] ]
} }
+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 }
] ]
} }
+115 -3
View File
@@ -7,13 +7,21 @@
"season.autumn": "autumn", "season.autumn": "autumn",
"season.winter": "winter", "season.winter": "winter",
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — 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.overview": "Overview",
"inspect.tab.genes": "Genes", "inspect.tab.genes": "Genes",
"inspect.tab.products": "Products", "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.stage": "Stage: {0} ({1}/{2})",
"inspect.growing": "Growth: {0:0}% to next stage", "inspect.growing": "Growth: {0:0}% to next stage",
"inspect.mature": "Fully grown", "inspect.mature": "Fully grown",
"inspect.age": "Age: {0:0.0} d · lives to {1:0}", "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": "State: {0}",
"inspect.state.growing": "growing", "inspect.state.growing": "growing",
"inspect.state.mature": "mature", "inspect.state.mature": "mature",
@@ -21,16 +29,67 @@
"inspect.state.dormanthot": "dormant — too hot", "inspect.state.dormanthot": "dormant — too hot",
"inspect.stress": "Stress: {0:0.0} d to death", "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.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
"inspect.genes": "Genes", "inspect.genes": "Genes",
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d", "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.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.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}", "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.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.gene.variant": "Morph: recessive variant",
"inspect.harvest": "Harvest: {0} ×{1:0.#}", "inspect.harvest": "Harvest: {0} ×{1:0.#}",
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}", "inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
"inspect.barren": "Bears no fruit", "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", "inspect.hint": "LMB — select · RMB/Esc — clear",
"hud.paused": "PAUSED", "hud.paused": "PAUSED",
"menu.title": "LittleSim", "menu.title": "LittleSim",
@@ -40,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",
@@ -61,6 +121,18 @@
"settings.resolution": "Resolution", "settings.resolution": "Resolution",
"settings.volume": "Volume", "settings.volume": "Volume",
"settings.uiscale": "UI scale", "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",
@@ -70,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",
@@ -83,7 +155,6 @@
"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",
@@ -121,8 +192,49 @@
"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",
+115 -3
View File
@@ -7,13 +7,21 @@
"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.overview": "Обзор",
"inspect.tab.genes": "Гены", "inspect.tab.genes": "Гены",
"inspect.tab.products": "Продукты", "inspect.tab.products": "Продукты",
"inspect.tab.needs": "Нужды",
"inspect.tab.skills": "Навыки",
"inspect.tab.health": "Здоровье",
"inspect.tab.mood": "Настроение",
"inspect.stage": "Стадия: {0} ({1}/{2})", "inspect.stage": "Стадия: {0} ({1}/{2})",
"inspect.growing": "Рост: {0:0}% до следующей стадии", "inspect.growing": "Рост: {0:0}% до следующей стадии",
"inspect.mature": "Полностью выросло", "inspect.mature": "Полностью выросло",
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}", "inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
"inspect.mass": "Вес: {0:0.#} кг · несёт до {1:0.#} кг",
"inspect.plantmass": "Вес: {0:0.##} кг",
"inspect.state": "Состояние: {0}", "inspect.state": "Состояние: {0}",
"inspect.state.growing": "растёт", "inspect.state.growing": "растёт",
"inspect.state.mature": "созрело", "inspect.state.mature": "созрело",
@@ -21,16 +29,67 @@
"inspect.state.dormanthot": "покой — слишком жарко", "inspect.state.dormanthot": "покой — слишком жарко",
"inspect.stress": "Стресс: {0:0.0} дн до гибели", "inspect.stress": "Стресс: {0:0.0} дн до гибели",
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C", "inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
"inspect.genes": "Гены", "inspect.genes": "Гены",
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн", "inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}", "inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн", "inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}", "inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2: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.gene.variant": "Морфа: рецессивный вариант",
"inspect.harvest": "Сбор: {0} ×{1:0.#}", "inspect.harvest": "Сбор: {0} ×{1:0.#}",
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}", "inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
"inspect.barren": "Не плодоносит", "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 — снять", "inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
"hud.paused": "ПАУЗА", "hud.paused": "ПАУЗА",
"menu.title": "LittleSim", "menu.title": "LittleSim",
@@ -40,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": "Новый мир",
@@ -61,6 +121,18 @@
"settings.resolution": "Разрешение", "settings.resolution": "Разрешение",
"settings.volume": "Громкость", "settings.volume": "Громкость",
"settings.uiscale": "Масштаб UI", "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": "Пауза",
@@ -70,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",
@@ -83,7 +155,6 @@
"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": "сосна",
@@ -121,8 +192,49 @@
"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": "подключено",
+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.
+1 -1
Submodule engine updated: 96e19c7c61...46f3931f85
+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>
+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."
);
}
}
}
} }
-16
View File
@@ -9,22 +9,6 @@ using MrGameEng.Net;
namespace LittleSim.Web; namespace LittleSim.Web;
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
public enum ConnectionStatus
{
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
Idle,
/// <summary>Идёт первая попытка подключения.</summary>
Connecting,
/// <summary>Соединение установлено, снапшоты приходят.</summary>
Connected,
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
Reconnecting,
}
/// <summary> /// <summary>
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к /// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой /// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
+2 -63
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>
@@ -31,63 +26,7 @@ public sealed class GameSettings
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary> /// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
public float UiScale { get; set; } = 1f; public float UiScale { get; set; } = 1f;
}
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary> /// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
public static class GameSettingsStore public bool DeveloperMode { get; set; } = true;
{
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();
}
} }
+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'");
-210
View File
@@ -1,210 +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/): порог высоты, цвет, поверхность, скаттер растений.</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/): что растение даёт при сборе (древесина, трава) или плодоношении
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
/// определяют гены. <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>Морозостойкость (°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>Растение (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>
/// Пресет размера мира (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; }
}
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное).</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;
}
}
+4
View File
@@ -118,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)
+1 -1
View File
@@ -53,7 +53,7 @@ public sealed class BootScene : Scene
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
{ {
+4 -4
View File
@@ -17,15 +17,15 @@ public sealed class CreditsScene : Scene
var input = this.UseInput(); var input = this.UseInput();
var desktop = this.UseScaledUI(); // ставит 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(() =>
{ {
+1 -1
View File
@@ -16,7 +16,7 @@ public static class GameLayers
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary> /// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
public static LayerId Selection { get; private set; } public static LayerId Selection { get; private set; }
/// <summary>Регистрирует слои при первом вызове.</summary> /// <summary>Регистрирует слои при первом вызове (порядок = порядок отрисовки).</summary>
public static void EnsureRegistered(Renderer2D renderer) public static void EnsureRegistered(Renderer2D renderer)
{ {
if (_registered) if (_registered)
+4 -4
View File
@@ -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()
+10 -10
View File
@@ -19,19 +19,19 @@ public sealed class MainMenuScene : Scene
var lang = content.Languages; var lang = content.Languages;
var desktop = this.UseScaledUI(); // ставит 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)
+4 -4
View File
@@ -38,9 +38,9 @@ public sealed class NewWorldScene : Scene
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()
+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
);
}
}
}
+3 -1
View File
@@ -47,7 +47,9 @@ 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()
{ {
+63
View File
@@ -0,0 +1,63 @@
using LittleSim.App;
using LittleSim.Content;
using LittleSim.UI;
using MrGameEng.Core;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
/// <summary>
/// Экран загрузки между окном создания/выбора мира и игровой сценой. Генерация мира
/// (<see cref="WorldScene.OnLoad"/>: рельеф, атласы, спавн) идёт синхронно и блокирует кадр; раньше под
/// ней был лишь сплошной фейд без индикатора. Эта сцена рисует надпись «Создание мира…», даёт ей
/// прорисоваться, затем переключается на <see cref="WorldScene"/> БЕЗ перехода — её последний кадр
/// (надпись) и остаётся на экране, пока идёт генерация, давая игроку понятную обратную связь.
/// </summary>
public sealed class WorldLoadingScene : Scene
{
private readonly WorldConfig _config;
private readonly WorldSave? _save;
private GameSettings _settings = new();
private Label _label = null!;
private int _frames;
/// <summary>Новый мир (<paramref name="save"/> = null) или загрузка сохранения.</summary>
public WorldLoadingScene(WorldConfig config, WorldSave? save = null)
{
_config = config;
_save = save;
}
protected override void OnLoad()
{
_settings = GameSettingsStore.Load();
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
_label = new Label
{
TextColor = Ui.Accent,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
desktop.Root = Ui.MenuScreen(_label);
UpdateSystems.Add(new CallbackSystem(Tick));
}
private void Tick()
{
var content = Context.Services.Get<GameContent>();
var word = content.Languages.Get("loading.world");
var dots = new string('.', (int)(Context.Clock.UnscaledTotalTime * 2) % 4);
_label.Text = word + dots;
// Дождаться, пока надпись точно прорисована (переход въезда завершён + пара кадров), затем
// мгновенно переключиться: блокирующий WorldScene.OnLoad оставит на экране кадр с надписью.
_frames++;
if (Context.Scenes.IsTransitioning || _frames < 2)
{
return;
}
Context.Scenes.Switch(new WorldScene(_config, _save));
}
}
+669
View File
@@ -0,0 +1,669 @@
using System;
using System.Collections.Generic;
using Friflo.Engine.ECS;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.Sim;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Formulas;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.Inspector;
using MrGameEng.Lighting;
using MrGameEng.Tilemaps;
using MrGameEng.UI;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
// Часть WorldScene: регистрация и обработчики команд дев-консоли.
public sealed partial class WorldScene
{
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
{
ContentCommands.Register(console, content, atlases);
console.Register(
"regen",
"regen [seed] — regenerate the world with a new seed",
(c, args) =>
{
if (Context.Scenes.IsTransitioning)
{
return;
}
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
c.WriteLine($"regenerating world, seed {seed}");
Context.Scenes.Switch(
new WorldScene(_config with { Seed = seed }),
Transitions.Fade(0.6f)
);
}
);
console.Register(
"formula",
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
(c, args) =>
{
if (args.Length == 0)
{
c.WriteLine(
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
);
return;
}
var expression = string.Join(' ', args);
try
{
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
}
catch (FormulaException e)
{
c.WriteLine($"error: {e.Message}");
}
}
);
console.Register(
"gene",
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
(c, args) => RunGeneDemo(c, content, args)
);
console.Register(
"plant",
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
(c, args) => RunPlantDemo(c, content, args)
);
console.Register(
"animal",
"animal <species> [seed] — sample an animal genome and show its gene-driven traits",
(c, args) => RunAnimalDemo(c, content, args)
);
console.Register(
"diet",
"diet [species] — gene-derived diet (herbivory/carnivory/omnivory), role and hunting stats",
(c, args) => RunDiet(c, content, args)
);
console.Register(
"popstats",
"popstats [species] — live population trait means and generation span (selection drift)",
(c, args) => RunPopStats(c, args)
);
console.Register(
"body",
"body <species> — dump a species body parts and a healthy individual's capacities",
(c, args) => RunBodyDemo(c, content, args)
);
console.Register(
"infect",
"infect [hediff] — give every animal a disease (default Fever) to test the health system",
(c, args) => RunInfect(c, content, args)
);
console.Register(
"intel",
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
(c, args) => RunIntel(c, content, args)
);
console.Register(
"mood",
"mood [species] — live population mood (avg/min/max) and content/stressed counts",
(c, args) => RunMood(c, args)
);
console.Register(
"skills",
"skills — mean skill levels across living animals (grow from practice, decay unused)",
(c, _) => RunSkills(c)
);
console.Register(
"menu",
"menu — return to the main menu",
(_, _) => Switch(new MainMenuScene())
);
}
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
{
var genes = content.Defs.All<GeneDef>();
if (genes.Count == 0)
{
console.WriteLine("no Gene defs loaded");
return;
}
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
var random = new Random(seed);
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
var parentA = Genome.Generate(genes, random);
foreach (var gene in genes)
{
var allele = parentA[gene.DefName];
console.WriteLine(
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
);
}
console.WriteLine("traits:");
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
{
console.WriteLine($" {trait} = {value:0.###}");
}
var parentB = Genome.Generate(genes, random);
var child = Genome.Breed(parentA, parentB, registry, random);
var childTraits = Phenotype.Compute(child, registry);
console.WriteLine(
$"bred child: {child.Alleles.Count} genes, "
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
);
}
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Plant");
if (args.Length == 0)
{
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
{
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var index = _plants.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
var genome = _plants[index].Template.Generate(new Random(seed));
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
console.WriteLine(
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
);
console.WriteLine(
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
);
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
console.WriteLine(
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
);
console.WriteLine(
$" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, "
+ $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})"
);
console.WriteLine($" mass (mature): {def.BaseMassKg:0.##} kg");
if (def.HarvestProduct is { } harvest)
{
console.WriteLine(
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
);
}
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
{
var season = content.Languages.Get(
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
);
console.WriteLine(
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
);
}
else
{
console.WriteLine(" barren (no fruit)");
}
}
// Демонстрация животного (фаза A1): по виду берём его набор генов, генерируем особь и печатаем
// признаки, вычисленные формулами генов (тело/мозг/метаболизм/чувства/мех).
private void RunAnimalDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Animal");
if (args.Length == 0)
{
console.WriteLine("usage: animal <species> [seed] e.g. animal Deer");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
{
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var index = _animals.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
var genome = _animals.GenerateGenome(index, new Random(seed));
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
var sex = AnimalFactory.IsMaleGenome(genome) ? "male" : "female";
console.WriteLine($"{def.DefName} ({sex}, seed {seed}):");
console.WriteLine(
$" body {traits.BodySize:0.##} cells, maturity {traits.MaturityAge:0} d, "
+ $"lifespan {traits.Lifespan:0} d, brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}"
);
console.WriteLine(
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}"
);
console.WriteLine(
$" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, "
+ $"toxinTolerance {traits.ToxinTolerance:0.##}"
);
console.WriteLine(
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
);
console.WriteLine(
$" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}"
);
var adultMass = Mass.OfAnimal(def, traits, 1f);
console.WriteLine(
$" mass: {adultMass:0.#} kg (adult), carries ~{Mass.CarryCapacity(adultMass, 1f):0.#} kg"
);
}
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
// выведенный рацион (ест растения/мясо), роль и боевые параметры — проверка, что хищники заданы.
private void RunDiet(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Animal");
for (var s = 0; s < _animals.Count; s++)
{
var sp = _animals[s];
if (
args.Length > 0
&& !string.Equals(sp.Def.DefName, args[0], StringComparison.OrdinalIgnoreCase)
)
{
continue;
}
var genome = _animals.GenerateGenome(s, new Random(0x01E7 + s));
var traits = AnimalPhenotype.FromTraits(
Phenotype.Compute(genome, _animals.GeneRegistry)
);
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(traits, sp.Def);
var role =
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
: eatsPlants ? "herbivore"
: "none";
console.WriteLine(
$"{sp.Def.DefName}: {role} — herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / "
+ $"omni {traits.Omnivory:0.##} -> plants={eatsPlants} meat={eatsMeat}, spawn {sp.Def.SpawnPer1000Cells:0.##}/1000"
);
if (eatsMeat && sp.Def.AttackDamage > 0f)
{
console.WriteLine(
$" hunt: dmg {sp.Def.AttackDamage:0}/d, bleed {sp.Def.AttackBleed:0.#}/d, "
+ $"bloodloss {sp.Def.AttackBloodLoss:0.#}/d, range {sp.Def.AttackRangeCells:0.#} cells"
);
}
}
if (args.Length == 0)
{
console.WriteLine($"species: {string.Join(", ", species)}");
}
}
// Наблюдаемость отбора (фаза A2): средние ключевых признаков живой популяции и размах поколений —
// видно дрейф генов под отбором. Без аргумента — все виды; с аргументом — один вид.
private void RunPopStats(DevConsole console, string[] args)
{
if (_animals.Count == 0)
{
console.WriteLine("no animal species");
return;
}
var count = new int[_animals.Count];
var body = new double[_animals.Count];
var brain = new double[_animals.Count];
var life = new double[_animals.Count];
var move = new double[_animals.Count];
var tox = new double[_animals.Count];
var maxGen = new int[_animals.Count];
Store
.Query<AnimalOrganism>()
.ForEachEntity(
(ref AnimalOrganism o, Entity _) =>
{
var s = o.Species;
count[s]++;
body[s] += o.Traits.BodySize;
brain[s] += o.Traits.BrainSize;
life[s] += o.Traits.Lifespan;
move[s] += o.Traits.MoveSpeed;
tox[s] += o.Traits.ToxinTolerance;
if (o.Generation > maxGen[s])
{
maxGen[s] = o.Generation;
}
}
);
var filter = args.Length > 0 ? args[0] : null;
var any = false;
for (var s = 0; s < _animals.Count; s++)
{
var name = _animals[s].Def.DefName;
if (
count[s] == 0
|| (
filter is not null
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
)
)
{
continue;
}
any = true;
var c = count[s];
console.WriteLine(
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, "
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, "
+ $"toxinTol {tox[s] / c:0.##}"
);
}
if (!any)
{
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
}
// Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра.
if (filter is null)
{
var pn = 0;
double pTox = 0,
pThorn = 0,
pPalat = 0;
Store
.Query<PlantOrganism>()
.ForEachEntity(
(ref PlantOrganism o, Entity _) =>
{
pn++;
pTox += o.Traits.Toxicity;
pThorn += o.Traits.Thorns;
pPalat += o.Traits.Palatability;
}
);
if (pn > 0)
{
console.WriteLine(
$"plants: n={pn}, toxicity {pTox / pn:0.###}, thorns {pThorn / pn:0.###}, "
+ $"palatability {pPalat / pn:0.###}"
);
}
}
}
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
private void RunBodyDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Animal");
if (args.Length == 0)
{
console.WriteLine("usage: body <species> e.g. body Deer");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
{
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
if (string.IsNullOrEmpty(def.Body) || !content.Defs.TryGet<BodyDef>(def.Body, out var body))
{
console.WriteLine($"{def.DefName}: no body");
return;
}
console.WriteLine($"{def.DefName} body '{body.DefName}': {body.Parts.Length} parts");
foreach (var part in body.Parts)
{
var caps =
part.Capacities.Count > 0
? " ["
+ string.Join(
", ",
part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")
)
+ "]"
: "";
console.WriteLine(
$" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}"
);
}
var state = HealthState.Create(body, 1f);
console.WriteLine(
"capacities (healthy): "
+ string.Join(
", ",
state.Capacities.OrderBy(k => k.Key).Select(kv => $"{kv.Key} {kv.Value:0.##}")
)
);
}
// Тест системы здоровья (фаза A6): навешивает болезнь всем живым животным.
private void RunInfect(DevConsole console, GameContent content, string[] args)
{
var name = args.Length > 0 ? args[0] : "Fever";
if (!content.Defs.TryGet<HediffDef>(name, out var def))
{
console.WriteLine($"no hediff '{name}'");
return;
}
var count = 0;
Store
.Query<Health>()
.ForEachEntity(
(ref Health h, Entity _) =>
{
h.State?.Add(def);
count++;
}
);
console.WriteLine($"infected {count} animals with {def.DefName}");
}
// Наблюдаемость интеллекта (фаза A7): мозг особи (ген) × сознание = эффективный интеллект, который
// гейтит набор нужд/действий. Показывает активные/отключённые нужды при заданном уровне сознания
// (по умолчанию 1 — здоров; меньше — имитация повреждения мозга/болезни/боли).
private void RunIntel(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Animal");
if (args.Length == 0)
{
console.WriteLine(
"usage: intel <species> [consciousness 0..1] [seed] e.g. intel Deer 0.6"
);
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
{
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var consciousness =
args.Length > 1
? Math.Clamp(
float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture),
0f,
1f
)
: 1f;
var seed = args.Length > 2 ? int.Parse(args[2]) : Random.Shared.Next();
var index = _animals.IndexOf(def);
var genome = _animals.GenerateGenome(index, new Random(seed));
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
var intelligence = traits.BrainSize * consciousness;
console.WriteLine(
$"{def.DefName} (seed {seed}): brain {traits.BrainSize:0.##} x consciousness {consciousness:0.##} "
+ $"= intelligence {intelligence:0.###}"
);
var active = new List<string>();
var gated = new List<string>();
for (var k = 0; k < _needs.Count; k++)
{
var need = _needs[k];
(need.MinBrain > intelligence ? gated : active).Add(
$"{need.DefName}({need.MinBrain:0.##})"
);
}
console.WriteLine($" active: {(active.Count > 0 ? string.Join(", ", active) : "")}");
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "")}");
}
// Наблюдаемость настроения (фаза A8): среднее/мин/макс настроения живой популяции по виду и счётчики
// довольных (>0.7) / угнетённых (<0.35). Считает только особей с активным настроением (тир 4+).
private void RunMood(DevConsole console, string[] args)
{
if (_animals.Count == 0)
{
console.WriteLine("no animal species");
return;
}
var count = new int[_animals.Count];
var sum = new double[_animals.Count];
var min = new float[_animals.Count];
var max = new float[_animals.Count];
var content_ = new int[_animals.Count];
var stressed = new int[_animals.Count];
Array.Fill(min, 1f);
Store
.Query<AnimalOrganism, Mood>()
.ForEachEntity(
(ref AnimalOrganism o, ref Mood m, Entity _) =>
{
if (!m.Active)
{
return; // тир ниже зверя — настроения нет
}
var s = o.Species;
count[s]++;
sum[s] += m.Value;
min[s] = MathF.Min(min[s], m.Value);
max[s] = MathF.Max(max[s], m.Value);
if (m.Value > 0.7f)
{
content_[s]++;
}
else if (m.Value < 0.35f)
{
stressed[s]++;
}
}
);
var filter = args.Length > 0 ? args[0] : null;
var any = false;
for (var s = 0; s < _animals.Count; s++)
{
var name = _animals[s].Def.DefName;
if (
count[s] == 0
|| (
filter is not null
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
)
)
{
continue;
}
any = true;
console.WriteLine(
$"{name}: n={count[s]}, mood {sum[s] / count[s]:0.##} (min {min[s]:0.##}, max {max[s]:0.##}), "
+ $"content {content_[s]}, stressed {stressed[s]}"
);
}
if (!any)
{
console.WriteLine(filter is null ? "no animals with mood" : $"no '{filter}' with mood");
}
}
// Наблюдаемость навыков: средний уровень каждого навыка по живым животным (растут от практики).
private void RunSkills(DevConsole console)
{
if (_skills.Count == 0)
{
console.WriteLine("no skill defs loaded");
return;
}
var sum = new double[_skills.Count];
var n = 0;
Store
.Query<Skills>()
.ForEachEntity(
(ref Skills s, Entity _) =>
{
if (s.Levels is null)
{
return;
}
n++;
for (var i = 0; i < _skills.Count && i < s.Levels.Length; i++)
{
sum[i] += s.Levels[i];
}
}
);
console.WriteLine($"skills over {n} animals (mean level):");
for (var i = 0; i < _skills.Count; i++)
{
console.WriteLine(
$" {_skills[i].DefName}: {(n > 0 ? sum[i] / n : 0):0.###} "
+ $"(learn {_skills[i].LearnRate:0.##}/d, decay {_skills[i].DecayPerDay:0.###}/d)"
);
}
}
private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
: productDefName;
}
+463
View File
@@ -0,0 +1,463 @@
using System;
using System.Collections.Generic;
using Friflo.Engine.ECS;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.Sim;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Formulas;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.Inspector;
using MrGameEng.Lighting;
using MrGameEng.Tilemaps;
using MrGameEng.UI;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
// Часть WorldScene: сохранение/загрузка мира и стартовый спавн животных.
public sealed partial class WorldScene
{
private string SaveWorld()
{
var save = new WorldSave
{
Name = _config.Name,
Width = _config.Width,
Height = _config.Height,
Seed = _config.Seed,
SmoothPasses = _config.SmoothPasses,
SavedUtc = DateTime.UtcNow,
ElapsedSeconds = Context.Clock.TotalTime,
};
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
Store
.Query<PlantGrowth, PlantOrganism, Transform2D>()
.ForEachEntity(
(
ref PlantGrowth grow,
ref PlantOrganism org,
ref Transform2D transform,
Entity _
) =>
{
save.Plants.Add(
new PlantSave
{
Species = _plants[grow.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
AgeDays = grow.AgeDays,
Stage = grow.Stage,
CellFertility = grow.CellFertility,
Genome = org.Genome.ToDictionary(),
}
);
}
);
SaveAnimals(save);
// Население — живой счётчик животных на момент сохранения (а не статичный пресет конфига).
save.Population = save.Animals.Count;
new SaveStore().Write(save);
Log.Info(
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
);
return _config.Name;
}
// Снимок популяции животных и трупов: всё нужное, чтобы эволюция/состояние пережили сейв/лоад
// (а не пересыпались из сида). Health/Mood/Pregnant берём через entity — запрос ограничен 5 типами.
private void SaveAnimals(WorldSave save)
{
Store
.Query<AnimalOrganism, AnimalGrowth, AnimalNeeds, Transform2D>()
.ForEachEntity(
(
ref AnimalOrganism org,
ref AnimalGrowth grow,
ref AnimalNeeds needs,
ref Transform2D transform,
Entity entity
) =>
{
var record = new AnimalSave
{
Species = _animals[org.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
Generation = org.Generation,
Stage = grow.Stage,
AgeDays = grow.AgeDays,
Genome = org.Genome.ToDictionary(),
Needs = needs.Values,
Starve = needs.Starve,
};
if (entity.GetComponent<Health>().State is { } health)
{
record.BloodLevel = health.BloodLevel;
record.PartHp = health.Parts.Select(p => p.Hp).ToArray();
foreach (var hediff in health.Hediffs)
{
record.Hediffs.Add(
new HediffSave
{
DefName = hediff.Def.DefName,
Severity = hediff.Severity,
Immunity = hediff.Immunity,
}
);
}
}
var mood = entity.GetComponent<Mood>();
record.Mood = mood.Value;
record.MoodActive = mood.Active;
if (mood.Thoughts is not null)
{
foreach (var thought in mood.Thoughts)
{
record.Thoughts.Add(
new ThoughtSave
{
DefName = thought.Def.DefName,
RemainingDays = thought.RemainingDays,
}
);
}
}
if (entity.TryGetComponent<Pregnant>(out var preg))
{
record.Pregnant = true;
record.FatherGenome = preg.FatherGenome.ToDictionary();
record.DueInDays = preg.DueInDays;
record.Litter = preg.Litter;
}
if (entity.TryGetComponent<Skills>(out var sk))
{
record.SkillLevels = sk.Levels ?? [];
record.SkillPassions = sk.Passions is null
? []
: Array.ConvertAll(sk.Passions, p => (int)p);
}
save.Animals.Add(record);
}
);
Store
.Query<Corpse, Transform2D>()
.ForEachEntity(
(ref Corpse corpse, ref Transform2D transform, Entity _) =>
save.Corpses.Add(
new CorpseSave
{
Species = _animals[corpse.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
RotDays = corpse.RotDays,
BodySize = corpse.BodySize,
Meat = corpse.Meat,
}
)
);
Store
.Query<Egg, Transform2D>()
.ForEachEntity(
(ref Egg egg, ref Transform2D transform, Entity _) =>
save.Eggs.Add(
new EggSave
{
Species = _animals[egg.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
Generation = egg.Generation,
IncubateDays = egg.IncubateDays,
Genome = egg.Genome?.ToDictionary() ?? new(),
}
)
);
}
private void RestorePlants(GameContent content)
{
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
foreach (var plant in _save!.Plants)
{
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
{
continue; // вид пропал (мод убрали) — пропускаем растение
}
var index = _plants.IndexOf(def);
var genome = plant.Genome is { Count: > 0 }
? new Genome(plant.Genome)
: _plants[index].Template.Generate(fallback);
PlantFactory.Create(
Store,
_plants,
index,
new Vector2(plant.X, plant.Y),
plant.AgeDays,
genome,
plant.CellFertility,
CellSize
);
}
}
// Восстанавливает популяцию животных и трупы из сейва: фабрика собирает особь из генома (геном
// несёт и пол), затем динамическое состояние (нужды/стадия/здоровье/настроение/беременность)
// накладывается поверх. Старый/битый сейв без генома — особь регенерируется из шаблона вида.
private void RestoreAnimals(GameContent content)
{
var fallback = new Random(_config.Seed);
foreach (var saved in _save!.Animals)
{
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
{
continue; // вид пропал (мод убрали) — пропускаем особь
}
var index = _animals.IndexOf(def);
var genome = saved.Genome is { Count: > 0 }
? new Genome(saved.Genome)
: _animals.GenerateGenome(index, fallback);
var entity = AnimalFactory.Create(
Store,
_animals,
_needs,
_skills,
index,
new Vector2(saved.X, saved.Y),
saved.AgeDays,
genome,
saved.Generation,
CellSize
);
RestoreAnimalState(entity, saved, content);
}
foreach (var saved in _save.Corpses)
{
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
{
continue;
}
var corpse = AnimalFactory.CreateCorpse(
Store,
_animals,
_animals.IndexOf(def),
new Vector2(saved.X, saved.Y),
saved.BodySize,
saved.Meat,
CellSize
);
ref var data = ref corpse.GetComponent<Corpse>();
data.RotDays = saved.RotDays;
data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике
}
foreach (var saved in _save.Eggs)
{
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
{
continue; // вид пропал (мод убрали) — пропускаем яйцо
}
var index = _animals.IndexOf(def);
var genome = saved.Genome is { Count: > 0 }
? new Genome(saved.Genome)
: _animals.GenerateGenome(index, fallback);
AnimalFactory.CreateEgg(
Store,
_animals,
index,
new Vector2(saved.X, saved.Y),
genome,
saved.Generation,
saved.IncubateDays,
CellSize
);
}
Log.Info(
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
);
}
// Накладывает на свежесозданную особь сохранённое динамическое состояние. Ref-компоненты правим до
// AddComponent(Pregnant) (структурное изменение инвалидирует ссылки); Health — managed, безопасен.
private void RestoreAnimalState(Entity entity, AnimalSave saved, GameContent content)
{
ref var needs = ref entity.GetComponent<AnimalNeeds>();
if (saved.Needs is { Length: > 0 } && saved.Needs.Length == _needs.Count)
{
needs.Values = saved.Needs; // состав совпал — иначе остаются свежие из фабрики
}
needs.Starve = saved.Starve;
entity.GetComponent<AnimalGrowth>().Stage = saved.Stage;
ref var mood = ref entity.GetComponent<Mood>();
mood.Value = saved.Mood;
mood.Active = saved.MoodActive;
mood.Thoughts.Clear();
foreach (var thought in saved.Thoughts)
{
if (content.Defs.TryGet<ThoughtDef>(thought.DefName, out var tdef))
{
mood.Thoughts.Add(
new ThoughtInstance { Def = tdef, RemainingDays = thought.RemainingDays }
);
}
}
if (entity.GetComponent<Health>().State is { } health)
{
health.BloodLevel = saved.BloodLevel;
// HP частей применяем только при точном совпадении анатомии (как для нужд/навыков): иначе
// мод сменил состав/порядок частей тела, и значения легли бы не на те части — оставляем свежее.
if (saved.PartHp.Length == health.Parts.Length)
{
for (var i = 0; i < health.Parts.Length; i++)
{
health.Parts[i].Hp = saved.PartHp[i];
}
}
else if (saved.PartHp.Length > 0)
{
Log.Warning(
$"Скелет вида '{saved.Species}' изменился ({saved.PartHp.Length}→{health.Parts.Length} частей) — HP частей не восстановлены."
);
}
foreach (var hediff in saved.Hediffs)
{
if (content.Defs.TryGet<HediffDef>(hediff.DefName, out var hdef))
{
health.AddHediff(hdef, hediff.Severity, hediff.Immunity);
}
}
health.RecomputeCapacities();
}
if (saved.Pregnant && saved.FatherGenome is { Count: > 0 })
{
entity.AddComponent(
new Pregnant
{
FatherGenome = new Genome(saved.FatherGenome),
DueInDays = saved.DueInDays,
Litter = saved.Litter,
}
);
}
// Навыки: накладываем сохранённые уровни/страсть, если состав совпал (иначе — свежие из фабрики).
if (entity.TryGetComponent<Skills>(out var skills) && skills.Levels is not null)
{
if (saved.SkillLevels.Length == skills.Levels.Length)
{
saved.SkillLevels.CopyTo(skills.Levels, 0);
}
if (skills.Passions is not null && saved.SkillPassions.Length == skills.Passions.Length)
{
for (var i = 0; i < skills.Passions.Length; i++)
{
skills.Passions[i] = (byte)Math.Clamp(saved.SkillPassions[i], 0, 255);
}
}
}
}
// Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0
// рассыпается по клеткам суши с плотностью из дефа; геном и возраст особи — из шаблона вида.
private void SpawnAnimals()
{
var landCells = 0;
for (var i = 0; i < _cellLand.Length; i++)
{
if (_cellLand[i])
{
landCells++;
}
}
if (_animals.Count == 0 || landCells == 0)
{
return;
}
// Свой офсет сида (0x5EED занят системой решений животных — нельзя связывать их RNG-потоки).
var random = new Random(_config.Seed + 0x5A1D);
var spawned = 0;
for (var s = 0; s < _animals.Count; s++)
{
var def = _animals[s].Def;
if (def.SpawnPer1000Cells <= 0f)
{
continue; // вид не спавнится сам (напр. появляется только в сейве/событием)
}
var count = Math.Max(1, (int)MathF.Round(landCells / 1000f * def.SpawnPer1000Cells));
var maxAge =
(def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f)
* 0.85f;
var placed = 0;
var attempts = 0;
while (placed < count && attempts < count * 50)
{
attempts++;
var cell = random.Next(_cellLand.Length);
if (!_cellLand[cell])
{
continue;
}
var cx = cell % _config.Width;
var cy = cell / _config.Width;
var px = (cx + 0.2f + random.NextSingle() * 0.6f) * CellSize;
var py = (cy + 0.2f + random.NextSingle() * 0.6f) * CellSize;
AnimalFactory.Create(
Store,
_animals,
_needs,
_skills,
s,
new Vector2(px, py),
ageDays: random.NextSingle() * maxAge,
_animals.GenerateGenome(s, random),
generation: 0,
CellSize
);
placed++;
}
spawned += placed;
}
Log.Info($"Spawned {spawned} animals ({_animals.Count} species)");
}
}
+226
View File
@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using Friflo.Engine.ECS;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.Sim;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Formulas;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.Inspector;
using MrGameEng.Lighting;
using MrGameEng.Tilemaps;
using MrGameEng.UI;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
// Часть WorldScene: генерация рельефа, поле берега и окклюдеры освещения.
public sealed partial class WorldScene
{
/// <summary>
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
/// </summary>
private void BuildTerrain(
GameContent content,
ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
AssetManager assets,
Random? scatterRandom
)
{
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
var tiles = new TileSet();
var tileByDef = new Dictionary<TerrainDef, ushort>();
foreach (var terrain in content.Terrains.All)
{
tileByDef[terrain] = terrain.Surface is { } surface
? tiles.Add(atlases.GetRegion(device, surface))
: tiles.Add(white, terrain.Tint);
}
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
var heights = WorldGenerator.Generate(
_config.Width,
_config.Height,
_config.Seed,
_config.SmoothPasses
);
var grid = new TileGrid(_config.Width, _config.Height);
var cells = _config.Width * _config.Height;
_cellFertility = new float[cells];
_cellLand = new bool[cells];
_cellOccluderBase = new bool[cells];
_cellOccluder = new bool[cells];
for (var x = 0; x < _config.Width; x++)
{
for (var y = 0; y < _config.Height; y++)
{
var terrain = content.Terrains.Classify(heights[x, y]);
grid[x, y] = tileByDef[terrain];
var cell = y * _config.Width + x;
_cellFertility[cell] = terrain.Fertility;
_cellLand[cell] = terrain.IsLand;
_cellOccluderBase[cell] = terrain.BlocksLight;
if (scatterRandom is not null)
{
ScatterSpawner.Spawn(
this,
content,
_plants,
terrain,
new Point(x, y),
CellSize,
scatterRandom
);
}
}
}
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
}
// Поле «ближайший берег» по клеткам мира: для каждой клетки — мировая позиция ближайшей береговой
// клетки (суша, граничащая с водой), найденная многоисточниковым BFS за O(клеток). Звери читают его
// за O(1) при поиске водопоя (см. AnimalDecisionSystem.TryFindShore), не сканируя береговую линию.
// Пусто, если воды/берега в мире нет — тогда пить негде (зверь не пойдёт).
private Vector2[] ComputeShoreField()
{
var w = _config.Width;
var h = _config.Height;
var count = w * h;
// BFS: nearest[c] — индекс ближайшей береговой клетки. Источники — все береговые клетки (шаг 0).
var nearest = new int[count];
Array.Fill(nearest, -1);
var queue = new Queue<int>();
for (var cell = 0; cell < count; cell++)
{
if (!_cellLand[cell])
{
continue;
}
var x = cell % w;
var y = cell / w;
var nearWater =
(x > 0 && !_cellLand[cell - 1])
|| (x < w - 1 && !_cellLand[cell + 1])
|| (y > 0 && !_cellLand[cell - w])
|| (y < h - 1 && !_cellLand[cell + w]);
if (nearWater)
{
nearest[cell] = cell;
queue.Enqueue(cell);
}
}
if (queue.Count == 0)
{
return []; // нет берега
}
// Волна по 4-связности: каждой клетке достаётся источник ближайшего фронта (берег по шагам сетки).
while (queue.Count > 0)
{
var cell = queue.Dequeue();
var src = nearest[cell];
var x = cell % w;
var y = cell / w;
if (x > 0 && nearest[cell - 1] < 0)
{
nearest[cell - 1] = src;
queue.Enqueue(cell - 1);
}
if (x < w - 1 && nearest[cell + 1] < 0)
{
nearest[cell + 1] = src;
queue.Enqueue(cell + 1);
}
if (y > 0 && nearest[cell - w] < 0)
{
nearest[cell - w] = src;
queue.Enqueue(cell - w);
}
if (y < h - 1 && nearest[cell + w] < 0)
{
nearest[cell + w] = src;
queue.Enqueue(cell + w);
}
}
var field = new Vector2[count];
for (var cell = 0; cell < count; cell++)
{
var src = nearest[cell];
if (src >= 0)
{
field[cell] = new Vector2((src % w + 0.5f) * CellSize, (src / w + 0.5f) * CellSize);
}
}
return field;
}
// Затеняют ли зрелые деревья землю. Сейчас выключено: одиночное дерево = 1 клетка-окклюдер, а спрайт
// кроны крупнее и смещён, плюс направленная тень тянется «хвостом» — на открытой почве это читается как
// странные оторванные пятна. Вернуть, когда у кроны будет настоящий многоклеточный след затенения.
private static readonly bool TreesCastShade = false;
// Сетка окклюдеров для лайтмапа: светонепроницаемый террейн (сейчас такого нет) + опц. зрелые деревья.
private bool[] BuildOccluders()
{
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
if (!TreesCastShade)
{
return _cellOccluder; // ровное освещение — без странных теней от отдельных деревьев
}
Store
.Query<PlantGrowth, Transform2D>()
.ForEachEntity(
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
{
var species = _plants[grow.Species];
if (
species.Def.TrunkRadiusCells <= 0f
|| grow.Stage < species.Stages.Length - 1
)
{
return; // затеняют только зрелые деревья (со стволом)
}
var cx = Math.Clamp(
(int)(transform.Position.X / CellSize),
0,
_config.Width - 1
);
var cy = Math.Clamp(
(int)(transform.Position.Y / CellSize),
0,
_config.Height - 1
);
_cellOccluder[cy * _config.Width + cx] = true;
}
);
return _cellOccluder;
}
}
+161
View File
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using Friflo.Engine.ECS;
using LittleSim.App;
using LittleSim.Content;
using LittleSim.Sim;
using LittleSim.UI;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Formulas;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input;
using MrGameEng.Inspector;
using MrGameEng.Lighting;
using MrGameEng.Tilemaps;
using MrGameEng.UI;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
// Часть WorldScene: HUD-хелперы, полоса скорости, горячие клавиши, переходы.
public sealed partial class WorldScene
{
// Переход к «ответственному» за событие (двойной клик в журнале): выбирает сущность и центрирует на
// ней камеру, если она ещё существует (актёр мог умереть). Камера зажимается к границам при отрисовке.
private void FocusEntity(Entity camera, int id)
{
if (
id < 0
|| !Store.TryGetEntityById(id, out var target)
|| target.IsNull
|| !target.HasComponent<Transform2D>()
)
{
return;
}
_selection.Select(id);
ref var cam = ref camera.GetComponent<Camera>();
cam.Position = target.GetComponent<Transform2D>().Position;
}
// Кнопка-переключатель оверлея HUD: подсвечивается как активная, пока оверлей открыт.
private static TextButton HudToggle(string text, Action onClick, Func<bool> isOpen)
{
var button = Ui.ChoiceButton(text);
button.Click += (_, _) =>
{
onClick();
Ui.SetChoiceActive(button, isOpen());
};
Ui.SetChoiceActive(button, isOpen());
return button;
}
private HorizontalStackPanel BuildSpeedBar(GameContent content)
{
var bar = Ui.Row(6);
bar.HorizontalAlignment = HorizontalAlignment.Center;
bar.VerticalAlignment = VerticalAlignment.Bottom;
bar.Margin = new Thickness(0, 0, 0, 12);
var hover = new SolidBrush(new Color(40, 54, 72)); // подсветка кнопок скорости при наведении
var pause = new TextButton
{
Text = content.Languages.Get("speed.pause"),
Padding = new Thickness(8, 4),
OverBackground = hover,
};
pause.Click += (_, _) => _speed.Pause();
bar.Widgets.Add(pause);
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
for (var i = 0; i < _speed.Steps.Count; i++)
{
var index = i;
var button = new TextButton
{
Text = $"x{_speed.Steps[i]:0}",
Padding = new Thickness(8, 4),
OverBackground = hover,
};
button.Click += (_, _) => _speed.SetStep(index);
bar.Widgets.Add(button);
_speedRefreshers.Add(() =>
button.TextColor =
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
);
}
void Refresh()
{
foreach (var refresh in _speedRefreshers)
{
refresh();
}
}
_speed.Changed += Refresh;
RegisterUnload(() => _speed.Changed -= Refresh);
Refresh();
return bar;
}
private void Hotkeys(InputManager input)
{
if (input.IsKeyPressed(Keys.Escape))
{
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
if (_selection.HasSelection)
{
_selection.Clear();
}
else
{
_pause.Toggle();
}
}
if (_pause.IsOpen)
{
return;
}
if (input.IsKeyPressed(Keys.Space))
{
_speed.TogglePause();
}
if (input.IsKeyPressed(Keys.D1))
{
_speed.SetStep(0);
}
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
{
_speed.SetStep(1);
}
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
{
_speed.SetStep(2);
}
}
private void Switch(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
{
_speed.Resume();
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
}
}
}
+300 -470
View File
@@ -20,6 +20,7 @@ using MrGameEng.Lighting;
using MrGameEng.Tilemaps; using MrGameEng.Tilemaps;
using MrGameEng.UI; using MrGameEng.UI;
using Myra.Graphics2D; using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
namespace LittleSim.Scenes; namespace LittleSim.Scenes;
@@ -32,7 +33,7 @@ namespace LittleSim.Scenes;
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости /// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1). /// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
/// </summary> /// </summary>
public sealed class WorldScene : Scene public sealed partial class WorldScene : Scene
{ {
public const int CellSize = 16; public const int CellSize = 16;
@@ -62,13 +63,19 @@ public sealed class WorldScene : Scene
private PauseMenu _pause = null!; private PauseMenu _pause = null!;
private Selection _selection = null!; private Selection _selection = null!;
private InspectPanel _inspect = null!; private InspectPanel _inspect = null!;
private PerfPanel? _perf; // оверлей производительности (только в дев-режиме)
private WorldEvents _events = null!; // журнал событий мира (фабрика записей истории особей)
private EventsPanel _eventsPanel = null!; // оверлей глобального журнала событий
private readonly List<Action> _speedRefreshers = []; private readonly List<Action> _speedRefreshers = [];
private PlantSet _plants = null!; private PlantSet _plants = null!;
private AnimalSet _animals = null!;
private NeedSet _needs = null!;
private SkillSet _skills = null!;
private float[] _cellFertility = []; private float[] _cellFertility = [];
private bool[] _cellLand = []; private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна) private bool[] _cellOccluderBase = []; // светонепроницаемый террейн (статично; сейчас такого нет)
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом) private bool[] _cellOccluder = []; // террейн-окклюдеры + зрелые деревья (пересобирается лайтмапом)
/// <summary>Новый мир из конфига.</summary> /// <summary>Новый мир из конфига.</summary>
public WorldScene(WorldConfig config) public WorldScene(WorldConfig config)
@@ -98,7 +105,10 @@ public sealed class WorldScene : Scene
GameLayers.EnsureRegistered(renderer); GameLayers.EnsureRegistered(renderer);
this.UseTilemaps(); this.UseTilemaps();
var calendar = Context.UseCalendar(SecondsPerDay, StartHour / 24.0); // Часы движка всегда стартуют с нуля, поэтому прошедшее время мира из сейва вносим
// смещением календаря (startDay): иначе загрузка сбрасывала время суток/сезон на день 0.
var startDay = StartHour / 24.0 + (_save?.ElapsedSeconds ?? 0.0) / SecondsPerDay;
var calendar = Context.UseCalendar(SecondsPerDay, startDay);
var climate = Context.UseClimate( var climate = Context.UseClimate(
ClimateSettings.Default with ClimateSettings.Default with
{ {
@@ -109,6 +119,11 @@ public sealed class WorldScene : Scene
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва. // Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
_plants = new PlantSet(content, atlases, device); _plants = new PlantSet(content, atlases, device);
_animals = new AnimalSet(content, atlases, device);
_needs = new NeedSet(content);
_skills = new SkillSet(content);
// Журнал событий мира — нужен и HUD-оверлею (ниже), и системам животных (создаются позже).
_events = new WorldEvents(content.Languages, _animals, calendar);
var loadingPlants = _save?.Plants is { Count: > 0 }; var loadingPlants = _save?.Plants is { Count: > 0 };
BuildTerrain( BuildTerrain(
content, content,
@@ -122,6 +137,17 @@ public sealed class WorldScene : Scene
RestorePlants(content); RestorePlants(content);
} }
// Животные: из сейва (сохранённая популяция — эволюция/состояние не теряются) либо
// детерминированный спавн из сида (новый мир или старый сейв без животных).
if (_save?.Animals is { Count: > 0 })
{
RestoreAnimals(content);
}
else
{
SpawnAnimals();
}
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира, // Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже). // а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
var lighting = this.UseLighting( var lighting = this.UseLighting(
@@ -138,7 +164,7 @@ public sealed class WorldScene : Scene
// HUD, полоса скорости и меню-пауза в одной корневой панели. // HUD, полоса скорости и меню-пауза в одной корневой панели.
var desktop = this.UseScaledUI(); var desktop = this.UseScaledUI();
var hudLabel = new Label { Left = 10, Top = 8 }; var hudLabel = new Label { TextColor = Ui.Accent }; // дата/время/сезон — заполняет HudSystem
var speedBar = BuildSpeedBar(content); var speedBar = BuildSpeedBar(content);
_pause = new PauseMenu( _pause = new PauseMenu(
Context, Context,
@@ -155,35 +181,149 @@ public sealed class WorldScene : Scene
atlases.GetRegion(device, "ui/overlays/selectionbracket"), atlases.GetRegion(device, "ui/overlays/selectionbracket"),
GameLayers.Selection GameLayers.Selection
); );
_inspect = new InspectPanel(Store, _plants, content, climate, _selection, brackets); _inspect = new InspectPanel(
desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root); Store,
_plants,
_animals,
_needs,
_skills,
content,
climate,
_selection,
brackets,
Context.Clock
);
// Режим разработчика (настройка, по умолчанию вкл): без него дев-консоль и спавнер не поднимаются.
var devMode = GameSettingsStore.Load().DeveloperMode;
var spawner = devMode
? new DevSpawner(
Store,
_animals,
_plants,
_needs,
_skills,
content,
_selection,
_cellFertility,
_config.Width,
_config.Height,
CellSize
)
: null;
// Кнопки-переключатели оверлеев (верх-право HUD). Perf-окно поднимается только в дев-режиме.
var hudButtons = new HorizontalStackPanel
{
Spacing = 6,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 8, 12, 0),
};
// Карточка статуса (верх-слева): строка даты/времени/сезона + краткие подсказки клавиш. Спавнер
// (F9) — только в дев-режиме; инспектор (F1) доступен всегда.
var hint = content.Languages.Get("hud.inspector");
if (spawner is not null)
{
hint += " · " + content.Languages.Get("hud.spawner");
}
var hudCard = new VerticalStackPanel
{
Spacing = 3,
Padding = new Thickness(12, 8),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(10, 8, 0, 0),
Background = new SolidBrush(new Color(10, 13, 18, 210)),
Border = new SolidBrush(new Color(96, 134, 168)),
BorderThickness = new Thickness(1),
};
hudCard.Widgets.Add(hudLabel);
hudCard.Widgets.Add(new Label { Text = hint, TextColor = Ui.Muted });
var hudChildren = new List<Widget> { hudCard, _inspect.Panel, speedBar, _pause.Root };
if (spawner is not null)
{
hudChildren.Add(spawner.Panel);
}
// Журнал событий — доступен всем (кнопка «События»). Двойной клик по записи ведёт камеру к
// «ответственному» и выбирает его (если он ещё жив).
_eventsPanel = new EventsPanel(
content.Languages,
_events,
Context.Clock,
id => FocusEntity(camera, id)
);
hudChildren.Add(_eventsPanel.Panel);
hudButtons.Widgets.Add(
HudToggle(
content.Languages.Get("hud.events"),
() => _eventsPanel.Toggle(),
() => _eventsPanel.IsOpen
)
);
if (devMode)
{
_perf = new PerfPanel(content.Languages, Context.Clock, UpdateSystems, DrawSystems);
hudChildren.Add(_perf.Panel);
hudButtons.Widgets.Add(
HudToggle(
content.Languages.Get("hud.perf"),
() => _perf.Toggle(),
() => _perf.IsOpen
)
);
}
hudChildren.Add(hudButtons); // кнопки добавляем последними — поверх остального HUD (кликабельны)
desktop.Root = Ui.Screen(hudChildren.ToArray());
this.UseInspector(renderer); this.UseInspector(renderer);
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases); MrGameEng.DevConsole.DevConsole? console = null;
console.Register( if (devMode)
"light", {
"light [radius] — place a point light at the cursor (night shadows demo)", console = this.UseDevConsole();
(c, args) => RegisterCommands(console, content, atlases);
{ // Тайминги систем Friflo в дев-консоль: команда `perf` (см. PerfReport). Включение мониторинга
var radius = // на все системы — в конце OnLoad, когда они уже добавлены в корни.
args.Length > 0 PerfReport.Register(console, UpdateSystems, DrawSystems);
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture) console.Register(
: 120f; "light",
var mouse = Mouse.GetState(); "light [radius] — place a point light at the cursor (night shadows demo)",
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y)); (c, args) =>
Store.CreateEntity( {
Transform2D.At(world), var radius =
new PointLight args.Length > 0
{ ? float.Parse(
Radius = radius, args[0],
Color = Color.White, System.Globalization.CultureInfo.InvariantCulture
Intensity = 0.9f, )
} : 120f;
); var mouse = Mouse.GetState();
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}"); var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
} Store.CreateEntity(
); Transform2D.At(world),
new PointLight
{
Radius = radius,
Color = Color.White,
Intensity = 0.9f,
}
);
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
}
);
}
if (spawner is not null)
{
UpdateSystems.Add(
new DevSpawnerSystem(renderer, spawner, () => desktop.IsMouseOverGUI)
);
}
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize)); UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate)); UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
@@ -203,6 +343,119 @@ public sealed class WorldScene : Scene
_config.Seed _config.Seed
) )
); );
// Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
var shoreByCell = ComputeShoreField();
var thoughts = new ThoughtSet(content);
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
UpdateSystems.Add(new AnimalMemorySystem(Context.Clock, SecondsPerDay));
UpdateSystems.Add(
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
);
UpdateSystems.Add(
new AnimalHealthSystem(
Store,
_animals,
_events,
CellSize,
Context.Clock,
SecondsPerDay,
climate,
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(),
content.Defs.TryGet<HediffDef>("Hypothermia", out var hypo) ? hypo : null,
content.Defs.TryGet<HediffDef>("Heatstroke", out var heat) ? heat : null,
content.Defs.TryGet<HediffDef>("Infection", out var infe) ? infe : null,
_config.Seed + 0x3EAD
)
);
// Настроение (A8) до выбора решения: стресс (низкое настроение) подавляет спаривание.
UpdateSystems.Add(new AnimalMoodSystem(Context.Clock, SecondsPerDay, _needs));
UpdateSystems.Add(
new AnimalDecisionSystem(
Store,
Context.Clock,
_animals,
_needs,
CellSize,
shoreByCell,
_config.Width,
_config.Height,
_events,
_config.Seed + 0x5EED
)
);
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null;
var poisoned = content.Defs.TryGet<HediffDef>("Poisoned", out var ps) ? ps : null;
UpdateSystems.Add(
new AnimalActionSystem(
Store,
_plants,
_animals,
_needs,
_skills,
thoughts,
_events,
Context.Clock,
SecondsPerDay,
CellSize,
_bounds,
bleeding,
poisoned,
_config.Seed + 0x1B17
)
);
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
UpdateSystems.Add(new DamageFlashSystem(Context.Clock)); // после внешнего вида — вспышка поверх
UpdateSystems.Add(
new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize, _events)
);
UpdateSystems.Add(new AnimalFacingSystem(_animals, CellSize));
UpdateSystems.Add(new AnimalBobSystem(Context.Clock));
UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, _events, CellSize));
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
UpdateSystems.Add(
new AnimalPregnancySystem(
Store,
_animals,
_needs,
_skills,
thoughts,
_events,
Context.Clock,
SecondsPerDay,
CellSize,
maxAnimals: 500,
_config.Seed + 0x4BED
)
);
UpdateSystems.Add(
new EggSystem(
Store,
_animals,
_needs,
_skills,
_events,
Context.Clock,
SecondsPerDay,
CellSize,
_config.Seed + 0x6E66
)
);
UpdateSystems.Add(
new SeedDispersalSystem(
Store,
_plants,
Context.Clock,
SecondsPerDay,
_config.Width,
_config.Height,
CellSize,
_cellFertility,
_cellLand
)
);
UpdateSystems.Add( UpdateSystems.Add(
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen) new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
); );
@@ -216,451 +469,28 @@ public sealed class WorldScene : Scene
() => desktop.IsMouseOverGUI () => desktop.IsMouseOverGUI
) )
); );
UpdateSystems.Add(new CallbackSystem(() => _inspect.Refresh()));
UpdateSystems.Add( UpdateSystems.Add(
new HudSystem( new CallbackSystem(() =>
Context, {
content.Languages, _inspect.Refresh();
hudLabel, _eventsPanel.Refresh();
"hud.world", _perf?.Refresh();
_config.Seed, })
calendar,
climate
)
); );
UpdateSystems.Add(new HudSystem(Context, content.Languages, hudLabel, calendar, climate));
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input))); UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
// Все системы добавлены — включаем per-system мониторинг (дев-режим), чтобы команда `perf`
// показывала данные с первого кадра. Вне дев-режима мониторинг не трогаем (нулевой оверхед).
if (devMode)
{
UpdateSystems.SetMonitorPerf(true);
DrawSystems.SetMonitorPerf(true);
}
Log.Info( Log.Info(
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}" $"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
+ (_save is null ? " (new)" : " (loaded)") + (_save is null ? " (new)" : " (loaded)")
); );
} }
/// <summary>
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
/// </summary>
private void BuildTerrain(
GameContent content,
ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
AssetManager assets,
Random? scatterRandom
)
{
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
var tiles = new TileSet();
var tileByDef = new Dictionary<TerrainDef, ushort>();
foreach (var terrain in content.Terrains.All)
{
tileByDef[terrain] = terrain.Surface is { } surface
? tiles.Add(atlases.GetRegion(device, surface))
: tiles.Add(white, terrain.Tint);
}
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
var heights = WorldGenerator.Generate(
_config.Width,
_config.Height,
_config.Seed,
_config.SmoothPasses
);
var grid = new TileGrid(_config.Width, _config.Height);
var cells = _config.Width * _config.Height;
_cellFertility = new float[cells];
_cellLand = new bool[cells];
_cellOccluderBase = new bool[cells];
_cellOccluder = new bool[cells];
for (var x = 0; x < _config.Width; x++)
{
for (var y = 0; y < _config.Height; y++)
{
var terrain = content.Terrains.Classify(heights[x, y]);
grid[x, y] = tileByDef[terrain];
var cell = y * _config.Width + x;
_cellFertility[cell] = terrain.Fertility;
_cellLand[cell] = terrain.IsLand;
_cellOccluderBase[cell] = terrain.BlocksLight;
if (scatterRandom is not null)
{
ScatterSpawner.Spawn(
this,
content,
_plants,
terrain,
new Point(x, y),
CellSize,
scatterRandom
);
}
}
}
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
}
private HorizontalStackPanel BuildSpeedBar(GameContent content)
{
var bar = Ui.Row(6);
bar.HorizontalAlignment = HorizontalAlignment.Center;
bar.VerticalAlignment = VerticalAlignment.Bottom;
bar.Margin = new Thickness(0, 0, 0, 12);
var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
pause.Click += (_, _) => _speed.Pause();
bar.Widgets.Add(pause);
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
for (var i = 0; i < _speed.Steps.Count; i++)
{
var index = i;
var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
button.Click += (_, _) => _speed.SetStep(index);
bar.Widgets.Add(button);
_speedRefreshers.Add(() =>
button.TextColor =
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
);
}
void Refresh()
{
foreach (var refresh in _speedRefreshers)
{
refresh();
}
}
_speed.Changed += Refresh;
RegisterUnload(() => _speed.Changed -= Refresh);
Refresh();
return bar;
}
private void Hotkeys(InputManager input)
{
if (input.IsKeyPressed(Keys.Escape))
{
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
if (_selection.HasSelection)
{
_selection.Clear();
}
else
{
_pause.Toggle();
}
}
if (_pause.IsOpen)
{
return;
}
if (input.IsKeyPressed(Keys.Space))
{
_speed.TogglePause();
}
if (input.IsKeyPressed(Keys.D1))
{
_speed.SetStep(0);
}
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
{
_speed.SetStep(1);
}
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
{
_speed.SetStep(2);
}
}
private string SaveWorld()
{
var save = new WorldSave
{
Name = _config.Name,
Width = _config.Width,
Height = _config.Height,
Seed = _config.Seed,
SmoothPasses = _config.SmoothPasses,
Population = _config.Population,
SavedUtc = DateTime.UtcNow,
ElapsedSeconds = Context.Clock.TotalTime,
};
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
Store
.Query<PlantGrowth, PlantOrganism, Transform2D>()
.ForEachEntity(
(
ref PlantGrowth grow,
ref PlantOrganism org,
ref Transform2D transform,
Entity _
) =>
{
save.Plants.Add(
new PlantSave
{
Species = _plants[grow.Species].Def.DefName,
X = transform.Position.X,
Y = transform.Position.Y,
AgeDays = grow.AgeDays,
Stage = grow.Stage,
CellFertility = grow.CellFertility,
Genome = org.Genome.ToDictionary(),
}
);
}
);
new SaveStore().Write(save);
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
return _config.Name;
}
private void RestorePlants(GameContent content)
{
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
foreach (var plant in _save!.Plants)
{
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
{
continue; // вид пропал (мод убрали) — пропускаем растение
}
var index = _plants.IndexOf(def);
var genome = plant.Genome is { Count: > 0 }
? new Genome(plant.Genome)
: _plants[index].Template.Generate(fallback);
PlantFactory.Create(
Store,
_plants,
index,
new Vector2(plant.X, plant.Y),
plant.AgeDays,
genome,
plant.CellFertility,
CellSize
);
}
}
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
private bool[] BuildOccluders()
{
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
Store
.Query<PlantGrowth, Transform2D>()
.ForEachEntity(
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
{
var species = _plants[grow.Species];
if (
species.Def.TrunkRadiusCells <= 0f
|| grow.Stage < species.Stages.Length - 1
)
{
return; // затеняют только зрелые деревья (со стволом)
}
var cx = Math.Clamp(
(int)(transform.Position.X / CellSize),
0,
_config.Width - 1
);
var cy = Math.Clamp(
(int)(transform.Position.Y / CellSize),
0,
_config.Height - 1
);
_cellOccluder[cy * _config.Width + cx] = true;
}
);
return _cellOccluder;
}
private void Switch(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
{
_speed.Resume();
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
}
}
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
{
ContentCommands.Register(console, content, atlases);
console.Register(
"regen",
"regen [seed] — regenerate the world with a new seed",
(c, args) =>
{
if (Context.Scenes.IsTransitioning)
{
return;
}
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
c.WriteLine($"regenerating world, seed {seed}");
Context.Scenes.Switch(
new WorldScene(_config with { Seed = seed }),
Transitions.Fade(0.6f)
);
}
);
console.Register(
"formula",
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
(c, args) =>
{
if (args.Length == 0)
{
c.WriteLine(
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
);
return;
}
var expression = string.Join(' ', args);
try
{
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
}
catch (FormulaException e)
{
c.WriteLine($"error: {e.Message}");
}
}
);
console.Register(
"gene",
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
(c, args) => RunGeneDemo(c, content, args)
);
console.Register(
"plant",
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
(c, args) => RunPlantDemo(c, content, args)
);
console.Register(
"menu",
"menu — return to the main menu",
(_, _) => Switch(new MainMenuScene())
);
}
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
{
var genes = content.Defs.All<GeneDef>();
if (genes.Count == 0)
{
console.WriteLine("no Gene defs loaded");
return;
}
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
var random = new Random(seed);
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
var parentA = Genome.Generate(genes, random);
foreach (var gene in genes)
{
var allele = parentA[gene.DefName];
console.WriteLine(
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
);
}
console.WriteLine("traits:");
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
{
console.WriteLine($" {trait} = {value:0.###}");
}
var parentB = Genome.Generate(genes, random);
var child = Genome.Breed(parentA, parentB, registry, random);
var childTraits = Phenotype.Compute(child, registry);
console.WriteLine(
$"bred child: {child.Alleles.Count} genes, "
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
);
}
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
{
var species = content.Defs.NamesOf("Plant");
if (args.Length == 0)
{
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
console.WriteLine($"species: {string.Join(", ", species)}");
return;
}
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
{
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
return;
}
var index = _plants.IndexOf(def);
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
var genome = _plants[index].Template.Generate(new Random(seed));
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
console.WriteLine(
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
);
console.WriteLine(
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
);
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
console.WriteLine(
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
);
if (def.HarvestProduct is { } harvest)
{
console.WriteLine(
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
);
}
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
{
var season = content.Languages.Get(
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
);
console.WriteLine(
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
);
}
else
{
console.WriteLine(" barren (no fruit)");
}
}
private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label)
: productDefName;
} }
+37
View File
@@ -0,0 +1,37 @@
using System.Collections.Generic;
namespace LittleSim.Sim;
/// <summary>Идентификаторы способностей (capacities) организма — на них ссылаются вклады частей тела.</summary>
public static class AnimalCapacities
{
public const string Consciousness = "Consciousness";
public const string Moving = "Moving";
public const string Breathing = "Breathing";
public const string BloodPumping = "BloodPumping";
public const string BloodFiltration = "BloodFiltration";
public const string Sight = "Sight";
public const string Hearing = "Hearing";
public const string Smell = "Smell";
public const string Touch = "Touch";
public const string Talking = "Talking";
public const string Eating = "Eating";
public const string Digestion = "Digestion";
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
public static readonly string[] DisplayOrder =
[
Consciousness,
Moving,
Sight,
Hearing,
Smell,
Touch,
Talking,
Eating,
Breathing,
BloodPumping,
BloodFiltration,
Digestion,
];
}
+324
View File
@@ -0,0 +1,324 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
using LittleSim.Scenes;
using Microsoft.Xna.Framework;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Единая точка создания сущности-животного: спрайт вида, признаки из генома формулами генов, тинт по
/// оттенку меха и компонент-организм. Переиспользуется начальным спавном, размножением и загрузкой —
/// чтобы все животные собирались одинаково. Зеркало <see cref="PlantFactory"/>.
/// </summary>
public static class AnimalFactory
{
// Якоря оттенка меха: тёплый рыжевато-песочный → холодный серо-голубой (по гену furColor).
private static readonly Color FurWarm = new(255, 226, 188);
private static readonly Color FurCool = new(206, 222, 235);
/// <summary>Создаёт животное вида <paramref name="species"/> заданного возраста, генома и поколения.</summary>
public static Entity Create(
EntityStore store,
AnimalSet animals,
NeedSet needs,
SkillSet skills,
int species,
Vector2 position,
float ageDays,
Genome genome,
int generation,
int cellSize
)
{
var sp = animals[species];
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry));
var isMale = IsMaleGenome(genome);
var (eatsPlants, eatsMeat) = Diet(traits, sp.Def); // диета фиксируется при рождении (см. EatsPlants/EatsMeat)
var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan, sp.Stages);
var region = RegionFor(sp, stage, isMale);
var sprite = new Sprite(region, GameLayers.Beings);
sprite.CenterOrigin();
sprite.Color = FurTint(traits);
var entity = store.CreateEntity(
new Transform2D(
position,
scale: new Vector2(Scale(sp, traits, stage, region, cellSize))
),
sprite,
new AnimalOrganism
{
Genome = genome,
Traits = traits,
Species = species,
IsMale = isMale,
EatsPlants = eatsPlants,
EatsMeat = eatsMeat,
Generation = generation,
},
new AnimalGrowth { Stage = stage, AgeDays = ageDays },
// Нужды — динамический массив по NeedSet (deplete полны, drive пусты); «мозг» решает сразу.
new AnimalNeeds { Values = needs.NewValues() },
new AnimalBrain
{
Action = AnimalActions.Wander,
TargetPlant = -1,
TargetMate = -1,
},
new Health { State = HealthState.Create(sp.Body, traits.BodySize) },
// Настроение (фаза A8): нейтральное на старте, активность по тиру мозга считает MoodSystem.
new Mood
{
Value = 0.5f,
Active = false,
Thoughts = [],
}
);
// Навыки: стартовые уровни + страсть (детерминированно по геному — воспроизводимо без RNG-параметра).
entity.AddComponent(
new Skills
{
Levels = skills.NewLevels(),
Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
}
);
// Память: объём ∝ размеру мозга (примитивные ~0 — ничего не помнят).
entity.AddComponent(
new Memory
{
Entries = [],
Capacity = (int)MathF.Round(MathF.Max(0f, traits.BrainSize) * MemoryPerBrain),
}
);
// Личная хроника (UI «История»): кольцевой список последних событий особи.
entity.AddComponent(new CreatureLog { Entries = [], Capacity = CreatureLogCapacity });
// Вспышка урона (презентация): заранее на нуле, чтобы удар лишь выставлял время (без структурных правок).
entity.AddComponent(new DamageFlash());
return entity;
}
/// <summary>Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это).</summary>
public const float MemoryPerBrain = 16f;
/// <summary>Глубина личной хроники особи (последние N событий о ней).</summary>
public const int CreatureLogCapacity = 24;
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
private static int SkillSeed(Genome genome)
{
var acc = 0;
foreach (var (_, allele) in genome.Alleles) // IReadOnlyDictionary без копии — без аллокации
{
acc += (int)(allele.A * 7919f) + (int)(allele.B * 104729f);
}
return acc;
}
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
private static readonly Color CorpseFreshTint = new(176, 148, 128);
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и
/// запас мяса <paramref name="meatKg"/> (∝ масса тела, считается вызывающим через <see cref="Mass"/>).</summary>
public static Entity CreateCorpse(
EntityStore store,
AnimalSet animals,
int species,
Vector2 position,
float bodySize,
float meatKg,
int cellSize
)
{
var sp = animals[species];
var region = sp.Corpse;
var sprite = new Sprite(region, GameLayers.Beings);
sprite.CenterOrigin();
sprite.Color = CorpseFreshTint;
var sizeCells = bodySize > 0f ? bodySize : sp.Def.SizeCells;
return store.CreateEntity(
new Transform2D(position, scale: new Vector2(cellSize * sizeCells / region.Width)),
sprite,
new Corpse
{
Species = species,
RotDays = 0f,
Stage = 0,
Meat = meatKg,
BodySize = bodySize,
}
);
}
// Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца.
private static readonly Color EggTint = new(238, 230, 208);
private const float EggSizeFraction = 0.45f;
/// <summary>
/// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент <see cref="Egg"/> с уже
/// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в <see cref="EggSystem"/>.
/// </summary>
public static Entity CreateEgg(
EntityStore store,
AnimalSet animals,
int species,
Vector2 position,
Genome genome,
int generation,
float incubateDays,
int cellSize
)
{
var sp = animals[species];
var region = sp.Egg;
var sprite = new Sprite(region, GameLayers.Beings);
sprite.CenterOrigin();
sprite.Color = EggTint;
return store.CreateEntity(
new Transform2D(
position,
scale: new Vector2(cellSize * EggSizeFraction / region.Width)
),
sprite,
new Egg
{
Species = species,
Genome = genome,
Generation = generation,
IncubateDays = incubateDays,
}
);
}
/// <summary>Порог выраженности признака диеты, с которого вид реально ест данный корм.</summary>
public const float DietThreshold = 0.5f;
/// <summary>
/// Рацион особи из ГЕНОВ: ест растения, если max(herbivory, omnivory) ≥ порога; ест мясо
/// (охота/падаль), если max(carnivory, omnivory) ≥ порога — всеядность (omnivory) включает оба
/// источника, специалисты задаются высоким herbivory ЛИБО carnivory. Если у вида нет генов диеты
/// (все три ≈ 0) — откат на список <see cref="AnimalDef.Diet"/> (легаси/модерский fallback).
/// </summary>
public static (bool EatsPlants, bool EatsMeat) Diet(in AnimalPhenotype traits, AnimalDef def)
{
if (traits.Herbivory <= 0f && traits.Carnivory <= 0f && traits.Omnivory <= 0f)
{
return (DietContains(def, "plant"), DietContains(def, "meat"));
}
var eatsPlants = MathF.Max(traits.Herbivory, traits.Omnivory) >= DietThreshold;
var eatsMeat = MathF.Max(traits.Carnivory, traits.Omnivory) >= DietThreshold;
return (eatsPlants, eatsMeat);
}
private static bool DietContains(AnimalDef def, string food)
{
foreach (var f in def.Diet)
{
if (string.Equals(f, food, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
/// <summary>Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти.</summary>
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
{
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
{
return;
}
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
var position = entity.GetComponent<Transform2D>().Position;
var sp = animals[org.Species];
var stageScale = entity.TryGetComponent<AnimalGrowth>(out var grow)
? sp.Stages[Math.Clamp(grow.Stage, 0, sp.Stages.Length - 1)].Scale
: 1f;
var meat = Mass.OfAnimal(sp.Def, org.Traits, stageScale) * Mass.MeatFraction;
CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, meat, cellSize);
entity.DeleteEntity();
}
/// <summary>
/// Эффективный интеллект особи (фаза A7): размер мозга (ген) × сознание (capacity из здоровья).
/// Повреждение мозга/болезнь/боль/кровопотеря снижают Consciousness → динамически роняют интеллект,
/// что гейтит набор доступных нужд/действий (см. <see cref="AnimalContext"/>). Вид без частей тела
/// (сознание не вычисляется) считается полностью сознательным — чтобы не отключить ему всё.
/// </summary>
public static float Intelligence(in AnimalPhenotype traits, HealthState? health)
{
var consciousness = health is { Parts.Length: > 0 }
? health.Capacity(AnimalCapacities.Consciousness)
: 1f;
return traits.BrainSize * consciousness;
}
/// <summary>Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).</summary>
public static bool IsMaleGenome(Genome genome) =>
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
/// <summary>Индекс стадии по возрасту: последняя стадия, чей возраст входа (EnterAt × якорь) ≤ возраст.</summary>
public static int StageAt(
float ageDays,
float maturity,
float lifespan,
AnimalStageDef[] stages
)
{
var stage = 0;
for (var i = 1; i < stages.Length; i++)
{
var anchor = string.Equals(stages[i].RelativeTo, "lifespan", StringComparison.Ordinal)
? lifespan
: maturity;
if (ageDays >= stages[i].EnterAt * anchor)
{
stage = i;
}
}
return stage;
}
/// <summary>Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка).</summary>
public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) =>
sp.LookDirs(stage, isMale).East; // восток — базовое направление; разворот делает AnimalFacingSystem
/// <summary>Взрослая ли стадия (способна размножаться) — флаг из данных стадии.</summary>
public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult;
/// <summary>Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона).</summary>
public static float Scale(
AnimalSet.Species sp,
in AnimalPhenotype traits,
int stage,
Texture2DRegion region,
int cellSize
)
{
var sizeCells =
(traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells) * sp.Stages[stage].Scale;
return cellSize * sizeCells / region.Width;
}
/// <summary>
/// Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры —
/// субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений).
/// Публичен, чтобы <see cref="AnimalAppearanceSystem"/> домножал его на яркость по нуждам.
/// </summary>
public static Color FurTint(in AnimalPhenotype traits)
{
var fur = Color.Lerp(FurWarm, FurCool, Math.Clamp(traits.FurHue, 0f, 1f));
return Color.Lerp(Color.White, fur, 0.5f);
}
}
@@ -0,0 +1,720 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Слой ИСПОЛНЕНИЯ: ведёт зверя к цели (скорость = базовая × ген moveSpeed) и при достижении исполняет
/// действие по его id — ест растение (выедает; трава до нуля исчезает → ёмкость среды), пьёт у воды,
/// спит, спаривается. Восполняемую нужду находит по действию через <see cref="NeedSet"/>. Структурные
/// изменения (гибель растений, беременность) применяются после прохода.
/// </summary>
public sealed class AnimalActionSystem : BaseSystem
{
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
private const float SeedIngestChance = 0.15f; // шанс проглотить семя при выедании зрелого растения
private const float GutSeedDropDays = 2f; // через сколько дней проглоченное семя высаживается
private const float IdleRestFactor = 0.3f; // доля скорости восстановления сил при холостом отдыхе
private const float DigForageFactor = 0.25f; // доля сытости с копания относительно нормальной кормёжки
private readonly EntityStore _store;
private readonly PlantSet _plants;
private readonly AnimalSet _animalSet;
private readonly NeedSet _needs;
private readonly SkillSet _skills;
private readonly ThoughtSet _thoughts;
private readonly WorldEvents _events;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly int _cellSize;
private readonly RectF _bounds;
private readonly HediffDef? _bleeding;
private readonly HediffDef? _poisoned;
private readonly Random _rng;
// Кэш индексов навыков-эффектов (−1, если навык не определён модом — тогда эффекта/XP нет).
private readonly int _forageSkill;
private readonly int _huntSkill;
private readonly int _evadeSkill;
private readonly ArchetypeQuery<
AnimalBrain,
AnimalNeeds,
AnimalOrganism,
Transform2D,
Health
> _query;
private readonly List<Entity> _eaten = [];
private readonly HashSet<Entity> _eatenSet = []; // O(1)-проверка членства (порядок даёт _eaten)
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
private readonly HashSet<Entity> _consumedSet = []; // O(1)-проверка членства (порядок даёт _consumed)
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
private readonly HashSet<Entity> _killsSet = []; // O(1)-проверка членства (порядок даёт _kills)
private readonly List<(Entity Self, Entity Partner)> _matings = [];
private readonly HashSet<(int, int)> _matedPairs = []; // обработанные пары за проход (дедуп (a,b)/(b,a))
private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория)
public AnimalActionSystem(
EntityStore store,
PlantSet plants,
AnimalSet animals,
NeedSet needs,
SkillSet skills,
ThoughtSet thoughts,
WorldEvents events,
GameClock clock,
float secondsPerDay,
int cellSize,
RectF bounds,
HediffDef? bleeding,
HediffDef? poisoned,
int seed
)
{
_store = store;
_plants = plants;
_animalSet = animals;
_needs = needs;
_skills = skills;
_forageSkill = skills.IndexOf(AnimalSkills.Foraging);
_huntSkill = skills.IndexOf(AnimalSkills.Hunting);
_evadeSkill = skills.IndexOf(AnimalSkills.Evasion);
_thoughts = thoughts;
_events = events;
_clock = clock;
_secondsPerDay = secondsPerDay;
_cellSize = cellSize;
_bounds = bounds;
_bleeding = bleeding;
_poisoned = poisoned;
_rng = new Random(seed);
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
}
protected override void OnUpdateGroup()
{
var seconds = _clock.DeltaTime;
if (seconds <= 0f)
{
return;
}
var days = seconds / _secondsPerDay;
_eaten.Clear();
_eatenSet.Clear();
_consumed.Clear();
_consumedSet.Clear();
_kills.Clear();
_killsSet.Clear();
_matings.Clear();
_ingested.Clear();
foreach (
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
)
{
var b = brains.Span;
var n = needsChunk.Span;
var o = organisms.Span;
var t = transforms.Span;
var hh = healths.Span;
for (var i = 0; i < b.Length; i++)
{
ref var brain = ref b[i];
var values = n[i].Values;
ref var pos = ref t[i].Position;
var def = _animalSet[o[i].Species].Def;
// Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее.
var moving = hh[i].State.Capacity(AnimalCapacities.Moving);
var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * moving;
// Питание (челюсть) масштабирует укус, пищеварение (желудок) — усвоение: их произведение
// даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
var self = entities.EntityAt(i);
self.TryGetComponent<Skills>(out var skills); // один лукап на особь (эффекты + XP)
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
if (self.TryGetComponent<Carrier>(out var carrier) && carrier.CarriedKg > 0f)
{
var sp = _animalSet[o[i].Species];
var stageScale = entities.EntityAt(i).TryGetComponent<AnimalGrowth>(out var gr)
? sp.Stages[Math.Clamp(gr.Stage, 0, sp.Stages.Length - 1)].Scale
: 1f;
var capacity = Mass.CarryCapacity(
Mass.OfAnimal(sp.Def, o[i].Traits, stageScale),
moving
);
speed *= Mass.LoadSpeedFactor(carrier.CarriedKg, capacity);
}
switch (brain.Action)
{
case AnimalActions.Sleep:
Refill(values, AnimalActions.Sleep, days);
break;
case AnimalActions.Eat:
if (MoveTo(ref pos, brain.Target, speed * seconds))
{
// Навык добычи корма: опытный форажир извлекает больше сытости; растёт от практики.
var forageF = SkillFactor(_forageSkill, skills);
Refill(values, AnimalActions.Eat, days, foodEff * forageF);
Graze(
brain.TargetPlant,
days,
def.ForageBiteDays * MathF.Max(0.2f, eating)
);
// Коэволюция: защита растения бьёт по поедателю (яд/шипы).
ApplyPlantDefense(
brain.TargetPlant,
hh[i].State,
o[i].Traits.ToxinTolerance,
days
);
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
TryIngestSeed(brain.TargetPlant, self);
GrantXp(_forageSkill, skills, days);
}
break;
case AnimalActions.Drink:
if (MoveTo(ref pos, brain.Target, speed * seconds))
{
// Утоление сильной жажды — событийная мысль настроения (A8); уровень берём
// ДО восполнения, чтобы радость была от облегчения, а не от каждого глотка.
var thirst = _needs.NeedForAction(AnimalActions.Drink);
var parched =
thirst >= 0
&& thirst < values.Length
&& values[thirst] < ThirstReliefBelow;
Refill(values, AnimalActions.Drink, days);
if (parched)
{
entities
.EntityAt(i)
.GetComponent<Mood>()
.Add(_thoughts.Get(AnimalThoughts.QuenchedThirst));
}
}
break;
case AnimalActions.Mate:
if (
brain.TargetMate >= 0
&& _store.TryGetEntityById(brain.TargetMate, out var partner)
&& !partner.IsNull
&& partner.HasComponent<Transform2D>()
)
{
var partnerPos = partner.GetComponent<Transform2D>().Position;
if (MoveTo(ref pos, partnerPos, speed * seconds))
{
_matings.Add((entities.EntityAt(i), partner));
}
}
break;
case AnimalActions.Hunt:
{
// Сила удара: размер тела особи (со стадией) относительно эталона вида — крупная/
// взрослая особь бьёт сильнее, детёныш слабее. Дальше домножается на навык охоты.
var spH = _animalSet[o[i].Species];
var stageScaleH = self.TryGetComponent<AnimalGrowth>(out var grH)
? spH.Stages[Math.Clamp(grH.Stage, 0, spH.Stages.Length - 1)].Scale
: 1f;
var strength = MathF.Max(
0.2f,
o[i].Traits.BodySize * stageScaleH / spH.RefBodySize
);
// Навык охоты: опытный хищник бьёт сильнее; растёт от практики.
Hunt(
ref brain,
ref pos,
values,
def,
speed * seconds,
seconds,
days,
foodEff,
SkillFactor(_huntSkill, skills),
strength,
self,
o[i].Species
);
GrantXp(_huntSkill, skills, days);
break;
}
case AnimalActions.Flee:
// Навык уклонения: опытная жертва убегает быстрее; растёт от практики бегства.
MoveTo(
ref pos,
brain.Target,
speed * FleeSpeedMult * SkillFactor(_evadeSkill, skills) * seconds
);
GrantXp(_evadeSkill, skills, days);
break;
case AnimalActions.Rest:
// Отдых на месте: понемногу восстанавливает силы (меньше, чем полноценный сон).
Refill(values, AnimalActions.Sleep, days, IdleRestFactor);
break;
case AnimalActions.Dig:
// Копание в земле: травоядные/всеядные выкапывают корешки/личинок — немного сытости.
if (o[i].EatsPlants)
{
Refill(values, AnimalActions.Eat, days, DigForageFactor * foodEff);
}
break;
case AnimalActions.Wallow:
// Валяние в грязи: стоит на месте у воды (комфорт/терморегуляция) — без эффекта на нужды.
break;
default: // Wander — блуждание к цели
MoveTo(ref pos, brain.Target, speed * seconds);
break;
}
pos.X = Math.Clamp(pos.X, _bounds.Left, _bounds.Right);
pos.Y = Math.Clamp(pos.Y, _bounds.Top, _bounds.Bottom);
}
}
foreach (var plant in _eaten)
{
plant.DeleteEntity();
}
foreach (var corpse in _consumed)
{
if (!corpse.IsNull)
{
corpse.DeleteEntity(); // труп доеден до нуля
}
}
foreach (var prey in _kills)
{
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
}
foreach (var (eater, seed) in _ingested)
{
if (!eater.IsNull && !eater.HasComponent<GutSeed>())
{
eater.AddComponent(seed); // структурное добавление — после прохода
}
}
ApplyMatings();
}
// Эндозоохория: при выедании ЗРЕЛОГО растения шанс проглотить его семя — зверь понесёт его и
// высадит позже в другом месте (SeedDispersalSystem). Геном клонируется (отдельная особь-росток).
private void TryIngestSeed(int plantId, Entity eater)
{
if (
eater.IsNull
|| eater.HasComponent<GutSeed>()
|| plantId < 0
|| _rng.NextSingle() >= SeedIngestChance
|| !_store.TryGetEntityById(plantId, out var plant)
|| plant.IsNull
|| !plant.HasComponent<PlantGrowth>()
|| !plant.HasComponent<PlantOrganism>()
)
{
return;
}
ref readonly var grow = ref plant.GetComponent<PlantGrowth>();
if (grow.Stage < _plants[grow.Species].Stages.Length - 1)
{
return; // только зрелое растение даёт семя
}
var genome = new Genome(plant.GetComponent<PlantOrganism>().Genome.ToDictionary());
_ingested.Add(
(
eater,
new GutSeed
{
PlantSpecies = grow.Species,
Genome = genome,
DropInDays = GutSeedDropDays,
}
)
);
}
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
// Минимальная видимая длительность красной вспышки урона у жертвы (реального времени).
private const float DamageFlashSeconds = 0.35f;
private void Hunt(
ref AnimalBrain brain,
ref Vector2 pos,
float[] values,
AnimalDef def,
float step,
float seconds,
float days,
float foodEff,
float huntFactor,
float strength,
Entity hunter,
int hunterSpecies
)
{
if (
brain.TargetPrey < 0
|| !_store.TryGetEntityById(brain.TargetPrey, out var target)
|| target.IsNull
|| !target.HasComponent<Transform2D>()
)
{
return; // цель исчезла — действие пересмотрят на следующем решении
}
var targetPos = target.GetComponent<Transform2D>().Position;
var range = def.AttackRangeCells * _cellSize;
var inRange = Vector2.DistanceSquared(pos, targetPos) <= range * range;
if (brain.TargetIsCorpse)
{
if (!target.HasComponent<Corpse>())
{
return;
}
if (!inRange)
{
MoveTo(ref pos, targetPos, step);
return;
}
ref var corpse = ref target.GetComponent<Corpse>();
Refill(values, AnimalActions.Eat, days, foodEff);
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
if (corpse.Meat <= 0f && _consumedSet.Add(target))
{
_consumed.Add(target);
}
return;
}
// Живая добыча.
if (!target.HasComponent<Health>() || !target.HasComponent<AnimalOrganism>())
{
return;
}
// Дискретные удары (как в RimWorld): кулдаун идёт и при сближении — первый контакт бьёт сразу.
brain.AttackCooldown -= seconds;
if (!inRange)
{
MoveTo(ref pos, targetPos, step);
return;
}
if (brain.AttackCooldown > 0f)
{
return; // в контакте, но удар ещё на перезарядке
}
brain.AttackCooldown = MathF.Max(0.05f, def.AttackIntervalSec);
var preyHealth = target.GetComponent<Health>().State;
if (preyHealth is null)
{
return;
}
// Сила удара = база дефа × сила особи (размер×стадия) × навык охоты; кровопотеря/кровотечение — ×силу.
var damage = def.AttackDamage * strength * huntFactor;
preyHealth.ApplyInjury(
damage,
def.AttackBloodLoss * strength,
_bleeding,
def.AttackBleed * strength,
_rng
);
// Красная вспышка жертвы (презентация) + удар в боевую хронику обоих участников.
if (target.HasComponent<DamageFlash>())
{
target.GetComponent<DamageFlash>().Seconds = DamageFlashSeconds;
}
_events.Struck(
hunter,
hunterSpecies,
target,
target.GetComponent<AnimalOrganism>().Species,
damage
);
if (
preyHealth.BloodLevel <= 0f
|| preyHealth.Capacity(AnimalCapacities.Consciousness) <= 0.01f
)
{
if (_killsSet.Add(target))
{
_kills.Add(target); // добита — смерть после прохода (структурное изменение)
_events.Killed(
hunter,
hunterSpecies,
target.GetComponent<AnimalOrganism>().Species
);
}
}
}
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay × эффективность
// (питание/пищеварение для еды — слабая челюсть/больной желудок дают меньше сытости с того же корма).
private void Refill(float[] values, string action, float days, float efficiency = 1f)
{
var index = _needs.NeedForAction(action);
if (index < 0 || values is null || index >= values.Length)
{
return;
}
values[index] = Math.Clamp(
values[index] + _needs[index].FeedPerDay * days * efficiency,
0f,
1f
);
}
// Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта).
private static float SkillFactor(int idx, in Skills s)
{
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
{
return 1f;
}
return 0.7f + 0.6f * s.Levels[idx];
}
// Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача).
private void GrantXp(int idx, in Skills s, float days)
{
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
{
return;
}
var passion = s.Passions is not null && idx < s.Passions.Length ? s.Passions[idx] : (byte)0;
var gain =
_skills[idx].LearnRate
* SkillSet.PassionMultiplier(passion)
* days
* (1f - s.Levels[idx]);
s.Levels[idx] = Math.Clamp(s.Levels[idx] + gain, 0f, 1f);
}
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
private static bool MoveTo(ref Vector2 pos, Vector2 target, float step)
{
var delta = target - pos;
var dist = delta.Length();
if (dist <= step || dist < 0.001f)
{
pos = dist < 0.001f ? pos : target;
return true;
}
pos += delta / dist * step;
return false;
}
// Выедание: убавляет рост растения; траву (без ствола), выеденную до нуля, помечает на гибель.
private void Graze(int plantId, float days, float bitePerDay)
{
if (
plantId < 0
|| !_store.TryGetEntityById(plantId, out var plant)
|| plant.IsNull
|| !plant.HasComponent<PlantGrowth>()
)
{
return;
}
ref var grow = ref plant.GetComponent<PlantGrowth>();
grow.AgeDays = MathF.Max(0f, grow.AgeDays - bitePerDay * days);
var isGrass = _plants[grow.Species].Def.TrunkRadiusCells <= 0f;
if (isGrass && grow.AgeDays <= GrazeKillAge && _eatenSet.Add(plant))
{
_eaten.Add(plant);
}
}
// Доза яда за день укуса при токсичности 1 и нулевой устойчивости (масштабируется тяжести Poisoned).
private const float PoisonDosePerDay = 1.6f;
// Порог шипов, выше которого укус о растение травмирует, и масштабы урона/кровопотери от шипов.
private const float ThornThreshold = 0.2f;
private const float ThornPartDamage = 4f;
private const float ThornBloodLoss = 0.02f;
// Коэволюция: при поедании растение «отвечает» — токсичность отравляет (тем сильнее, чем ниже
// устойчивость зверя к яду), шипы наносят лёгкую травму. Доза яда копится в хедифе Poisoned; если
// зверь ест яд быстрее, чем выводит, тяжесть дойдёт до летальной. Детерминированно (общий _rng).
private void ApplyPlantDefense(
int plantId,
HealthState? health,
float toxinTolerance,
float days
)
{
if (
health is null
|| plantId < 0
|| !_store.TryGetEntityById(plantId, out var plant)
|| plant.IsNull
|| !plant.HasComponent<PlantOrganism>()
)
{
return;
}
ref readonly var traits = ref plant.GetComponent<PlantOrganism>().Traits;
var poisoned = false;
if (_poisoned is not null && traits.Toxicity > 0f)
{
var dose = traits.Toxicity * (1f - Math.Clamp(toxinTolerance, 0f, 1f));
if (dose > 0f)
{
health.Intensify(_poisoned, dose * PoisonDosePerDay * days);
poisoned = true;
}
}
if (traits.Thorns > ThornThreshold)
{
// ApplyInjury сам пересчитывает способности (учтёт и свежий яд).
health.ApplyInjury(
traits.Thorns * ThornPartDamage * days,
traits.Thorns * ThornBloodLoss * days,
_bleeding,
traits.Thorns * 0.15f * days,
_rng
);
}
else if (poisoned)
{
health.RecomputeCapacities(); // отравление меняет capacity-моды — применить сразу
}
}
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление —
// после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
private void ApplyMatings()
{
var mateNeed = _needs.NeedForAction(AnimalActions.Mate);
_matedPairs.Clear();
foreach (var (a, b) in _matings)
{
if (
a.IsNull
|| b.IsNull
|| !a.HasComponent<AnimalOrganism>()
|| !b.HasComponent<AnimalOrganism>()
)
{
continue;
}
// Оба партнёра независимо ставят себя в очередь, так что пара приходит дважды — (a,b) и (b,a).
// Дедупим по неупорядоченному ключу: иначе ResetMating/мысль «спаривание» применяются повторно.
var pairKey = a.Id < b.Id ? (a.Id, b.Id) : (b.Id, a.Id);
if (!_matedPairs.Add(pairKey))
{
continue;
}
var aMale = a.GetComponent<AnimalOrganism>().IsMale;
var bMale = b.GetComponent<AnimalOrganism>().IsMale;
if (aMale == bMale)
{
continue; // один пол — не пара (страховка)
}
var mother = aMale ? b : a;
var father = aMale ? a : b;
if (!mother.HasComponent<Pregnant>())
{
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
// Плодовитость падает со старостью (тот же фактор сенесценса): дряхлая самка приносит
// меньший помёт, а после порога пострепродуктивного возраста уже не зачинает.
var momAge = mother.HasComponent<AnimalGrowth>()
? mother.GetComponent<AnimalGrowth>().AgeDays
: 0f;
var ageFrac = mom.Traits.Lifespan > 0f ? momAge / mom.Traits.Lifespan : 0f;
if (ageFrac <= PostReproductiveFrac)
{
var fert = AnimalHealthSystem.SenescenceFactor(momAge, mom.Traits.Lifespan);
mother.AddComponent(
new Pregnant
{
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize * fert)),
}
);
_events.Pregnant(mother, mom.Species);
}
}
ResetMating(a, mateNeed);
ResetMating(b, mateNeed);
// Спаривание — положительная событийная мысль обоим партнёрам (A8).
var mated = _thoughts.Get(AnimalThoughts.Mated);
a.GetComponent<Mood>().Add(mated);
b.GetComponent<Mood>().Add(mated);
_events.Mated(
a,
a.GetComponent<AnimalOrganism>().Species,
b,
b.GetComponent<AnimalOrganism>().Species
);
}
}
private static void ResetMating(Entity entity, int mateNeed)
{
if (mateNeed < 0 || !entity.HasComponent<AnimalNeeds>())
{
return;
}
var values = entity.GetComponent<AnimalNeeds>().Values;
if (values is not null && mateNeed < values.Length)
{
values[mateNeed] = 0f;
}
}
}
@@ -0,0 +1,43 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Id поведений-исполнителей (реестр действий ИИ). Дефы нужд (<see cref="NeedDef.Action"/>) ссылаются на
/// них; <see cref="AnimalDecisionSystem"/> выбирает действие, <see cref="AnimalActionSystem"/> исполняет.
/// Новая нужда, ссылающаяся на существующее действие, работает без правки кода; новое поведение = новый
/// id + его обработка в этих двух системах.
/// </summary>
public static class AnimalActions
{
public const string Wander = "Wander";
public const string Eat = "Eat";
public const string Drink = "Drink";
public const string Sleep = "Sleep";
public const string Mate = "Mate";
/// <summary>Охота: преследовать живую добычу и кусать (урон) либо есть труп — утоляет голод хищника.</summary>
public const string Hunt = "Hunt";
/// <summary>Бегство: уходить от ближайшего хищника (реакция жертвы, важнее прочих нужд).</summary>
public const string Flee = "Flee";
// — Холостые (idle) активности: чем зверь занят, когда нет срочных нужд. Выбираются внутри ветки
// Wander с фиксированной длительностью (см. AnimalDecisionSystem) — придают поведению разнообразие.
/// <summary>Отдых: стоит/лежит на месте, понемногу восстанавливая силы.</summary>
public const string Rest = "Rest";
/// <summary>Копание в земле в поисках корня/личинок (травоядные/всеядные) — даёт чуть сытости.</summary>
public const string Dig = "Dig";
/// <summary>Валяние в грязи у воды (комфорт/терморегуляция) — стоит на месте у кромки.</summary>
public const string Wallow = "Wallow";
}
@@ -0,0 +1,49 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
/// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
/// <see cref="Sprite"/> — граница sim/presentation цела.
/// </summary>
public sealed class AnimalAppearanceSystem(NeedSet needs)
: QuerySystem<AnimalNeeds, AnimalOrganism, Sprite>
{
private const float MinBrightness = 0.5f;
protected override void OnUpdate()
{
foreach (var (needsChunk, organisms, sprites, _) in Query.Chunks)
{
var n = needsChunk.Span;
var o = organisms.Span;
var s = sprites.Span;
for (var i = 0; i < n.Length; i++)
{
var values = n[i].Values;
var worst = 1f;
if (values is not null)
{
for (var k = 0; k < needs.Count && k < values.Length; k++)
{
if (needs.IsDeplete(k))
{
worst = MathF.Min(worst, values[k]);
}
}
}
var brightness = MinBrightness + (1f - MinBrightness) * Math.Clamp(worst, 0f, 1f);
s[i].Color = AnimalFactory.FurTint(o[i].Traits) * brightness;
}
}
}
}
@@ -0,0 +1,56 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// «Раскачка» при ходьбе (презентация): идущий зверь покачивается из стороны в сторону (наклон спрайта
/// через <see cref="Transform2D.Rotation"/>), стоящий — плавно выпрямляется. Делает движение живее, не
/// затрагивая симуляцию (она вращение не использует). Фаза покачивания идёт по игровому времени (стоп на
/// паузе, быстрее на ускорении) и сдвинута по id особи — звери качаются вразнобой. Граница sim/presentation
/// цела: система пишет только поворот трансформа.
/// </summary>
public sealed class AnimalBobSystem(GameClock clock) : QuerySystem<AnimalBrain, Transform2D>
{
private const float SwaySpeed = 9f; // скорость набега фазы (рад/сек игрового времени)
private const float SwayAmplitude = 0.08f; // амплитуда наклона (рад ≈ 4.6°)
private const float MinMoveSq = 0.5f * 0.5f; // ближе к цели — считаем стоящим
private const float UprightDecay = 0.85f; // как быстро выпрямляется стоя (за кадр)
private float _phase;
protected override void OnUpdate()
{
var dt = clock.DeltaTime;
if (dt <= 0f)
{
return; // пауза — не качаем и не выпрямляем
}
_phase += dt * SwaySpeed;
foreach (var (brains, transforms, entities) in Query.Chunks)
{
var b = brains.Span;
var t = transforms.Span;
for (var i = 0; i < b.Length; i++)
{
ref var rot = ref t[i].Rotation;
if ((b[i].Target - t[i].Position).LengthSquared() > MinMoveSq)
{
var offset = entities.EntityAt(i).Id * 0.7f; // десинхрон между особями
rot = MathF.Sin(_phase + offset) * SwayAmplitude;
}
else
{
rot *= UprightDecay; // у цели/стоя — плавно к вертикали
}
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// «Мозг» зверя: выбранное действие (id поведения), его цель и таймер до пересмотра. Решение принимает
/// <see cref="AnimalDecisionSystem"/> (движковый <see cref="UtilityAi{TContext}"/>), исполняет
/// <see cref="AnimalActionSystem"/>.
/// </summary>
public struct AnimalBrain : IComponent
{
/// <summary>Id текущего действия (см. <see cref="AnimalActions"/>).</summary>
public string Action;
/// <summary>Куда идти (позиция растения/воды/партнёра/точки блуждания).</summary>
public Vector2 Target;
/// <summary>Id растения-цели для <see cref="AnimalActions.Eat"/>; -1 — нет.</summary>
public int TargetPlant;
/// <summary>Id партнёра для <see cref="AnimalActions.Mate"/>; -1 — нет.</summary>
public int TargetMate;
/// <summary>Id цели для <see cref="AnimalActions.Hunt"/> — живой добычи или трупа; -1 — нет.</summary>
public int TargetPrey;
/// <summary>Цель — труп (падаль), а не живая добыча: при достижении едим, а не атакуем.</summary>
public bool TargetIsCorpse;
/// <summary>Секунды до следующего пересмотра решения.</summary>
public float DecideIn;
/// <summary>Игровые секунды до следующего удара по добыче (кулдаун дискретных атак, как в RimWorld).</summary>
public float AttackCooldown;
/// <summary>Текущее направление блуждания (коррелированное — плавно дрейфует, не дёргается каждый раз).</summary>
public Vector2 Heading;
/// <summary>Игровое время (TotalTime), до которого длится текущая холостая активность (отдых/копание/…).</summary>
public float ActivityUntil;
}
@@ -0,0 +1,59 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Снимок нужд зверя для соображений utility-выбора (доступ по индексу нужды) с гейтом интеллектом
/// (фаза A7): нужда, чей <see cref="NeedDef.MinBrain"/> выше эффективного интеллекта особи
/// (brainSize × Consciousness), в выбор не входит.
/// </summary>
public readonly struct AnimalContext(
float[] values,
NeedSet needs,
float intelligence,
int overrideIndex = -1,
float overrideValue = 0f
)
{
private readonly float[] _values = values;
private readonly NeedSet _needs = needs;
private readonly float _intelligence = intelligence;
// Переопределение значения одной нужды (стиль питания: «эффективный голод» с гистерезисом —
// см. AnimalDecisionSystem). Override имеет приоритет над сырым значением и над гейтом мозга.
private readonly int _overrideIndex = overrideIndex;
private readonly float _overrideValue = overrideValue;
/// <summary>
/// Значение нужды по индексу. Если для этой нужды задан override (стиль питания) — он и возвращается.
/// Если её порог мозга выше интеллекта особи — нужда недоступна и возвращается её «не-срочный» край
/// (deplete → 1 «сыта», drive → 0 «без влечения»), чтобы соображение дало ~0 и действие не выбралось.
/// Индекс вне диапазона → 0.
/// </summary>
public float Get(int index)
{
if (index == _overrideIndex)
{
return _overrideValue;
}
if (index >= _values.Length)
{
return 0f;
}
if (_needs[index].MinBrain > _intelligence)
{
return _needs.IsDrive(index) ? 0f : 1f;
}
return _values[index];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Разворот спрайта по направлению движения (как в RimWorld): зверь смотрит туда, куда идёт. Восток —
/// базовая текстура, запад — её зеркало (флип по X), север/юг — отдельные текстуры (если есть у вида).
/// Направление берётся из цели «мозга» (<see cref="AnimalBrain.Target"/>) относительно позиции; стоящий
/// зверь сохраняет прежний разворот. При смене региона пересчитывает центр-origin и масштаб (текстуры
/// направлений могут отличаться шириной). Презентация (читает sim, пишет только Sprite/Transform-scale).
/// </summary>
public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize)
: QuerySystem<AnimalBrain, AnimalOrganism, AnimalGrowth, Transform2D, Sprite>
{
private const float MinMove = 0.5f; // короче — считаем стоящим, разворот не трогаем
protected override void OnUpdate()
{
foreach (var (brains, organisms, growths, transforms, sprites, _) in Query.Chunks)
{
var b = brains.Span;
var o = organisms.Span;
var g = growths.Span;
var t = transforms.Span;
var s = sprites.Span;
for (var i = 0; i < b.Length; i++)
{
var dir = b[i].Target - t[i].Position;
if (dir.LengthSquared() < MinMove * MinMove)
{
continue; // стоит/у цели — сохраняем текущий разворот
}
var sp = animals[o[i].Species];
var dirs = sp.LookDirs(g[i].Stage, o[i].IsMale);
Texture2DRegion region;
SpriteFlip flip;
if (MathF.Abs(dir.X) >= MathF.Abs(dir.Y))
{
region = dirs.East;
flip = dir.X < 0f ? SpriteFlip.X : SpriteFlip.None; // запад = зеркало востока
}
else
{
region = dir.Y < 0f ? dirs.North : dirs.South; // экранный Y вниз: вверх = север
flip = SpriteFlip.None;
}
ref var sprite = ref s[i];
if (sprite.Region == region && sprite.Flip == flip)
{
continue; // разворот не изменился
}
sprite.Region = region;
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
sprite.Flip = flip;
t[i].Scale = new Vector2(
AnimalFactory.Scale(sp, o[i].Traits, g[i].Stage, region, cellSize)
);
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using Friflo.Engine.ECS;
namespace LittleSim.Sim;
/// <summary>
/// Состояние роста животного (фаза A3): текущая стадия и накопленный возраст в игровых днях. Плоские
/// данные — переход между стадиями (Baby→Juvenile→Adult→Senior), смену спрайта и размера выполняет
/// <see cref="AnimalGrowthSystem"/>, читая пороги из генов (возраст созревания, продолжительность жизни).
/// Зеркало <see cref="PlantGrowth"/>.
/// </summary>
public struct AnimalGrowth : IComponent
{
/// <summary>Текущая стадия: 0 Baby, 1 Juvenile, 2 Adult, 3 Senior.</summary>
public int Stage;
/// <summary>Накопленный возраст в игровых днях.</summary>
public float AgeDays;
}
@@ -0,0 +1,67 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.
/// </summary>
public sealed class AnimalGrowthSystem(
GameClock clock,
float secondsPerDay,
AnimalSet animals,
int cellSize,
WorldEvents events
) : QuerySystem<AnimalGrowth, AnimalOrganism, Transform2D, Sprite>
{
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
foreach (var (growths, organisms, transforms, sprites, entities) in Query.Chunks)
{
var g = growths.Span;
var o = organisms.Span;
var t = transforms.Span;
var s = sprites.Span;
for (var i = 0; i < g.Length; i++)
{
ref var grow = ref g[i];
grow.AgeDays += days;
ref readonly var org = ref o[i];
var sp = animals[org.Species];
var stage = AnimalFactory.StageAt(
grow.AgeDays,
org.Traits.MaturityAge,
org.Traits.Lifespan,
sp.Stages
);
if (stage == grow.Stage)
{
continue;
}
grow.Stage = stage;
var region = AnimalFactory.RegionFor(sp, stage, org.IsMale);
ref var sprite = ref s[i];
sprite.Region = region;
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
t[i].Scale = new Vector2(
AnimalFactory.Scale(sp, org.Traits, stage, region, cellSize)
);
events.Grew(entities.EntityAt(i), org.Species, sp.Stages[stage].Name); // в хронику особи
}
}
}
}
@@ -0,0 +1,219 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Здоровье и болезни (фаза A6): медленный тик (~игровой час). Фоновое заражение, прогресс болезней
/// (гонка тяжесть↔иммунитет — выздоровление или смерть), пересчёт способностей с учётом хедифов. Смерть
/// по здоровью (летальная болезнь / кровь на нуле / потеря сознания) оставляет труп. Первый реальный
/// источник урона — оживляет каркас A5 (больные звери теряют Moving → медленнее).
/// </summary>
public sealed class AnimalHealthSystem : BaseSystem
{
private const float TickDays = 1f / 24f; // ~игровой час
private const float BloodRecoveryPerDay = 0.25f; // восстановление крови в покое (нет кровотечений)
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly WorldEvents _events;
private readonly int _cellSize;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly Climate _climate;
private readonly HediffDef[] _ambient;
private readonly HediffDef? _hypothermia;
private readonly HediffDef? _heatstroke;
private readonly HediffDef? _infection;
private readonly Random _rng;
private readonly ArchetypeQuery<Health, AnimalOrganism, AnimalGrowth> _query;
private readonly List<Entity> _deaths = [];
private float _accum;
// Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает
// максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее.
private const float SenescenceStart = 0.6f;
private const float SenescenceMax = 0.45f;
// Термокомфорт (°C) при теплоизоляции 0.5 и размере тела 1; границы сдвигаются мехом и размером тела.
private const float ColdComfort = 2f; // ниже — мёрзнет
private const float HeatComfort = 30f; // выше — перегревается
private const float ColdInsulationBonus = 30f; // мех (insulation 0..1) расширяет холодоустойчивость
private const float HeatInsulationPenalty = 14f; // тот же мех ухудшает жароустойчивость
private const float ColdBodyBonus = 8f; // крупное тело (тепловая инерция) держит холод лучше
private const float ThermalRatePerDegree = 0.06f; // прирост тяжести за °C за пределом комфорта в день
// Заражение раны: шанс в день при кровотечении тяжести 1 подхватить инфекцию.
private const float InfectionChancePerDay = 0.25f;
public AnimalHealthSystem(
EntityStore store,
AnimalSet animals,
WorldEvents events,
int cellSize,
GameClock clock,
float secondsPerDay,
Climate climate,
HediffDef[] ambient,
HediffDef? hypothermia,
HediffDef? heatstroke,
HediffDef? infection,
int seed
)
{
_store = store;
_animals = animals;
_events = events;
_cellSize = cellSize;
_clock = clock;
_secondsPerDay = secondsPerDay;
_climate = climate;
_ambient = ambient;
_hypothermia = hypothermia;
_heatstroke = heatstroke;
_infection = infection;
_rng = new Random(seed);
_query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
}
/// <summary>Множитель способностей от старения (1 — молод/расцвет, &lt;1 — дряхлеет к концу жизни).</summary>
public static float SenescenceFactor(float ageDays, float lifespan)
{
if (lifespan <= 0f)
{
return 1f;
}
var frac = (ageDays / lifespan - SenescenceStart) / MathF.Max(0.01f, 1f - SenescenceStart);
return 1f - SenescenceMax * Math.Clamp(frac, 0f, 1f);
}
protected override void OnUpdateGroup()
{
_accum += _clock.DeltaTime / _secondsPerDay;
if (_accum < TickDays)
{
return;
}
var days = _accum;
_accum = 0f;
_deaths.Clear();
foreach (var (healths, organisms, growths, entities) in _query.Chunks)
{
var h = healths.Span;
var o = organisms.Span;
var g = growths.Span;
for (var i = 0; i < h.Length; i++)
{
var state = h[i].State;
if (state is null)
{
continue;
}
foreach (var disease in _ambient)
{
if (
!state.Has(disease.DefName)
&& _rng.NextSingle() < disease.AmbientPerDay * days
)
{
state.Add(disease);
}
}
// Терморегуляция: вне комфорта (зависит от GeneInsulation + размера тела) копится тяжесть
// гипотермии/теплового удара; в комфорте immunityPerDay их рассасывает. Мех расширяет
// холодоустойчивость, но ухудшает жароустойчивость; крупное тело держит холод.
var insul = Math.Clamp(o[i].Traits.Insulation, 0f, 1f);
var coldLimit =
ColdComfort
- ColdInsulationBonus * insul
- ColdBodyBonus * MathF.Max(0f, o[i].Traits.BodySize - 1f);
var heatLimit = HeatComfort - HeatInsulationPenalty * insul;
var temp = _climate.Temperature;
if (temp < coldLimit && _hypothermia is not null)
{
state.Intensify(_hypothermia, (coldLimit - temp) * ThermalRatePerDegree * days);
}
else if (temp > heatLimit && _heatstroke is not null)
{
state.Intensify(_heatstroke, (temp - heatLimit) * ThermalRatePerDegree * days);
}
// Заражение раны: пока есть кровотечение, шанс (∝ его тяжести) подхватить инфекцию.
if (_infection is not null && !state.Has(_infection.DefName))
{
var bleedSeverity = 0f;
foreach (var hd in state.Hediffs)
{
if (hd.Def.BloodLossPerDay > 0f && hd.Severity > bleedSeverity)
{
bleedSeverity = hd.Severity;
}
}
if (
bleedSeverity > 0f
&& _rng.NextSingle() < InfectionChancePerDay * bleedSeverity * days
)
{
state.Add(_infection);
}
}
// Фильтрация крови (почки/печень) ускоряет вывод токсинов и набор иммунитета: повреждённый
// орган → медленнее выздоровление от яда/болезни/инфекции (для тела без неё Capacity вернёт 1).
var lethal = state.AdvanceHediffs(
days,
state.Capacity(AnimalCapacities.BloodFiltration)
);
// Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
var bleed = 0f;
foreach (var hd in state.Hediffs)
{
bleed += hd.Def.BloodLossPerDay * hd.Severity;
}
if (bleed > 0f)
{
state.BloodLevel = Math.Clamp(state.BloodLevel - bleed * days, 0f, 1f);
}
else if (state.BloodLevel < 1f)
{
state.BloodLevel = Math.Clamp(
state.BloodLevel + BloodRecoveryPerDay * days,
0f,
1f
);
}
// Старение: к концу жизни способности дряхлеют (медленнее/слабее, уязвимее к ранам/болезни).
var senescence = SenescenceFactor(g[i].AgeDays, o[i].Traits.Lifespan);
state.RecomputeCapacities(senescence);
if (
lethal
|| state.BloodLevel <= 0f
|| state.Capacity(AnimalCapacities.Consciousness) <= 0.01f
)
{
_deaths.Add(entities.EntityAt(i));
_events.Died(o[i].Species, "event.death.disease");
}
}
}
foreach (var dead in _deaths)
{
AnimalFactory.Die(_store, _animals, dead, _cellSize);
}
}
}
@@ -0,0 +1,57 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие
/// наблюдения записывает/освежает <see cref="AnimalDecisionSystem"/>; эта система только «выветривает»
/// память, давая ей конечный срок.
/// </summary>
public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem<Memory>
{
private const float ForgetPerDay = 0.5f; // уверенность падает (память живёт ~2 дня без подтверждения)
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
foreach (var (memories, _) in Query.Chunks)
{
var m = memories.Span;
for (var i = 0; i < m.Length; i++)
{
var entries = m[i].Entries;
if (entries is null)
{
continue;
}
for (var k = entries.Count - 1; k >= 0; k--)
{
var e = entries[k];
e.Confidence -= ForgetPerDay * days;
e.AgeDays += days;
if (e.Confidence <= 0f)
{
entries.RemoveAt(k);
}
else
{
entries[k] = e;
}
}
}
}
}
}
@@ -0,0 +1,107 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Настроение (фаза A8): для особей тира 4+ (мозг ≥ порога) считает настроение из непрерывного фона
/// (удовлетворённость убывающих нужд минус боль из здоровья) и суммы затухающих событийных мыслей.
/// Мысли угасают со временем. У примитивных (тир &lt; 4) настроения нет (Active=false, мысли сбрасываются).
/// Гейт интеллектом — тем же порогом тира 4, что и высшие нужды (сон/секс). Низкое настроение влияет на
/// поведение (стресс не даёт спариваться — см. <see cref="AnimalDecisionSystem"/>) и видно в консоли/инспекторе.
/// </summary>
public sealed class AnimalMoodSystem(GameClock clock, float secondsPerDay, NeedSet needs)
: QuerySystem<Mood, AnimalNeeds, AnimalOrganism, Health>
{
/// <summary>Порог размера мозга (тир 4), с которого у особи появляется настроение.</summary>
public const float MoodMinBrain = 0.40f;
private const float NeutralBase = 0.35f; // настроение при полном истощении нужд (без боли/мыслей)
private const float NeedsWeight = 0.55f; // вклад средней удовлетворённости убывающих нужд
private const float PainWeight = 0.4f; // боль из здоровья тянет настроение вниз
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
foreach (var (moods, needsChunk, organisms, healths, _) in Query.Chunks)
{
var m = moods.Span;
var n = needsChunk.Span;
var o = organisms.Span;
var h = healths.Span;
for (var i = 0; i < m.Length; i++)
{
ref var mood = ref m[i];
if (o[i].Traits.BrainSize < MoodMinBrain)
{
mood.Active = false; // тир ниже зверя — настроения нет
mood.Value = 0.5f;
mood.Thoughts?.Clear();
continue;
}
mood.Active = true;
// 1) Затухание событийных мыслей + сумма их сдвигов.
var thoughtSum = 0f;
var list = mood.Thoughts;
if (list is not null)
{
for (var k = list.Count - 1; k >= 0; k--)
{
var t = list[k];
t.RemainingDays -= days;
if (t.RemainingDays <= 0f)
{
list.RemoveAt(k);
continue;
}
list[k] = t;
thoughtSum += t.Def.MoodOffset;
}
}
// 2) Непрерывный фон: средняя удовлетворённость убывающих нужд минус боль из здоровья.
var wellbeing = AvgDeplete(n[i].Values);
var pain = h[i].State?.Pain ?? 0f;
mood.Value = Math.Clamp(
NeutralBase + NeedsWeight * wellbeing - PainWeight * pain + thoughtSum,
0f,
1f
);
}
}
}
// Средняя удовлетворённость убывающих нужд (голод/жажда/усталость) [0,1]; нет нужд → 1 (нейтрально).
private float AvgDeplete(float[] values)
{
if (values is null)
{
return 1f;
}
var sum = 0f;
var count = 0;
for (var k = 0; k < needs.Count && k < values.Length; k++)
{
if (needs.IsDeplete(k))
{
sum += values[k];
count++;
}
}
return count > 0 ? sum / count : 1f;
}
}
@@ -0,0 +1,67 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Смертность (фаза A3/A5): гибель от старости (возраст превысил продолжительность жизни) или истощения
/// (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды. Фаза A5: вместо деспавна
/// оставляет <see cref="Corpse"/> (через <see cref="AnimalFactory.CreateCorpse"/>), затем удаляет особь.
/// </summary>
public sealed class AnimalMortalitySystem : BaseSystem
{
private const float LethalStarveDays = 3f;
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly WorldEvents _events;
private readonly int _cellSize;
private readonly ArchetypeQuery<AnimalGrowth, AnimalOrganism, AnimalNeeds> _query;
private readonly List<Entity> _deaths = [];
public AnimalMortalitySystem(
EntityStore store,
AnimalSet animals,
WorldEvents events,
int cellSize
)
{
_store = store;
_animals = animals;
_events = events;
_cellSize = cellSize;
_query = store.Query<AnimalGrowth, AnimalOrganism, AnimalNeeds>();
}
protected override void OnUpdateGroup()
{
_deaths.Clear();
foreach (var (growths, organisms, needsChunk, entities) in _query.Chunks)
{
var g = growths.Span;
var o = organisms.Span;
var n = needsChunk.Span;
for (var i = 0; i < g.Length; i++)
{
var ageDeath = g[i].AgeDays > o[i].Traits.Lifespan;
var starveDeath = n[i].Starve >= LethalStarveDays;
if (ageDeath || starveDeath)
{
_deaths.Add(entities.EntityAt(i));
_events.Died(o[i].Species, ageDeath ? "event.death.age" : "event.death.starve");
}
}
}
foreach (var dead in _deaths)
{
AnimalFactory.Die(_store, _animals, dead, _cellSize);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
/// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
/// <see cref="AnimalNeedsSystem"/> по <see cref="NeedDef"/>, восполняются исполнением действия.
/// </summary>
public struct AnimalNeeds : IComponent
{
/// <summary>Значения нужд [0,1] по индексам <see cref="NeedSet"/>.</summary>
public float[] Values;
/// <summary>Накоплено игровых дней истощения (летальная нужда на нуле); порог — смерть (A3).</summary>
public float Starve;
}
@@ -0,0 +1,91 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Падение/рост нужд со временем (игровые дни через <see cref="GameClock"/>): deplete-нужды убывают
/// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные
/// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>.
/// </summary>
public sealed class AnimalNeedsSystem(
GameClock clock,
float secondsPerDay,
NeedSet needs,
AnimalSet animals
) : QuerySystem<AnimalNeeds, AnimalOrganism, Health, AnimalGrowth>
{
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
private const float ToleranceMetabolicCost = 0.5f;
protected override void OnUpdate()
{
var days = clock.DeltaTime / secondsPerDay;
if (days <= 0f)
{
return;
}
foreach (var (needsChunk, organisms, healths, growths, _) in Query.Chunks)
{
var n = needsChunk.Span;
var o = organisms.Span;
var h = healths.Span;
var g = growths.Span;
for (var i = 0; i < n.Length; i++)
{
var values = n[i].Values;
if (values is null || values.Length < needs.Count)
{
continue; // защита от старого/пустого состояния
}
// Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела:
// расход ∝ масса^0.75 = (линейный размер/эталон)^2.25 (масса ∝ размер³), нормирован на
// эталон вида → молодняк экономнее, крупные прожорливее. Эталон предпосчитан (без словаря).
var sp = animals[o[i].Species];
var stageScale = sp.Stages[Math.Clamp(g[i].Stage, 0, sp.Stages.Length - 1)].Scale;
var sizeRatio = MathF.Max(
0.01f,
o[i].Traits.BodySize * stageScale / sp.RefBodySize
);
var metabolism =
MathF.Max(0.1f, o[i].Traits.Metabolism)
* (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f))
* Mass.KleiberFactorFromSizeRatio(sizeRatio);
var lethalEmpty = false;
for (var k = 0; k < needs.Count; k++)
{
var def = needs[k];
if (needs.IsDrive(k))
{
var rising = def.RisesUnder is null || h[i].State.Has(def.RisesUnder);
var delta = (rising ? def.RisePerDay : -def.DecayPerDay) * days;
values[k] = Math.Clamp(values[k] + delta, 0f, 1f);
}
else
{
values[k] = Math.Clamp(
values[k] - def.DecayPerDay * metabolism * days,
0f,
1f
);
if (def.Lethal && values[k] <= 0f)
{
lethalEmpty = true;
}
}
}
ref var starve = ref n[i].Starve;
starve = lethalEmpty ? starve + days : MathF.Max(0f, starve - days * 2f);
}
}
}
}
@@ -0,0 +1,34 @@
using Friflo.Engine.ECS;
using MrGameEng.Genetics;
namespace LittleSim.Sim;
/// <summary>
/// Компонент-организм животного: управляемый <see cref="Genome"/> (для размножения, сейва и
/// пересчёта), выраженный фенотип <see cref="Traits"/>, индекс вида в <see cref="Content.AnimalSet"/>
/// и номер поколения (для <c>popstats</c> и видимого дрейфа). Геном — ссылочный объект, поэтому
/// компонент несёт ссылку, а не копию (как у растений). Зеркало <see cref="PlantOrganism"/>.
/// </summary>
public struct AnimalOrganism : IComponent
{
/// <summary>Геном особи (аллели по генам); источник истины для признаков и размножения.</summary>
public Genome Genome;
/// <summary>Выраженные признаки — то, что читают системы.</summary>
public AnimalPhenotype Traits;
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/>.</summary>
public int Species;
/// <summary>Пол: самец (XY) или самка (XX) — выведен из локуса GeneSex при создании.</summary>
public bool IsMale;
/// <summary>Ест ли растения (диета по генам/дефу, неизменна после рождения — кэш вместо пересчёта на тик).</summary>
public bool EatsPlants;
/// <summary>Ест ли мясо (диета по генам/дефу, неизменна после рождения — кэш вместо пересчёта на тик).</summary>
public bool EatsMeat;
/// <summary>Номер поколения: max(родителей) + 1; у основателей 0.</summary>
public int Generation;
}
@@ -0,0 +1,134 @@
using System.Collections.Generic;
using MrGameEng.Genetics;
namespace LittleSim.Sim;
/// <summary>
/// Признаки (фенотип) животного, вычисленные из генома формулами генов (см. <c>Defs/Genes/</c>):
/// готовые значения, которые читают системы, не касаясь генов напрямую. Зеркало
/// <see cref="PlantPhenotype"/>. Имена полей соответствуют именам признаков в эффектах генов.
/// </summary>
public struct AnimalPhenotype
{
/// <summary>Размер тела в клетках (масштаб спрайта; позже — HP/расход энергии).</summary>
public float BodySize;
/// <summary>Скорость обмена веществ — множитель расхода нужд.</summary>
public float Metabolism;
/// <summary>Скорость передвижения (множитель).</summary>
public float MoveSpeed;
/// <summary>Объём крови (множитель) — масштаб скорости кровопотери.</summary>
public float BloodVolume;
/// <summary>Дальность зрения (множитель радиуса поиска).</summary>
public float Vision;
/// <summary>Острота слуха (множитель радиуса).</summary>
public float Hearing;
/// <summary>Острота обоняния (множитель радиуса).</summary>
public float Smell;
/// <summary>Острота осязания (множитель радиуса; чувство ближнего контакта).</summary>
public float Touch;
/// <summary>Камуфляж [0..1] — снижает обнаружение этой особи зрением (заметность = 1camouflage).</summary>
public float Camouflage;
/// <summary>Запах [0..1] — повышает обнаружение этой особи обонянием.</summary>
public float Scent;
/// <summary>Шумность [0..1] — повышает обнаружение этой особи слухом при движении.</summary>
public float Noise;
/// <summary>Размер мозга (0..1) — основа интеллекта.</summary>
public float BrainSize;
/// <summary>Теплоизоляция мехом (0..1).</summary>
public float Insulation;
/// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта.</summary>
public float FurHue;
/// <summary>Возраст созревания (игровых дней) — порог стадии Adult.</summary>
public float MaturityAge;
/// <summary>Травоядность [0..1] — способность/тяга питаться растениями.</summary>
public float Herbivory;
/// <summary>Хищничество [0..1] — тяга охотиться и питаться мясом (живая добыча/падаль).</summary>
public float Carnivory;
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
public float Omnivory;
/// <summary>
/// Стиль питания [0..1]: 0 — «пастьба» (graze), часто подтапливает сытость мелкими приёмами;
/// 1 — «обжорство» (gorge), ищет еду лишь при сильном голоде и ест до полной сытости (редкие
/// крупные приёмы, как у хищников). Задаёт пороги начала/конца кормёжки в <see cref="Sim.AnimalDecisionSystem"/>.
/// </summary>
public float FeedingStyle;
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
public float ToxinTolerance;
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
public float Sociability;
/// <summary>Продолжительность жизни (игровых дней).</summary>
public float Lifespan;
/// <summary>Сезон гона (0 весна … 3 зима).</summary>
public int BreedingSeason;
/// <summary>Срок вынашивания (игровых дней).</summary>
public float GestationDays;
/// <summary>Размер помёта.</summary>
public float LitterSize;
/// <summary>Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.</summary>
public float EggLaying;
/// <summary>Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.</summary>
public readonly bool Oviparous => EggLaying >= 0.5f;
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{
float T(string name) => traits.GetValueOrDefault(name);
// Острота чувств по умолчанию 1 (вид без гена не «слепнет»); заметность — мягкие дефолты.
float Acuity(string name) => traits.TryGetValue(name, out var v) ? v : 1f;
return new AnimalPhenotype
{
BodySize = T("bodySize"),
Metabolism = T("metabolism"),
MoveSpeed = T("moveSpeed"),
BloodVolume = T("bloodVolume"),
Vision = T("vision"),
Hearing = Acuity("hearing"),
Smell = Acuity("smell"),
Touch = Acuity("touch"),
Camouflage = T("camouflage"),
Scent = traits.GetValueOrDefault("scent", 0.5f),
Noise = traits.GetValueOrDefault("noise", 0.5f),
BrainSize = T("brainSize"),
Insulation = T("insulation"),
FurHue = T("furHue"),
MaturityAge = T("maturityAge"),
Herbivory = T("herbivory"),
Carnivory = T("carnivory"),
Omnivory = T("omnivory"),
FeedingStyle = traits.GetValueOrDefault("feedingStyle", 0.5f),
ToxinTolerance = T("toxinTolerance"),
Sociability = T("sociability"),
Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"),
LitterSize = T("litterSize"),
EggLaying = T("eggLaying"),
};
}
}
@@ -0,0 +1,183 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Беременность и роды (фаза A4): тикает срок вынашивания; по истечении рожает помёт — каждый детёныш =
/// <see cref="Genome.Breed"/> генома матери и отца (пол наследуется без YY), стадия Baby, поколение
/// матери + 1, рядом с матерью. Компонент <see cref="Pregnant"/> снимается. Потолок численности
/// страхует от взрыва популяции.
/// </summary>
public sealed class AnimalPregnancySystem : BaseSystem
{
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly NeedSet _needs;
private readonly SkillSet _skills;
private readonly ThoughtSet _thoughts;
private readonly WorldEvents _events;
private readonly GameClock _clock;
private readonly float _secondsPerDay;
private readonly int _cellSize;
private readonly int _maxAnimals;
private readonly Random _rng;
private readonly ArchetypeQuery<Pregnant, AnimalOrganism, Transform2D> _query;
private readonly ArchetypeQuery<AnimalOrganism> _census;
private readonly List<Birth> _births = [];
public AnimalPregnancySystem(
EntityStore store,
AnimalSet animals,
NeedSet needs,
SkillSet skills,
ThoughtSet thoughts,
WorldEvents events,
GameClock clock,
float secondsPerDay,
int cellSize,
int maxAnimals,
int seed
)
{
_store = store;
_animals = animals;
_needs = needs;
_skills = skills;
_thoughts = thoughts;
_events = events;
_clock = clock;
_secondsPerDay = secondsPerDay;
_cellSize = cellSize;
_maxAnimals = maxAnimals;
_rng = new Random(seed);
_query = store.Query<Pregnant, AnimalOrganism, Transform2D>();
_census = store.Query<AnimalOrganism>();
}
protected override void OnUpdateGroup()
{
var days = _clock.DeltaTime / _secondsPerDay;
if (days <= 0f)
{
return;
}
_births.Clear();
foreach (var (pregnancies, organisms, transforms, entities) in _query.Chunks)
{
var p = pregnancies.Span;
var o = organisms.Span;
var t = transforms.Span;
for (var i = 0; i < p.Length; i++)
{
ref var preg = ref p[i];
preg.DueInDays -= days;
if (preg.DueInDays > 0f)
{
continue;
}
_births.Add(
new Birth
{
Mother = entities.EntityAt(i),
Species = o[i].Species,
MotherGenome = o[i].Genome,
FatherGenome = preg.FatherGenome,
Litter = preg.Litter,
Generation = o[i].Generation + 1,
Position = t[i].Position,
Oviparous = o[i].Traits.Oviparous,
IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
}
);
}
}
var count = _census.Count;
foreach (var birth in _births)
{
if (birth.Mother.HasComponent<Pregnant>())
{
birth.Mother.RemoveComponent<Pregnant>();
}
// Роды — сильная положительная мысль матери (A8).
if (birth.Mother.HasComponent<Mood>())
{
birth.Mother.GetComponent<Mood>().Add(_thoughts.Get(AnimalThoughts.GaveBirth));
}
var bornLive = 0;
for (var k = 0; k < birth.Litter && count < _maxAnimals; k++)
{
var child = Genome.Breed(
birth.MotherGenome,
birth.FatherGenome,
_animals.GeneRegistry,
_rng
);
var offset =
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
if (birth.Oviparous)
{
// Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже.
AnimalFactory.CreateEgg(
_store,
_animals,
birth.Species,
birth.Position + offset,
child,
birth.Generation,
birth.IncubateDays,
_cellSize
);
}
else
{
AnimalFactory.Create(
_store,
_animals,
_needs,
_skills,
birth.Species,
birth.Position + offset,
ageDays: 0f,
child,
birth.Generation,
_cellSize
);
bornLive++;
}
count++;
}
// Живые роды — заметное событие (яйцекладка отметится позже при вылуплении в EggSystem).
if (bornLive > 0)
{
_events.Born(birth.Mother, birth.Species, bornLive);
}
}
}
private struct Birth
{
public Entity Mother;
public int Species;
public Genome MotherGenome;
public Genome FatherGenome;
public int Litter;
public int Generation;
public Vector2 Position;
public bool Oviparous;
public float IncubateDays;
}
}
@@ -0,0 +1,44 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using Microsoft.Xna.Framework;
using MrGameEng.AI;
using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics;
namespace LittleSim.Sim;
/// <summary>
/// Гон (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut, вне сезона снимает. Рост влечения
/// под этим хедифом считает <see cref="AnimalNeedsSystem"/> (нужда Mating с <c>risesUnder=Rut</c>) —
/// сезонность и влечение разнесены: эта система отвечает только за состояние.
/// </summary>
public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDef rut)
: QuerySystem<AnimalGrowth, AnimalOrganism, Health>
{
protected override void OnUpdate()
{
var season = (int)climate.Season;
foreach (var (growths, organisms, healths, _) in Query.Chunks)
{
var g = growths.Span;
var o = organisms.Span;
var h = healths.Span;
for (var i = 0; i < g.Length; i++)
{
var inRut =
AnimalFactory.IsAdult(animals[o[i].Species], g[i].Stage)
&& season == o[i].Traits.BreedingSeason;
if (inRut)
{
h[i].State.Add(rut);
}
else
{
h[i].State.Remove(rut.DefName);
}
}
}
}
}

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