Compare commits

..
19 Commits
Author SHA1 Message Date
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
31 changed files with 2388 additions and 151 deletions
+43 -3
View File
@@ -16,6 +16,7 @@
"body": "Quadruped", "body": "Quadruped",
"diet": ["plant"], "diet": ["plant"],
"spawnPer1000Cells": 2.5, "spawnPer1000Cells": 2.5,
"baseMassKg": 70,
"genome": { "genome": {
"GeneMaxBodySize": 1.5, "GeneMaxBodySize": 1.5,
"GeneMetabolism": 1.0, "GeneMetabolism": 1.0,
@@ -30,7 +31,9 @@
"GeneBreedingSeason": 2, "GeneBreedingSeason": 2,
"GeneGestationDays": 30, "GeneGestationDays": 30,
"GeneLitterSize": 1, "GeneLitterSize": 1,
"GeneHerbivory": 1.0 "GeneHerbivory": 1.0,
"GeneToxinTolerance": 0.1,
"GeneSociability": 0.8
} }
}, },
@@ -49,6 +52,7 @@
"attackDamage": 80, "attackDamage": 80,
"attackBleed": 3.0, "attackBleed": 3.0,
"attackBloodLoss": 5.0, "attackBloodLoss": 5.0,
"baseMassKg": 45,
"genome": { "genome": {
"GeneMaxBodySize": 1.6, "GeneMaxBodySize": 1.6,
"GeneMetabolism": 1.1, "GeneMetabolism": 1.1,
@@ -63,7 +67,8 @@
"GeneBreedingSeason": 3, "GeneBreedingSeason": 3,
"GeneGestationDays": 35, "GeneGestationDays": 35,
"GeneLitterSize": 4, "GeneLitterSize": 4,
"GeneCarnivory": 1.0 "GeneCarnivory": 1.0,
"GeneSociability": 0.6
} }
}, },
@@ -81,6 +86,7 @@
"attackDamage": 45, "attackDamage": 45,
"attackBleed": 1.8, "attackBleed": 1.8,
"attackBloodLoss": 3.0, "attackBloodLoss": 3.0,
"baseMassKg": 80,
"genome": { "genome": {
"GeneMaxBodySize": 1.3, "GeneMaxBodySize": 1.3,
"GeneMetabolism": 1.2, "GeneMetabolism": 1.2,
@@ -97,7 +103,41 @@
"GeneLitterSize": 5, "GeneLitterSize": 5,
"GeneHerbivory": 0.4, "GeneHerbivory": 0.4,
"GeneCarnivory": 0.2, "GeneCarnivory": 0.2,
"GeneOmnivory": 0.85 "GeneOmnivory": 0.85,
"GeneToxinTolerance": 0.25,
"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,
"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,
"GeneSociability": 0.55
} }
} }
] ]
+16 -3
View File
@@ -7,23 +7,36 @@
{ {
"defName": "Quadruped", "defName": "Quadruped",
"parts": [ "parts": [
{ "name": "torso", "coverage": 0.32, "maxHp": 40, "vital": true }, { "name": "torso", "coverage": 0.3, "maxHp": 40, "vital": true },
{ "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true, { "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "BloodPumping": 1.0 } }, "capacities": { "BloodPumping": 1.0 } },
{ "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12, { "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } }, "capacities": { "Breathing": 0.5 } },
{ "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12, { "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Breathing": 0.5 } }, "capacities": { "Breathing": 0.5 } },
{ "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true }, { "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, { "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
"capacities": { "Digestion": 1.0 } }, "capacities": { "Digestion": 1.0 } },
{ "name": "head", "coverage": 0.1, "maxHp": 25 }, { "name": "head", "coverage": 0.08, "maxHp": 25 },
{ "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true, { "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true,
"capacities": { "Consciousness": 1.0 } }, "capacities": { "Consciousness": 1.0 } },
{ "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8, { "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } }, "capacities": { "Sight": 0.5 } },
{ "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8, { "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8,
"capacities": { "Sight": 0.5 } }, "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": "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": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
{ "name": "legFrontRight", "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": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
+20
View File
@@ -46,12 +46,32 @@
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"], "default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
"effects": { "omnivory": "value" } }, "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" } },
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из // Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y). // самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY. // Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex", { "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "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 зима), срок вынашивания и помёт. // Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason", { "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"], "default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
+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",
+33
View File
@@ -22,6 +22,39 @@
"defName": "Bleeding", "label": "hediff.bleeding", "defName": "Bleeding", "label": "hediff.bleeding",
"initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8, "initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8,
"pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 } "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 }
} }
] ]
} }
+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,
+9 -9
View File
@@ -3,16 +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): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже. // Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже.
{ "defName": "ProductMeat", "label": "product.meat", "kind": "food" }, { "defName": "ProductMeat", "label": "product.meat", "kind": "food", "massKg": 0.5 },
{ "defName": "ProductBone", "label": "product.bone", "kind": "material" }, { "defName": "ProductBone", "label": "product.bone", "kind": "material", "massKg": 0.3 },
{ "defName": "ProductLeather", "label": "product.leather", "kind": "material" } { "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 }
]
}
+30
View File
@@ -11,12 +11,15 @@
"inspect.tab.genes": "Genes", "inspect.tab.genes": "Genes",
"inspect.tab.products": "Products", "inspect.tab.products": "Products",
"inspect.tab.needs": "Needs", "inspect.tab.needs": "Needs",
"inspect.tab.skills": "Skills",
"inspect.tab.health": "Health", "inspect.tab.health": "Health",
"inspect.tab.mood": "Mood", "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",
@@ -30,6 +33,7 @@
"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.#}",
@@ -39,6 +43,10 @@
"inspect.animal.sex": "Sex: {0} · generation {1}", "inspect.animal.sex": "Sex: {0} · generation {1}",
"inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth", "inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth",
"inspect.needline": "{0}: {1:0}%", "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.blood": "Blood: {0:0}% · pain: {1:0}%",
"inspect.health.caps": "— Capacities —", "inspect.health.caps": "— Capacities —",
"inspect.health.cap": "{0}: {1:0}%", "inspect.health.cap": "{0}: {1:0}%",
@@ -61,9 +69,20 @@
"cap.consciousness": "consciousness", "cap.consciousness": "consciousness",
"cap.moving": "moving", "cap.moving": "moving",
"cap.sight": "sight", "cap.sight": "sight",
"cap.hearing": "hearing",
"cap.talking": "talking",
"cap.eating": "eating",
"cap.breathing": "breathing",
"cap.bloodpumping": "blood pumping",
"cap.bloodfiltration": "blood filtration",
"cap.digestion": "digestion",
"hediff.rut": "rut", "hediff.rut": "rut",
"hediff.fever": "fever", "hediff.fever": "fever",
"hediff.bleeding": "bleeding", "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",
@@ -94,6 +113,16 @@
"settings.resolution": "Resolution", "settings.resolution": "Resolution",
"settings.volume": "Volume", "settings.volume": "Volume",
"settings.uiscale": "UI scale", "settings.uiscale": "UI scale",
"settings.devmode": "Developer mode",
"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",
@@ -154,6 +183,7 @@
"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.gaveBirth": "gave birth",
+30
View File
@@ -11,12 +11,15 @@
"inspect.tab.genes": "Гены", "inspect.tab.genes": "Гены",
"inspect.tab.products": "Продукты", "inspect.tab.products": "Продукты",
"inspect.tab.needs": "Нужды", "inspect.tab.needs": "Нужды",
"inspect.tab.skills": "Навыки",
"inspect.tab.health": "Здоровье", "inspect.tab.health": "Здоровье",
"inspect.tab.mood": "Настроение", "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": "созрело",
@@ -30,6 +33,7 @@
"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.#}",
@@ -39,6 +43,10 @@
"inspect.animal.sex": "Пол: {0} · поколение {1}", "inspect.animal.sex": "Пол: {0} · поколение {1}",
"inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов", "inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов",
"inspect.needline": "{0}: {1: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.blood": "Кровь: {0:0}% · боль: {1:0}%",
"inspect.health.caps": "— Способности —", "inspect.health.caps": "— Способности —",
"inspect.health.cap": "{0}: {1:0}%", "inspect.health.cap": "{0}: {1:0}%",
@@ -61,9 +69,20 @@
"cap.consciousness": "сознание", "cap.consciousness": "сознание",
"cap.moving": "движение", "cap.moving": "движение",
"cap.sight": "зрение", "cap.sight": "зрение",
"cap.hearing": "слух",
"cap.talking": "речь",
"cap.eating": "питание",
"cap.breathing": "дыхание",
"cap.bloodpumping": "кровоснабжение",
"cap.bloodfiltration": "фильтрация крови",
"cap.digestion": "пищеварение",
"hediff.rut": "гон", "hediff.rut": "гон",
"hediff.fever": "лихорадка", "hediff.fever": "лихорадка",
"hediff.bleeding": "кровотечение", "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",
@@ -94,6 +113,16 @@
"settings.resolution": "Разрешение", "settings.resolution": "Разрешение",
"settings.volume": "Громкость", "settings.volume": "Громкость",
"settings.uiscale": "Масштаб UI", "settings.uiscale": "Масштаб UI",
"settings.devmode": "Режим разработчика",
"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": "Пауза",
@@ -154,6 +183,7 @@
"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.gaveBirth": "родила потомство",
+3
View File
@@ -31,6 +31,9 @@ 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>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
public bool DeveloperMode { get; set; } = true;
} }
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary> /// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
+31
View File
@@ -149,6 +149,12 @@ public sealed class AnimalSave
/// <summary>Размер помёта.</summary> /// <summary>Размер помёта.</summary>
public int Litter { get; set; } 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; } = [];
} }
/// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary> /// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary>
@@ -173,6 +179,28 @@ public sealed class CorpseSave
public float Meat { get; set; } public float Meat { get; set; }
} }
/// <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();
}
/// <summary> /// <summary>
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание), /// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он /// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
@@ -216,6 +244,9 @@ public sealed class WorldSave
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary> /// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
public List<CorpseSave> Corpses { get; set; } = []; public List<CorpseSave> Corpses { get; set; } = [];
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
public List<EggSave> Eggs { get; set; } = [];
/// <summary>Конфиг мира для пересоздания сцены.</summary> /// <summary>Конфиг мира для пересоздания сцены.</summary>
public WorldConfig ToConfig() => public WorldConfig ToConfig() =>
new() new()
+77 -4
View File
@@ -30,6 +30,9 @@ public sealed class AnimalSet
/// <summary>Спрайт трупа (fallback — самка).</summary> /// <summary>Спрайт трупа (fallback — самка).</summary>
public required Texture2DRegion Corpse { get; init; } public required Texture2DRegion Corpse { get; init; }
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
public required Texture2DRegion Egg { get; init; }
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary> /// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
public required AnimalStageDef[] Stages { get; init; } public required AnimalStageDef[] Stages { get; init; }
@@ -38,15 +41,49 @@ public sealed class AnimalSet
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary> /// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
public required GenomeTemplate Template { get; init; } public required GenomeTemplate Template { get; init; }
/// <summary>Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей
/// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле.</summary>
public required float RefBodySize { get; init; }
} }
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные. // Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
private static readonly AnimalStageDef[] DefaultStages = private static readonly AnimalStageDef[] DefaultStages =
[ [
new() { Name = "Baby", EnterAt = 0f, RelativeTo = "maturity", Scale = 0.45f, Texture = "baby" }, new()
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 }, Name = "Baby",
new() { Name = "Senior", EnterAt = 0.8f, RelativeTo = "lifespan", Scale = 0.92f, Texture = "adult", Adult = true }, 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 Species[] _species;
@@ -82,9 +119,16 @@ public sealed class AnimalSet
Corpse = string.IsNullOrEmpty(def.CorpseTexture) Corpse = string.IsNullOrEmpty(def.CorpseTexture)
? female ? female
: atlases.GetRegion(device, def.CorpseTexture), : atlases.GetRegion(device, def.CorpseTexture),
Egg = string.IsNullOrEmpty(def.EggTexture)
? female
: atlases.GetRegion(device, def.EggTexture),
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null, Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
Template = BuildTemplate(def.Genome, genes), Template = BuildTemplate(def.Genome, genes),
RefBodySize =
def.Genome.TryGetValue("GeneMaxBodySize", out var refSize) && refSize > 0f
? refSize
: 1f,
}; };
_index[def] = i; _index[def] = i;
} }
@@ -111,6 +155,35 @@ public sealed class AnimalSet
return genome; 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: разброс берётся из // Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены // самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны. // (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
+2
View File
@@ -68,6 +68,8 @@ public sealed class GameContent
defs.RegisterType<NeedDef>("Need"); defs.RegisterType<NeedDef>("Need");
defs.RegisterType<ThoughtDef>("Thought"); defs.RegisterType<ThoughtDef>("Thought");
defs.RegisterType<BodyDef>("Body"); 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'");
+60
View File
@@ -75,6 +75,9 @@ public sealed class ProductDef : Def
{ {
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary> /// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
public string Kind { get; init; } = ""; public string Kind { get; init; } = "";
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
public float MassKg { get; init; } = 1f;
} }
/// <summary> /// <summary>
@@ -143,6 +146,15 @@ public sealed class GenomeDef
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary> /// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
public float LeafHue { get; init; } = 0.33f; 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;
} }
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary> /// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
@@ -168,6 +180,9 @@ public sealed class PlantDef : Def
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary> /// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
public string? FruitProduct { get; init; } public string? FruitProduct { get; init; }
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
public float BaseMassKg { get; init; } = 20f;
} }
/// <summary> /// <summary>
@@ -252,6 +267,12 @@ public sealed class AnimalDef : PawnDef
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary> /// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
public string CorpseTexture { get; init; } = ""; 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> /// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
public string Body { get; init; } = ""; public string Body { get; init; } = "";
@@ -347,6 +368,45 @@ public sealed class ThoughtDef : Def
public float DurationDays { get; init; } = 1f; public float DurationDays { get; init; } = 1f;
} }
/// <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; } = "";
}
/// <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; }
}
/// <summary> /// <summary>
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель), /// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в /// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
+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,
};
}
+241 -9
View File
@@ -67,6 +67,7 @@ public sealed class WorldScene : Scene
private PlantSet _plants = null!; private PlantSet _plants = null!;
private AnimalSet _animals = null!; private AnimalSet _animals = null!;
private NeedSet _needs = 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 = []; // горы (статично из террейна)
@@ -113,6 +114,7 @@ public sealed class WorldScene : Scene
_plants = new PlantSet(content, atlases, device); _plants = new PlantSet(content, atlases, device);
_animals = new AnimalSet(content, atlases, device); _animals = new AnimalSet(content, atlases, device);
_needs = new NeedSet(content); _needs = new NeedSet(content);
_skills = new SkillSet(content);
var loadingPlants = _save?.Plants is { Count: > 0 }; var loadingPlants = _save?.Plants is { Count: > 0 };
BuildTerrain( BuildTerrain(
content, content,
@@ -175,15 +177,39 @@ public sealed class WorldScene : Scene
_plants, _plants,
_animals, _animals,
_needs, _needs,
_skills,
content, content,
climate, climate,
_selection, _selection,
brackets brackets
); );
desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root); // Режим разработчика (настройка, по умолчанию вкл): без него дев-консоль и спавнер не поднимаются.
var devMode = GameSettingsStore.Load().DeveloperMode;
var spawner = devMode
? new DevSpawner(
Store,
_animals,
_plants,
_needs,
_skills,
content,
_selection,
_cellFertility,
_config.Width,
_config.Height,
CellSize
)
: null;
desktop.Root = spawner is null
? Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root)
: Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root, spawner.Panel);
this.UseInspector(renderer); this.UseInspector(renderer);
var console = this.UseDevConsole();
MrGameEng.DevConsole.DevConsole? console = null;
if (devMode)
{
console = this.UseDevConsole();
RegisterCommands(console, content, atlases); RegisterCommands(console, content, atlases);
console.Register( console.Register(
"light", "light",
@@ -192,7 +218,10 @@ public sealed class WorldScene : Scene
{ {
var radius = var radius =
args.Length > 0 args.Length > 0
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture) ? float.Parse(
args[0],
System.Globalization.CultureInfo.InvariantCulture
)
: 120f; : 120f;
var mouse = Mouse.GetState(); var mouse = Mouse.GetState();
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y)); var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
@@ -208,6 +237,14 @@ public sealed class WorldScene : Scene
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}"); 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));
@@ -231,7 +268,8 @@ public sealed class WorldScene : Scene
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой. // нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
var shore = ComputeShore(); var shore = ComputeShore();
var thoughts = new ThoughtSet(content); var thoughts = new ThoughtSet(content);
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs)); UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
UpdateSystems.Add( UpdateSystems.Add(
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut")) new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
); );
@@ -242,7 +280,11 @@ public sealed class WorldScene : Scene
CellSize, CellSize,
Context.Clock, Context.Clock,
SecondsPerDay, SecondsPerDay,
climate,
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(), 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 _config.Seed + 0x3EAD
) )
); );
@@ -260,18 +302,21 @@ public sealed class WorldScene : Scene
) )
); );
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null; 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( UpdateSystems.Add(
new AnimalActionSystem( new AnimalActionSystem(
Store, Store,
_plants, _plants,
_animals, _animals,
_needs, _needs,
_skills,
thoughts, thoughts,
Context.Clock, Context.Clock,
SecondsPerDay, SecondsPerDay,
CellSize, CellSize,
_bounds, _bounds,
bleeding, bleeding,
poisoned,
_config.Seed + 0x1B17 _config.Seed + 0x1B17
) )
); );
@@ -284,6 +329,7 @@ public sealed class WorldScene : Scene
Store, Store,
_animals, _animals,
_needs, _needs,
_skills,
thoughts, thoughts,
Context.Clock, Context.Clock,
SecondsPerDay, SecondsPerDay,
@@ -292,6 +338,31 @@ public sealed class WorldScene : Scene
_config.Seed + 0x4BED _config.Seed + 0x4BED
) )
); );
UpdateSystems.Add(
new EggSystem(
Store,
_animals,
_needs,
_skills,
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)
@@ -517,7 +588,7 @@ public sealed class WorldScene : Scene
new SaveStore().Write(save); new SaveStore().Write(save);
Log.Info( Log.Info(
$"World '{_config.Name}' saved ({save.Plants.Count} plants, " $"World '{_config.Name}' saved ({save.Plants.Count} plants, "
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses)" + $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
); );
return _config.Name; return _config.Name;
} }
@@ -592,6 +663,14 @@ public sealed class WorldScene : Scene
record.Litter = preg.Litter; 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); save.Animals.Add(record);
} }
); );
@@ -607,11 +686,28 @@ public sealed class WorldScene : Scene
X = transform.Position.X, X = transform.Position.X,
Y = transform.Position.Y, Y = transform.Position.Y,
RotDays = corpse.RotDays, RotDays = corpse.RotDays,
BodySize = corpse.Meat / AnimalFactory.MeatPerBodySize, BodySize = corpse.BodySize,
Meat = corpse.Meat, 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) private void RestorePlants(GameContent content)
@@ -662,6 +758,7 @@ public sealed class WorldScene : Scene
Store, Store,
_animals, _animals,
_needs, _needs,
_skills,
index, index,
new Vector2(saved.X, saved.Y), new Vector2(saved.X, saved.Y),
saved.AgeDays, saved.AgeDays,
@@ -685,16 +782,39 @@ public sealed class WorldScene : Scene
_animals.IndexOf(def), _animals.IndexOf(def),
new Vector2(saved.X, saved.Y), new Vector2(saved.X, saved.Y),
saved.BodySize, saved.BodySize,
saved.Meat,
CellSize CellSize
); );
ref var data = ref corpse.GetComponent<Corpse>(); ref var data = ref corpse.GetComponent<Corpse>();
data.RotDays = saved.RotDays; data.RotDays = saved.RotDays;
data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике
data.Meat = saved.Meat; }
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( Log.Info(
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save" $"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
); );
} }
@@ -755,6 +875,23 @@ public sealed class WorldScene : Scene
} }
); );
} }
// Навыки: накладываем сохранённые уровни/страсть, если состав совпал (иначе — свежие из фабрики).
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 // Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0
@@ -808,6 +945,7 @@ public sealed class WorldScene : Scene
Store, Store,
_animals, _animals,
_needs, _needs,
_skills,
s, s,
new Vector2(px, py), new Vector2(px, py),
ageDays: random.NextSingle() * maxAge, ageDays: random.NextSingle() * maxAge,
@@ -986,6 +1124,11 @@ public sealed class WorldScene : Scene
"mood [species] — live population mood (avg/min/max) and content/stressed counts", "mood [species] — live population mood (avg/min/max) and content/stressed counts",
(c, args) => RunMood(c, args) (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( console.Register(
"menu", "menu",
"menu — return to the main menu", "menu — return to the main menu",
@@ -1070,6 +1213,11 @@ public sealed class WorldScene : Scene
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C " $" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})" + $"(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) if (def.HarvestProduct is { } harvest)
{ {
@@ -1126,6 +1274,21 @@ public sealed class WorldScene : Scene
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, " $" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue: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, // Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
@@ -1187,6 +1350,7 @@ public sealed class WorldScene : Scene
var brain = new double[_animals.Count]; var brain = new double[_animals.Count];
var life = new double[_animals.Count]; var life = new double[_animals.Count];
var move = new double[_animals.Count]; var move = new double[_animals.Count];
var tox = new double[_animals.Count];
var maxGen = new int[_animals.Count]; var maxGen = new int[_animals.Count];
Store Store
@@ -1200,6 +1364,7 @@ public sealed class WorldScene : Scene
brain[s] += o.Traits.BrainSize; brain[s] += o.Traits.BrainSize;
life[s] += o.Traits.Lifespan; life[s] += o.Traits.Lifespan;
move[s] += o.Traits.MoveSpeed; move[s] += o.Traits.MoveSpeed;
tox[s] += o.Traits.ToxinTolerance;
if (o.Generation > maxGen[s]) if (o.Generation > maxGen[s])
{ {
maxGen[s] = o.Generation; maxGen[s] = o.Generation;
@@ -1227,7 +1392,8 @@ public sealed class WorldScene : Scene
var c = count[s]; var c = count[s];
console.WriteLine( console.WriteLine(
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, " $"{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.##}" + $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, "
+ $"toxinTol {tox[s] / c:0.##}"
); );
} }
@@ -1235,6 +1401,33 @@ public sealed class WorldScene : Scene
{ {
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive"); 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): дерево частей тела вида и способности здоровой особи. // Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
@@ -1436,6 +1629,45 @@ public sealed class WorldScene : Scene
} }
} }
// Наблюдаемость навыков: средний уровень каждого навыка по живым животным (растут от практики).
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) => private static string ProductLabel(GameContent content, string productDefName) =>
content.Defs.TryGet<ProductDef>(productDefName, out var product) content.Defs.TryGet<ProductDef>(productDefName, out var product)
? content.Languages.Get(product.Label) ? content.Languages.Get(product.Label)
+75 -7
View File
@@ -23,6 +23,7 @@ public static class AnimalFactory
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
NeedSet needs, NeedSet needs,
SkillSet skills,
int species, int species,
Vector2 position, Vector2 position,
float ageDays, float ageDays,
@@ -41,7 +42,7 @@ public static class AnimalFactory
sprite.CenterOrigin(); sprite.CenterOrigin();
sprite.Color = FurTint(traits); sprite.Color = FurTint(traits);
return store.CreateEntity( var entity = store.CreateEntity(
new Transform2D( new Transform2D(
position, position,
scale: new Vector2(Scale(sp, traits, stage, region, cellSize)) scale: new Vector2(Scale(sp, traits, stage, region, cellSize))
@@ -73,21 +74,42 @@ public static class AnimalFactory
Thoughts = [], Thoughts = [],
} }
); );
// Навыки: стартовые уровни + страсть (детерминированно по геному — воспроизводимо без RNG-параметра).
entity.AddComponent(
new Skills
{
Levels = skills.NewLevels(),
Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
}
);
return entity;
} }
/// <summary>Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков).</summary> // Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
public const float MeatPerBodySize = 25f; 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). // Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
private static readonly Color CorpseFreshTint = new(176, 148, 128); private static readonly Color CorpseFreshTint = new(176, 148, 128);
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).</summary> /// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и
/// запас мяса <paramref name="meatKg"/> (∝ масса тела, считается вызывающим через <see cref="Mass"/>).</summary>
public static Entity CreateCorpse( public static Entity CreateCorpse(
EntityStore store, EntityStore store,
AnimalSet animals, AnimalSet animals,
int species, int species,
Vector2 position, Vector2 position,
float bodySize, float bodySize,
float meatKg,
int cellSize int cellSize
) )
{ {
@@ -105,7 +127,48 @@ public static class AnimalFactory
Species = species, Species = species,
RotDays = 0f, RotDays = 0f,
Stage = 0, Stage = 0,
Meat = bodySize * MeatPerBodySize, 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,
} }
); );
} }
@@ -144,7 +207,7 @@ public static class AnimalFactory
return false; return false;
} }
/// <summary>Гибель особи: оставляет труп (мясо ∝ размер тела) и удаляет сущность. Общий путь смерти.</summary> /// <summary>Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти.</summary>
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize) public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
{ {
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>()) if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
@@ -154,7 +217,12 @@ public static class AnimalFactory
ref readonly var org = ref entity.GetComponent<AnimalOrganism>(); ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
var position = entity.GetComponent<Transform2D>().Position; var position = entity.GetComponent<Transform2D>().Position;
CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, cellSize); 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(); entity.DeleteEntity();
} }
+104 -6
View File
@@ -48,6 +48,12 @@ public struct AnimalPhenotype
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary> /// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
public float Omnivory; public float Omnivory;
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
public float ToxinTolerance;
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
public float Sociability;
/// <summary>Продолжительность жизни (игровых дней).</summary> /// <summary>Продолжительность жизни (игровых дней).</summary>
public float Lifespan; public float Lifespan;
@@ -60,6 +66,12 @@ public struct AnimalPhenotype
/// <summary>Размер помёта.</summary> /// <summary>Размер помёта.</summary>
public float LitterSize; 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> /// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits) public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{ {
@@ -78,10 +90,13 @@ public struct AnimalPhenotype
Herbivory = T("herbivory"), Herbivory = T("herbivory"),
Carnivory = T("carnivory"), Carnivory = T("carnivory"),
Omnivory = T("omnivory"), Omnivory = T("omnivory"),
ToxinTolerance = T("toxinTolerance"),
Sociability = T("sociability"),
Lifespan = T("lifespan"), Lifespan = T("lifespan"),
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)), BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
GestationDays = T("gestationDays"), GestationDays = T("gestationDays"),
LitterSize = T("litterSize"), LitterSize = T("litterSize"),
EggLaying = T("eggLaying"),
}; };
} }
} }
@@ -187,9 +202,11 @@ public sealed class HealthState
/// <summary> /// <summary>
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы /// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
/// способностей от хедифов (лерп 1→factor по тяжести). Вызывать после изменения частей/крови/хедифов. /// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель <paramref name="capacityScale"/>
/// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
/// частей/крови/хедифов; <paramref name="capacityScale"/>=1 — без возрастного спада.
/// </summary> /// </summary>
public void RecomputeCapacities() public void RecomputeCapacities(float capacityScale = 1f)
{ {
var pain = 0f; var pain = 0f;
foreach (var h in _hediffs) foreach (var h in _hediffs)
@@ -199,7 +216,8 @@ public sealed class HealthState
Pain = Math.Clamp(pain, 0f, 1f); Pain = Math.Clamp(pain, 0f, 1f);
var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain); // Множитель старения применяется внутри Compute (без второго прохода/аллокации списка ключей).
var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain, capacityScale);
foreach (var h in _hediffs) foreach (var h in _hediffs)
{ {
if (h.Def.CapMods.Count == 0) if (h.Def.CapMods.Count == 0)
@@ -220,8 +238,10 @@ public sealed class HealthState
/// <summary> /// <summary>
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие /// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести. /// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
/// <paramref name="immunityScale"/> — множитель набора иммунитета (способность «фильтрация крови»:
/// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
/// </summary> /// </summary>
public bool AdvanceHediffs(float days) public bool AdvanceHediffs(float days, float immunityScale = 1f)
{ {
var lethal = false; var lethal = false;
for (var i = _hediffs.Count - 1; i >= 0; i--) for (var i = _hediffs.Count - 1; i >= 0; i--)
@@ -233,7 +253,7 @@ public sealed class HealthState
} }
h.Severity += h.Def.SeverityPerDay * days; h.Severity += h.Def.SeverityPerDay * days;
h.Immunity += h.Def.ImmunityPerDay * days; h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
if (h.Immunity >= 1f) if (h.Immunity >= 1f)
{ {
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление _hediffs.RemoveAt(i); // иммунитет победил — выздоровление
@@ -436,8 +456,86 @@ public struct Corpse : IComponent
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary> /// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
public int Stage; public int Stage;
/// <summary>Доступное мясо (∝ размер тела) — для разделки/падальщиков (фаза-горизонт).</summary> /// <summary>Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт).</summary>
public float Meat; public float Meat;
/// <summary>Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса.</summary>
public float BodySize;
}
/// <summary>
/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
/// вылупляется в детёныша (<see cref="Sim.EggSystem"/>). Геном — managed-ссылка (как у трупа/беременности).
/// </summary>
public struct Egg : IComponent
{
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта при вылуплении и сейва).</summary>
public int Species;
/// <summary>Геном будущего детёныша (уже скрещён из родителей на момент кладки).</summary>
public Genome Genome;
/// <summary>Поколение будущего детёныша.</summary>
public int Generation;
/// <summary>Остаток инкубации в игровых днях; ≤0 — вылупление.</summary>
public float IncubateDays;
}
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
public static class AnimalSkills
{
/// <summary>Добыча корма: эффективность выпаса/поедания растений.</summary>
public const string Foraging = "Foraging";
/// <summary>Охота: урон по добыче.</summary>
public const string Hunting = "Hunting";
/// <summary>Уклонение: скорость бегства от хищника.</summary>
public const string Evasion = "Evasion";
}
/// <summary>
/// Навыки особи (как нужды/способности — managed-массивы по индексам <see cref="Content.SkillSet"/>):
/// уровни [0..1] (растут от практики, угасают без неё) и страсть к каждому навыку (скорость обучения).
/// Есть у животных; те же навыки получат будущие люди. Добавление навыка не меняет структуру.
/// </summary>
public struct Skills : IComponent
{
/// <summary>Уровни навыков [0..1] по индексам <see cref="Content.SkillSet"/>.</summary>
public float[] Levels;
/// <summary>Страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) — множитель обучения.</summary>
public byte[] Passions;
}
/// <summary>
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
/// сверх грузоподъёмности (<see cref="Sim.Mass.CarryCapacity"/>) замедляет движение. Компонент появляется
/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь.
/// </summary>
public struct Carrier : IComponent
{
/// <summary>Суммарная масса переносимого груза (кг).</summary>
public float CarriedKg;
}
/// <summary>
/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
/// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) —
/// зверь становится разносчиком флоры. Геном — managed-ссылка (клон родительского растения).
/// </summary>
public struct GutSeed : IComponent
{
/// <summary>Индекс вида растения в <see cref="Content.PlantSet"/>.</summary>
public int PlantSpecies;
/// <summary>Геном будущего ростка (клон съеденного растения).</summary>
public Genome Genome;
/// <summary>Остаток времени до высадки в игровых днях; ≤0 — росток падает на землю.</summary>
public float DropInDays;
} }
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary> /// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
File diff suppressed because it is too large Load Diff
+57 -10
View File
@@ -9,8 +9,27 @@ public static class AnimalCapacities
public const string Moving = "Moving"; public const string Moving = "Moving";
public const string Breathing = "Breathing"; public const string Breathing = "Breathing";
public const string BloodPumping = "BloodPumping"; public const string BloodPumping = "BloodPumping";
public const string BloodFiltration = "BloodFiltration";
public const string Sight = "Sight"; public const string Sight = "Sight";
public const string Hearing = "Hearing";
public const string Talking = "Talking";
public const string Eating = "Eating";
public const string Digestion = "Digestion"; public const string Digestion = "Digestion";
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
public static readonly string[] DisplayOrder =
[
Consciousness,
Moving,
Sight,
Hearing,
Talking,
Eating,
Breathing,
BloodPumping,
BloodFiltration,
Digestion,
];
} }
/// <summary> /// <summary>
@@ -21,10 +40,20 @@ public static class AnimalCapacities
/// </summary> /// </summary>
public static class CapacityCalc public static class CapacityCalc
{ {
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary> /// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1);
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain) /// <paramref name="capacityScale"/> — общий множитель (старение), применяется к итогу без лишних аллокаций.</summary>
public static Dictionary<string, float> Compute(
PartInstance[] parts,
float blood,
float pain,
float capacityScale = 1f
)
{ {
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
// статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%.
var raw = new Dictionary<string, float>(System.StringComparer.Ordinal); var raw = new Dictionary<string, float>(System.StringComparer.Ordinal);
var declared = new HashSet<string>(System.StringComparer.Ordinal);
foreach (var part in parts) foreach (var part in parts)
{ {
if (part.Def is null) if (part.Def is null)
@@ -35,36 +64,54 @@ public static class CapacityCalc
var fraction = part.Fraction; var fraction = part.Fraction;
foreach (var (capacity, contribution) in part.Def.Capacities) foreach (var (capacity, contribution) in part.Def.Capacities)
{ {
declared.Add(capacity);
raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction; raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction;
} }
} }
float Raw(string id) => raw.GetValueOrDefault(id); float Raw(string id) => raw.GetValueOrDefault(id);
static float Clamp01(float v) => v < 0f ? 0f : v > 1f ? 1f : v; static float Clamp01(float v) =>
v < 0f ? 0f
: v > 1f ? 1f
: v;
// Базовые (независимые) способности: кровоснабжение/дыхание/фильтрация × уровень крови.
var bloodLevel = Clamp01(blood); var bloodLevel = Clamp01(blood);
var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel; var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel;
var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel; var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel;
var bloodFiltration = Raw(AnimalCapacities.BloodFiltration) * bloodLevel;
// Сознание зависит от кровоснабжения, дыхания и боли (модель RimWorld).
var consciousness = var consciousness =
Raw(AnimalCapacities.Consciousness) Raw(AnimalCapacities.Consciousness)
* Clamp01(bloodPumping) * Clamp01(bloodPumping)
* Clamp01(breathing) * Clamp01(breathing)
* (1f - Clamp01(pain)); * (1f - Clamp01(pain));
var cons = Clamp01(consciousness);
var pump = Clamp01(bloodPumping);
var result = new Dictionary<string, float>(System.StringComparer.Ordinal) // Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения).
var computed = new Dictionary<string, float>(System.StringComparer.Ordinal)
{ {
[AnimalCapacities.BloodPumping] = bloodPumping, [AnimalCapacities.BloodPumping] = bloodPumping,
[AnimalCapacities.Breathing] = breathing, [AnimalCapacities.Breathing] = breathing,
[AnimalCapacities.BloodFiltration] = bloodFiltration,
[AnimalCapacities.Consciousness] = consciousness, [AnimalCapacities.Consciousness] = consciousness,
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping), [AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump,
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness, [AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping), [AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons,
[AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons,
[AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons,
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump,
}; };
// Модовые способности, не охваченные зависимостями, — сырыми. // В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
foreach (var (capacity, value) in raw) // (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
var scale = Clamp01(capacityScale);
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
foreach (var capacity in declared)
{ {
result.TryAdd(capacity, value); var value = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity);
result[capacity] = scale < 1f ? value * scale : value;
} }
return result; return result;
+70
View File
@@ -0,0 +1,70 @@
using LittleSim.Content;
namespace LittleSim.Sim;
/// <summary>
/// Система веса (масса тел и грузоподъёмность). Масса — производная от РАЗМЕРА по кубическому закону
/// (вдвое крупнее ⇒ в 8 раз тяжелее): <c>масса = BaseMassKg × (размер / эталон)³</c>, где эталон —
/// базовый размер вида. Так детёныш/росток легче взрослого/зрелого, а ген размера двигает и массу.
/// Грузоподъёмность — задел под перенос предметов: доля массы тела × способность (манипуляция/движение).
/// </summary>
public static class Mass
{
/// <summary>Какую долю собственной массы существо может нести (×способность). Человек (руки) — больше.</summary>
public const float CarryFraction = 0.35f;
/// <summary>Доля массы тела, превращающаяся в мясо трупа (для разделки/падальщиков).</summary>
public const float MeatFraction = 0.5f;
private static float Cube(float v) => v * v * v;
/// <summary>Масса особи-животного (кг): эталон вида × куб (текущий линейный размер / эталонный).</summary>
public static float OfAnimal(AnimalDef def, in AnimalPhenotype traits, float stageScale)
{
var reference = def.Genome.GetValueOrDefault("GeneMaxBodySize", 1f);
if (reference <= 0f)
{
reference = 1f;
}
var linear = MathF.Max(0.01f, traits.BodySize) * MathF.Max(0.05f, stageScale);
return def.BaseMassKg * Cube(linear / reference);
}
/// <summary>Масса растения (кг): эталон вида × куб (размер текущей стадии / размер зрелой).</summary>
public static float OfPlant(PlantDef def, float stageSizeCells, float matureSizeCells)
{
var ratio = matureSizeCells > 0f ? stageSizeCells / matureSizeCells : 1f;
return def.BaseMassKg * Cube(ratio);
}
/// <summary>Грузоподъёмность (кг): доля массы тела × способность нести (манипуляция/движение, 0..1+).</summary>
public static float CarryCapacity(float bodyMassKg, float capability) =>
bodyMassKg * CarryFraction * MathF.Max(0f, capability);
/// <summary>
/// Метаболический множитель по закону Клайбера: расход энергии ∝ масса^0.75. Нормирован на эталонную
/// массу вида (взрослый базового размера = 1), поэтому крупные особи прожорливее, а молодняк — экономнее,
/// без межвидового взрыва баланса.
/// </summary>
public static float KleiberFactor(float massKg, float referenceMassKg)
{
if (referenceMassKg <= 0f || massKg <= 0f)
{
return 1f;
}
return MathF.Pow(massKg / referenceMassKg, 0.75f);
}
/// <summary>Множитель скорости от ноши: в пределах грузоподъёмности — 1; перегруз тормозит (до 0.25).</summary>
public static float LoadSpeedFactor(float carriedKg, float capacityKg)
{
if (capacityKg <= 0f || carriedKg <= capacityKg)
{
return 1f;
}
return MathF.Max(0.25f, capacityKg / carriedKg);
}
}
+73
View File
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace LittleSim.Sim;
/// <summary>
/// Пространственный индекс растений (равномерная сетка ячеек) для радиус-запросов поиска корма: вместо
/// прохода по ВСЕМ растениям на каждое решение травоядного (O(растений) на особь) — только растения в
/// ближних ячейках. Перестраивается раз в кадр из позиций растений (O(растений) линейно). В ячейке лежит
/// то, что нужно поиску — позиция, id, токсичность и вкусность — без дорефетча компонентов. Списки ячеек
/// переиспользуются (пул), чтобы перестройка не аллоцировала. Порядок обхода детерминирован (вложенные
/// циклы по ячейкам + порядок вставки), а выбор всё равно ломает ничьи по меньшему id — детерминизм цел.
/// </summary>
public sealed class PlantGrid
{
/// <summary>Запись растения в ячейке: id, позиция и признаки, нужные поиску корма.</summary>
public readonly record struct Entry(int Id, Vector2 Pos, float Toxicity, float Palatability);
private readonly float _cell;
private readonly Dictionary<(int, int), List<Entry>> _cells = new();
private readonly Stack<List<Entry>> _pool = new();
public PlantGrid(float cellSize) => _cell = cellSize > 0f ? cellSize : 1f;
/// <summary>Очищает сетку (списки ячеек возвращаются в пул для переиспользования).</summary>
public void Clear()
{
foreach (var list in _cells.Values)
{
list.Clear();
_pool.Push(list);
}
_cells.Clear();
}
/// <summary>Добавляет растение в его ячейку.</summary>
public void Add(int id, Vector2 pos, float toxicity, float palatability)
{
var key = CellOf(pos);
if (!_cells.TryGetValue(key, out var list))
{
list = _pool.Count > 0 ? _pool.Pop() : new List<Entry>();
_cells[key] = list;
}
list.Add(new Entry(id, pos, toxicity, palatability));
}
/// <summary>Собирает в <paramref name="results"/> растения из ячеек, перекрывающих радиус (точную
/// дистанцию проверяет вызывающий). Буфер переиспользуется — без аллокаций на запрос.</summary>
public void Collect(Vector2 center, float radius, List<Entry> results)
{
results.Clear();
var minX = (int)MathF.Floor((center.X - radius) / _cell);
var maxX = (int)MathF.Floor((center.X + radius) / _cell);
var minY = (int)MathF.Floor((center.Y - radius) / _cell);
var maxY = (int)MathF.Floor((center.Y + radius) / _cell);
for (var cx = minX; cx <= maxX; cx++)
{
for (var cy = minY; cy <= maxY; cy++)
{
if (_cells.TryGetValue((cx, cy), out var list))
{
results.AddRange(list);
}
}
}
}
private (int, int) CellOf(Vector2 p) =>
((int)MathF.Floor(p.X / _cell), (int)MathF.Floor(p.Y / _cell));
}
+1
View File
@@ -57,6 +57,7 @@ public sealed class PlantGrowthSystem(
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
var rate = var rate =
traits.Vigor traits.Vigor
* traits.DefenseGrowthFactor() // цена защиты: токсичные/колючие растут медленнее
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance) * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) * Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
* Suitability.Gaussian( * Suitability.Gaussian(
+27
View File
@@ -32,6 +32,16 @@ public struct PlantPhenotype
public float HarvestAmount; public float HarvestAmount;
public float LeafHue; public float LeafHue;
// --- Защита (коэволюция с травоядными) ---
/// <summary>Токсичность [0..1]: отравляет поедателя; цена — замедление роста.</summary>
public float Toxicity;
/// <summary>Шипы [0..1]: травмируют поедателя; цена — замедление роста.</summary>
public float Thorns;
/// <summary>Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2).</summary>
public float Palatability;
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary> /// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant; public bool IsVariant;
@@ -71,9 +81,26 @@ public struct PlantPhenotype
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)), FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
HarvestAmount = T("harvestAmount"), HarvestAmount = T("harvestAmount"),
LeafHue = T("leafHue"), LeafHue = T("leafHue"),
Toxicity = T("toxicity"),
Thorns = T("thorns"),
Palatability = T("palatability"),
IsVariant = T("variant") >= 0.5f, IsVariant = T("variant") >= 0.5f,
}; };
} }
// Цена защиты: токсичность и шипы отнимают ресурсы у роста. Без этой платы отбор гнал бы защиту к
// максимуму у всех растений и коэволюция бы встала — а так под слабым выпасом выгоднее расти быстрее.
private const float ToxicityGrowthCost = 0.4f;
private const float ThornsGrowthCost = 0.3f;
private const float MinGrowthFactor = 0.2f;
/// <summary>Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ <c>MinGrowthFactor</c>).</summary>
public readonly float DefenseGrowthFactor() =>
Math.Clamp(
1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns,
MinGrowthFactor,
1f
);
} }
/// <summary> /// <summary>
+2 -2
View File
@@ -125,13 +125,13 @@ public sealed class GodCameraSystem(
Entity cameraEntity, Entity cameraEntity,
InputManager input, InputManager input,
Renderer2D renderer, Renderer2D renderer,
MrGameEng.DevConsole.DevConsole console, MrGameEng.DevConsole.DevConsole? console,
Func<bool>? blocked = null Func<bool>? blocked = null
) : BaseSystem ) : BaseSystem
{ {
protected override void OnUpdateGroup() protected override void OnUpdateGroup()
{ {
if (console.IsOpen || blocked?.Invoke() == true) if (console?.IsOpen == true || blocked?.Invoke() == true)
{ {
return; return;
} }
+299
View File
@@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using LittleSim.Content;
using LittleSim.Sim;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Graphics;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
namespace LittleSim.UI;
/// <summary>
/// Дев-спавнер в духе RimWorld (только в режиме разработчика): боковая панель со списком по категориям.
/// Виды животных/растений берутся ИЗ ДЕФОВ (модовый контент появляется сам); «инструменты» (урон/смерть/
/// болезнь) и прочие модовые пункты — из <see cref="DevToolDef"/>. Спавн-пункт «берётся в руку»: клик по
/// миру создаёт сущность в курсоре (ПКМ снимает). Инструменты-действия применяются к выбранной особи
/// сразу по нажатию. Так удобно сравнивать сущности и наносить им урон. Управляет <see cref="DevSpawnerSystem"/>.
/// </summary>
public sealed class DevSpawner
{
private readonly EntityStore _store;
private readonly AnimalSet _animals;
private readonly PlantSet _plants;
private readonly NeedSet _needs;
private readonly SkillSet _skills;
private readonly GameContent _content;
private readonly Selection _selection;
private readonly float[] _cellFertility;
private readonly int _width;
private readonly int _height;
private readonly int _cellSize;
private readonly HediffDef? _fever;
private readonly HediffDef? _bleeding;
private readonly Random _rng = new(0x5A11);
private readonly VerticalStackPanel _panel;
private readonly Label _title;
private string? _armedKind;
private string? _armedTarget;
public DevSpawner(
EntityStore store,
AnimalSet animals,
PlantSet plants,
NeedSet needs,
SkillSet skills,
GameContent content,
Selection selection,
float[] cellFertility,
int width,
int height,
int cellSize
)
{
_store = store;
_animals = animals;
_plants = plants;
_needs = needs;
_skills = skills;
_content = content;
_selection = selection;
_cellFertility = cellFertility;
_width = width;
_height = height;
_cellSize = cellSize;
_fever = content.Defs.TryGet<HediffDef>("Fever", out var f) ? f : null;
_bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var b) ? b : null;
var lang = content.Languages;
_title = new Label { TextColor = Ui.Accent, Wrap = true };
var list = new VerticalStackPanel { Spacing = 2 };
// Инструменты-действия (по выбранной особи) + модовые DevToolDef.
Header(list, lang.Get("dev.tools"));
Entry(list, lang.Get("dev.damage"), () => ActOnSelection("damage", ""));
Entry(list, lang.Get("dev.kill"), () => ActOnSelection("kill", ""));
Entry(list, lang.Get("dev.infect"), () => ActOnSelection("infect", ""));
foreach (var tool in content.Defs.All<DevToolDef>())
{
var kind = tool.Kind;
var target = tool.Target;
var label = lang.Get(tool.Label);
if (kind is "spawn-animal" or "spawn-plant")
{
Entry(list, label, () => Arm(kind, target));
}
else
{
Entry(list, label, () => ActOnSelection(kind, target));
}
}
// Виды — автоматически из дефов (включая модовые): «взять в руку» спавн.
Header(list, lang.Get("dev.animals"));
foreach (var name in content.Defs.NamesOf("Animal"))
{
var defName = name;
Entry(list, "+ " + lang.Get(AnimalLabel(defName)), () => Arm("spawn-animal", defName));
}
Header(list, lang.Get("dev.plants"));
foreach (var name in content.Defs.NamesOf("Plant"))
{
var defName = name;
Entry(list, "+ " + lang.Get(PlantLabel(defName)), () => Arm("spawn-plant", defName));
}
_panel = new VerticalStackPanel
{
Spacing = 6,
Padding = new Thickness(12),
Width = 250,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 14, 12, 0),
Background = new SolidBrush(new Color(10, 13, 18, 240)),
Border = new SolidBrush(new Color(96, 134, 168)),
BorderThickness = new Thickness(2),
Visible = false,
};
_panel.Widgets.Add(new Label { Text = lang.Get("dev.title"), TextColor = Ui.Accent });
_panel.Widgets.Add(_title);
_panel.Widgets.Add(new HorizontalSeparator());
_panel.Widgets.Add(new ScrollViewer { Height = 460, Content = list });
UpdateTitle();
}
/// <summary>Виджет панели (добавляется в корень экрана сцены).</summary>
public Widget Panel => _panel;
/// <summary>Взведён ли спавн-инструмент (клик по миру создаёт сущность).</summary>
public bool ArmedSpawn => _armedKind is "spawn-animal" or "spawn-plant";
/// <summary>Показать/скрыть панель (горячая клавиша из системы).</summary>
public void Toggle() => _panel.Visible = !_panel.Visible;
/// <summary>Снять «инструмент с руки» (ПКМ).</summary>
public void Disarm()
{
_armedKind = null;
_armedTarget = null;
UpdateTitle();
}
/// <summary>Создаёт взведённую сущность в точке мира (вызывает система по клику ЛКМ).</summary>
public void SpawnAt(Vector2 world)
{
if (
_armedKind == "spawn-animal"
&& _content.Defs.TryGet<AnimalDef>(_armedTarget ?? "", out var adef)
)
{
var idx = _animals.IndexOf(adef);
var age = adef.Genome.GetValueOrDefault("GeneMaturityAge", 90f); // взрослый
AnimalFactory.Create(
_store,
_animals,
_needs,
_skills,
idx,
world,
age,
_animals.GenerateGenome(idx, _rng),
generation: 0,
_cellSize
);
}
else if (
_armedKind == "spawn-plant"
&& _content.Defs.TryGet<PlantDef>(_armedTarget ?? "", out var pdef)
)
{
var idx = _plants.IndexOf(pdef);
var cx = Math.Clamp((int)(world.X / _cellSize), 0, _width - 1);
var cy = Math.Clamp((int)(world.Y / _cellSize), 0, _height - 1);
var cell = cy * _width + cx;
var fertility = cell >= 0 && cell < _cellFertility.Length ? _cellFertility[cell] : 1f;
PlantFactory.Create(
_store,
_plants,
idx,
world,
_plants[idx].MaturityDays, // зрелое
_plants[idx].Template.Generate(_rng),
fertility,
_cellSize
);
}
}
// Применяет инструмент-действие к выбранной особи (урон/смерть/болезнь) — для сравнения/тестов.
private void ActOnSelection(string kind, string target)
{
if (
!_selection.HasSelection
|| !_store.TryGetEntityById(_selection.EntityId, out var e)
|| e.IsNull
)
{
return;
}
switch (kind)
{
case "kill" when e.HasComponent<AnimalOrganism>():
AnimalFactory.Die(_store, _animals, e, _cellSize);
break;
case "damage" when e.TryGetComponent<Health>(out var h) && h.State is not null:
h.State.ApplyInjury(15f, 0.08f, _bleeding, 1.5f, _rng);
break;
case "infect" when e.TryGetComponent<Health>(out var h2) && h2.State is not null:
var def =
target.Length > 0 && _content.Defs.TryGet<HediffDef>(target, out var hd)
? hd
: _fever;
if (def is not null)
{
h2.State.Add(def);
}
break;
}
}
private void Arm(string kind, string target)
{
_armedKind = kind;
_armedTarget = target;
UpdateTitle();
}
private void UpdateTitle() =>
_title.Text = _armedKind is null
? _content.Languages.Get("dev.hint")
: _content.Languages.Format("dev.armed", _armedTarget ?? "");
private static void Header(VerticalStackPanel list, string text) =>
list.Widgets.Add(new Label { Text = text, TextColor = Ui.Muted });
private static void Entry(VerticalStackPanel list, string text, Action onClick)
{
var button = new TextButton { Text = text };
button.Click += (_, _) => onClick();
list.Widgets.Add(button);
}
private string AnimalLabel(string defName) =>
_content.Defs.TryGet<AnimalDef>(defName, out var def) ? def.Label : defName;
private string PlantLabel(string defName) =>
_content.Defs.TryGet<PlantDef>(defName, out var def) ? def.Label : defName;
}
/// <summary>
/// Управление дев-спавнером: горячая клавиша F9 открывает/закрывает панель; при взведённом спавн-
/// инструменте ЛКМ по миру (не над UI) создаёт сущность в курсоре, ПКМ снимает инструмент.
/// </summary>
public sealed class DevSpawnerSystem(Renderer2D renderer, DevSpawner spawner, Func<bool> overUi)
: BaseSystem
{
private bool _prevToggle;
private bool _prevLeft;
private bool _prevRight;
protected override void OnUpdateGroup()
{
var keyboard = Keyboard.GetState();
var toggle = keyboard.IsKeyDown(Keys.F9);
if (toggle && !_prevToggle)
{
spawner.Toggle();
}
_prevToggle = toggle;
var mouse = Mouse.GetState();
var left = mouse.LeftButton == ButtonState.Pressed;
var right = mouse.RightButton == ButtonState.Pressed;
if (spawner.ArmedSpawn && left && !_prevLeft && !overUi())
{
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
spawner.SpawnAt(world);
}
if (right && !_prevRight)
{
spawner.Disarm();
}
_prevLeft = left;
_prevRight = right;
}
}
+94 -5
View File
@@ -31,17 +31,26 @@ internal sealed class InspectPanel
Genes, Genes,
Products, Products,
Needs, Needs,
Skills,
Health, Health,
Mood, Mood,
} }
private static readonly Tab[] PlantTabs = [Tab.Overview, Tab.Genes, Tab.Products]; private static readonly Tab[] PlantTabs = [Tab.Overview, Tab.Genes, Tab.Products];
private static readonly Tab[] AnimalTabs = [Tab.Overview, Tab.Needs, Tab.Health, Tab.Mood]; private static readonly Tab[] AnimalTabs =
[
Tab.Overview,
Tab.Needs,
Tab.Skills,
Tab.Health,
Tab.Mood,
];
private readonly EntityStore _store; private readonly EntityStore _store;
private readonly PlantSet _plants; private readonly PlantSet _plants;
private readonly AnimalSet _animals; private readonly AnimalSet _animals;
private readonly NeedSet _needs; private readonly NeedSet _needs;
private readonly SkillSet _skills;
private readonly GameContent _content; private readonly GameContent _content;
private readonly Climate _climate; private readonly Climate _climate;
private readonly Selection _selection; private readonly Selection _selection;
@@ -58,6 +67,7 @@ internal sealed class InspectPanel
PlantSet plants, PlantSet plants,
AnimalSet animals, AnimalSet animals,
NeedSet needs, NeedSet needs,
SkillSet skills,
GameContent content, GameContent content,
Climate climate, Climate climate,
Selection selection, Selection selection,
@@ -68,6 +78,7 @@ internal sealed class InspectPanel
_plants = plants; _plants = plants;
_animals = animals; _animals = animals;
_needs = needs; _needs = needs;
_skills = skills;
_content = content; _content = content;
_climate = climate; _climate = climate;
_selection = selection; _selection = selection;
@@ -81,6 +92,7 @@ internal sealed class InspectPanel
AddTab(tabBar, Tab.Genes, "inspect.tab.genes"); AddTab(tabBar, Tab.Genes, "inspect.tab.genes");
AddTab(tabBar, Tab.Products, "inspect.tab.products"); AddTab(tabBar, Tab.Products, "inspect.tab.products");
AddTab(tabBar, Tab.Needs, "inspect.tab.needs"); AddTab(tabBar, Tab.Needs, "inspect.tab.needs");
AddTab(tabBar, Tab.Skills, "inspect.tab.skills");
AddTab(tabBar, Tab.Health, "inspect.tab.health"); AddTab(tabBar, Tab.Health, "inspect.tab.health");
AddTab(tabBar, Tab.Mood, "inspect.tab.mood"); AddTab(tabBar, Tab.Mood, "inspect.tab.mood");
@@ -246,6 +258,9 @@ internal sealed class InspectPanel
case Tab.Needs: case Tab.Needs:
BuildAnimalNeeds(text, entity); BuildAnimalNeeds(text, entity);
break; break;
case Tab.Skills:
BuildAnimalSkills(text, entity);
break;
case Tab.Health: case Tab.Health:
BuildAnimalHealth(text, entity); BuildAnimalHealth(text, entity);
break; break;
@@ -291,6 +306,14 @@ internal sealed class InspectPanel
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan)); text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan));
// Вес растения (∝ кубу размера текущей стадии относительно зрелой).
var plantMass = Mass.OfPlant(
species.Def,
species.Stages[grow.Stage].SizeCells,
species.Stages[^1].SizeCells
);
text.AppendLine(languages.Format("inspect.plantmass", plantMass));
// Состояние по температуре (рост/покой) и накопленный стресс. // Состояние по температуре (рост/покой) и накопленный стресс.
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand(); var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
var temperature = _climate.Temperature; var temperature = _climate.Temperature;
@@ -331,6 +354,13 @@ internal sealed class InspectPanel
traits.LeafHue traits.LeafHue
) )
); );
if (traits.Toxicity > 0.01f || traits.Thorns > 0.01f)
{
text.AppendLine(
languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns)
);
}
if (traits.IsVariant) if (traits.IsVariant)
{ {
text.AppendLine(languages.Get("inspect.gene.variant")); text.AppendLine(languages.Get("inspect.gene.variant"));
@@ -393,6 +423,16 @@ internal sealed class InspectPanel
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan)); text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan));
var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female"); var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female");
text.AppendLine(languages.Format("inspect.animal.sex", sex, org.Generation)); text.AppendLine(languages.Format("inspect.animal.sex", sex, org.Generation));
// Вес тела и грузоподъёмность (∝ массе × способность нести; четвероногие — через Moving).
var stageScale = sp.Stages[grow.Stage].Scale;
var mass = Mass.OfAnimal(sp.Def, org.Traits, stageScale);
var carryCap =
entity.TryGetComponent<Health>(out var health) && health.State is not null
? Mass.CarryCapacity(mass, health.State.Capacity(AnimalCapacities.Moving))
: Mass.CarryCapacity(mass, 1f);
text.AppendLine(languages.Format("inspect.mass", mass, carryCap));
if (entity.TryGetComponent<Pregnant>(out var preg)) if (entity.TryGetComponent<Pregnant>(out var preg))
{ {
text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays)); text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays));
@@ -421,6 +461,37 @@ internal sealed class InspectPanel
} }
} }
// — Животное: Навыки — уровень каждого навыка (%) и страсть (★/★★), растут от практики.
private void BuildAnimalSkills(StringBuilder text, Entity entity)
{
var languages = _content.Languages;
if (!entity.TryGetComponent<Skills>(out var sk) || sk.Levels is null)
{
text.AppendLine(languages.Get("inspect.health.none"));
return;
}
for (var i = 0; i < _skills.Count && i < sk.Levels.Length; i++)
{
var passion =
sk.Passions is not null && i < sk.Passions.Length ? sk.Passions[i] : (byte)0;
var stars = passion switch
{
2 => "★★",
1 => "★",
_ => "",
};
text.AppendLine(
languages.Format(
"inspect.skillline",
languages.Get(_skills[i].Label),
sk.Levels[i] * 100f,
stars
)
);
}
}
// — Животное: Здоровье — кровь/боль, ключевые способности, активные хедифы (раны/болезни/гон). // — Животное: Здоровье — кровь/боль, ключевые способности, активные хедифы (раны/болезни/гон).
private void BuildAnimalHealth(StringBuilder text, Entity entity) private void BuildAnimalHealth(StringBuilder text, Entity entity)
{ {
@@ -435,12 +506,27 @@ internal sealed class InspectPanel
text.AppendLine( text.AppendLine(
languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f) languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f)
); );
if (state.Parts.Length > 0) if (state.Capacities.Count > 0)
{ {
text.AppendLine(languages.Get("inspect.health.caps")); text.AppendLine(languages.Get("inspect.health.caps"));
AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness"); // Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть.
AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving"); var shown = new HashSet<string>(StringComparer.Ordinal);
AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight"); foreach (var capId in AnimalCapacities.DisplayOrder)
{
if (state.Capacities.ContainsKey(capId))
{
AppendCapacity(text, state, capId, CapacityLabel(capId));
shown.Add(capId);
}
}
foreach (var capId in state.Capacities.Keys)
{
if (shown.Add(capId))
{
AppendCapacity(text, state, capId, CapacityLabel(capId));
}
}
} }
if (state.Hediffs.Count > 0) if (state.Hediffs.Count > 0)
@@ -463,6 +549,9 @@ internal sealed class InspectPanel
} }
} }
// Ключ локализации метки способности: cap.<id в нижнем регистре> (cap.consciousness, cap.bloodpumping…).
private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant();
private void AppendCapacity( private void AppendCapacity(
StringBuilder text, StringBuilder text,
HealthState state, HealthState state,
+10
View File
@@ -98,6 +98,16 @@ internal static class SettingsPanel
// Громкость — кнопки −/+ с подписью процента. // Громкость — кнопки −/+ с подписью процента.
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers)); column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
// Режим разработчика — тумблер (дев-консоль и спавнер; по умолчанию включён).
column.Widgets.Add(
Toggle(
lang.Get("settings.devmode"),
() => settings.DeveloperMode,
v => settings.DeveloperMode = v,
refreshers
)
);
column.Widgets.Add(new Label { Height = 8 }); column.Widgets.Add(new Label { Height = 8 });
var buttons = Ui.Row(10); var buttons = Ui.Row(10);
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150)); buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));