Compare commits
63
Commits
c47f99bc29
...
main
+4
-1
@@ -417,4 +417,7 @@ FodyWeavers.xsd
|
|||||||
|
|
||||||
# LittleSim
|
# LittleSim
|
||||||
Cache/
|
Cache/
|
||||||
|
# Рантайм-файлы (пишутся игрой в рабочем каталоге) — не версионируем
|
||||||
|
/settings.json
|
||||||
|
/Saves/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"type": "Animal",
|
||||||
|
// Виды-животные: подтип Pawn (kind=animal) + базовый геном вида из общих Gene-дефов (см. Defs/Genes/).
|
||||||
|
// Особь при спавне получает аллели вокруг этих баз, фенотип — формулами генов. Диета задаётся ГЕНАМИ
|
||||||
|
// (herbivory/carnivory/omnivory): олень — травоядный, волк — хищник, кабан — всеядный. Хищники несут
|
||||||
|
// боевые параметры (attack*) и охотятся на добычу не крупнее себя; жертвы убегают (см. AnimalSystems).
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"defName": "Deer",
|
||||||
|
"label": "pawn.deer",
|
||||||
|
"kind": "animal",
|
||||||
|
"texture": "things/pawn/animal/deer/DeerFemale_east",
|
||||||
|
"maleTexture": "things/pawn/animal/deer/DeerMale_east",
|
||||||
|
"babyTexture": "things/pawn/animal/deer/DeerBaby_east",
|
||||||
|
"corpseTexture": "things/pawn/animal/deer/Dessicated_DeerFemale_east",
|
||||||
|
"body": "Quadruped",
|
||||||
|
"diet": ["plant"],
|
||||||
|
"spawnPer1000Cells": 2.5,
|
||||||
|
"baseMassKg": 70,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 1.5,
|
||||||
|
"GeneMetabolism": 1.0,
|
||||||
|
"GeneMoveSpeed": 1.2,
|
||||||
|
"GeneBloodVolume": 1.4,
|
||||||
|
"GeneVision": 1.3,
|
||||||
|
"GeneHearing": 1.4,
|
||||||
|
"GeneSmell": 1.2,
|
||||||
|
"GeneTouch": 1.0,
|
||||||
|
"GeneCamouflage": 0.35,
|
||||||
|
"GeneScent": 0.5,
|
||||||
|
"GeneNoise": 0.4,
|
||||||
|
"GeneBrainSize": 0.45,
|
||||||
|
"GeneInsulation": 0.55,
|
||||||
|
"GeneFurColor": 0.45,
|
||||||
|
"GeneMaturityAge": 90,
|
||||||
|
"GeneLifespan": 360,
|
||||||
|
"GeneBreedingSeason": 2,
|
||||||
|
"GeneGestationDays": 30,
|
||||||
|
"GeneLitterSize": 1,
|
||||||
|
"GeneHerbivory": 1.0,
|
||||||
|
"GeneToxinTolerance": 0.1,
|
||||||
|
"GeneFeedingStyle": 0.15,
|
||||||
|
"GeneSociability": 0.8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"defName": "Wolf",
|
||||||
|
"label": "pawn.wolf",
|
||||||
|
"kind": "animal",
|
||||||
|
"texture": "things/pawn/animal/wolf_timber/Wolf_Timber_east",
|
||||||
|
"maleTexture": "things/pawn/animal/wolf_timber/Wolf_Timber3_east",
|
||||||
|
"body": "Quadruped",
|
||||||
|
"diet": ["meat"],
|
||||||
|
"spawnPer1000Cells": 0.8,
|
||||||
|
"baseSpeed": 32,
|
||||||
|
"visionCells": 18,
|
||||||
|
"attackRangeCells": 1.3,
|
||||||
|
"attackIntervalSec": 1.4,
|
||||||
|
"attackDamage": 16,
|
||||||
|
"attackBleed": 0.16,
|
||||||
|
"attackBloodLoss": 0.06,
|
||||||
|
"baseMassKg": 45,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 1.6,
|
||||||
|
"GeneMetabolism": 1.1,
|
||||||
|
"GeneMoveSpeed": 1.4,
|
||||||
|
"GeneBloodVolume": 1.3,
|
||||||
|
"GeneVision": 1.5,
|
||||||
|
"GeneHearing": 1.5,
|
||||||
|
"GeneSmell": 1.8,
|
||||||
|
"GeneTouch": 1.0,
|
||||||
|
"GeneCamouflage": 0.2,
|
||||||
|
"GeneScent": 0.6,
|
||||||
|
"GeneNoise": 0.5,
|
||||||
|
"GeneBrainSize": 0.5,
|
||||||
|
"GeneInsulation": 0.6,
|
||||||
|
"GeneFurColor": 0.7,
|
||||||
|
"GeneMaturityAge": 70,
|
||||||
|
"GeneLifespan": 240,
|
||||||
|
"GeneBreedingSeason": 3,
|
||||||
|
"GeneGestationDays": 35,
|
||||||
|
"GeneLitterSize": 4,
|
||||||
|
"GeneCarnivory": 1.0,
|
||||||
|
"GeneFeedingStyle": 0.9,
|
||||||
|
"GeneSociability": 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"defName": "WildBoar",
|
||||||
|
"label": "pawn.boar",
|
||||||
|
"kind": "animal",
|
||||||
|
"texture": "things/pawn/animal/wildboar/WildBoar_east",
|
||||||
|
"body": "Quadruped",
|
||||||
|
"diet": ["plant", "meat"],
|
||||||
|
"spawnPer1000Cells": 1.0,
|
||||||
|
"baseSpeed": 26,
|
||||||
|
"visionCells": 13,
|
||||||
|
"attackRangeCells": 1.2,
|
||||||
|
"attackIntervalSec": 2.0,
|
||||||
|
"attackDamage": 9,
|
||||||
|
"attackBleed": 0.1,
|
||||||
|
"attackBloodLoss": 0.035,
|
||||||
|
"baseMassKg": 80,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 1.3,
|
||||||
|
"GeneMetabolism": 1.2,
|
||||||
|
"GeneMoveSpeed": 1.0,
|
||||||
|
"GeneBloodVolume": 1.2,
|
||||||
|
"GeneVision": 1.0,
|
||||||
|
"GeneHearing": 1.2,
|
||||||
|
"GeneSmell": 1.6,
|
||||||
|
"GeneTouch": 1.0,
|
||||||
|
"GeneCamouflage": 0.2,
|
||||||
|
"GeneScent": 0.7,
|
||||||
|
"GeneNoise": 0.7,
|
||||||
|
"GeneBrainSize": 0.42,
|
||||||
|
"GeneInsulation": 0.5,
|
||||||
|
"GeneFurColor": 0.85,
|
||||||
|
"GeneMaturityAge": 80,
|
||||||
|
"GeneLifespan": 280,
|
||||||
|
"GeneBreedingSeason": 0,
|
||||||
|
"GeneGestationDays": 28,
|
||||||
|
"GeneLitterSize": 5,
|
||||||
|
"GeneHerbivory": 0.4,
|
||||||
|
"GeneCarnivory": 0.2,
|
||||||
|
"GeneOmnivory": 0.85,
|
||||||
|
"GeneToxinTolerance": 0.25,
|
||||||
|
"GeneFeedingStyle": 0.55,
|
||||||
|
"GeneSociability": 0.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"defName": "Chicken",
|
||||||
|
"label": "pawn.chicken",
|
||||||
|
"kind": "animal",
|
||||||
|
"texture": "things/pawn/animal/chicken/Chicken_east",
|
||||||
|
"body": "Quadruped",
|
||||||
|
"diet": ["plant"],
|
||||||
|
"spawnPer1000Cells": 1.5,
|
||||||
|
"baseSpeed": 22,
|
||||||
|
"visionCells": 10,
|
||||||
|
"baseMassKg": 2.5,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 0.5,
|
||||||
|
"GeneMetabolism": 1.3,
|
||||||
|
"GeneMoveSpeed": 0.9,
|
||||||
|
"GeneBloodVolume": 0.7,
|
||||||
|
"GeneVision": 1.1,
|
||||||
|
"GeneHearing": 1.0,
|
||||||
|
"GeneSmell": 0.6,
|
||||||
|
"GeneTouch": 1.0,
|
||||||
|
"GeneCamouflage": 0.2,
|
||||||
|
"GeneScent": 0.4,
|
||||||
|
"GeneNoise": 0.6,
|
||||||
|
"GeneBrainSize": 0.3,
|
||||||
|
"GeneInsulation": 0.45,
|
||||||
|
"GeneFurColor": 0.6,
|
||||||
|
"GeneMaturityAge": 50,
|
||||||
|
"GeneLifespan": 180,
|
||||||
|
"GeneBreedingSeason": 1,
|
||||||
|
"GeneGestationDays": 12,
|
||||||
|
"GeneLitterSize": 6,
|
||||||
|
"GeneHerbivory": 0.8,
|
||||||
|
"GeneToxinTolerance": 0.15,
|
||||||
|
"GeneEggLaying": 1.0,
|
||||||
|
"GeneFeedingStyle": 0.2,
|
||||||
|
"GeneSociability": 0.55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"type": "Body",
|
||||||
|
// Анатомия видов (фаза A5): дерево частей/органов. Вклады в способности (capacities) по телу
|
||||||
|
// суммируются ≈1 у здоровой особи. maxHp умножается на размер тела особи при создании.
|
||||||
|
// Источников урона пока нет — части на полном HP, способности = 1; каркас под болезни/хищников.
|
||||||
|
"defs": [
|
||||||
|
{
|
||||||
|
"defName": "Quadruped",
|
||||||
|
"parts": [
|
||||||
|
{ "name": "torso", "coverage": 0.3, "maxHp": 40, "vital": true },
|
||||||
|
{ "name": "heart", "parent": "torso", "coverage": 0.02, "maxHp": 12, "vital": true,
|
||||||
|
"capacities": { "BloodPumping": 1.0 } },
|
||||||
|
{ "name": "lungLeft", "parent": "torso", "coverage": 0.03, "maxHp": 12,
|
||||||
|
"capacities": { "Breathing": 0.5 } },
|
||||||
|
{ "name": "lungRight", "parent": "torso", "coverage": 0.03, "maxHp": 12,
|
||||||
|
"capacities": { "Breathing": 0.5 } },
|
||||||
|
{ "name": "liver", "parent": "torso", "coverage": 0.03, "maxHp": 14, "vital": true,
|
||||||
|
"capacities": { "BloodFiltration": 0.4 } },
|
||||||
|
{ "name": "kidneyLeft", "parent": "torso", "coverage": 0.02, "maxHp": 10,
|
||||||
|
"capacities": { "BloodFiltration": 0.3 } },
|
||||||
|
{ "name": "kidneyRight", "parent": "torso", "coverage": 0.02, "maxHp": 10,
|
||||||
|
"capacities": { "BloodFiltration": 0.3 } },
|
||||||
|
{ "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
|
||||||
|
"capacities": { "Digestion": 1.0 } },
|
||||||
|
{ "name": "head", "coverage": 0.08, "maxHp": 25 },
|
||||||
|
{ "name": "brain", "parent": "head", "coverage": 0.02, "maxHp": 12, "vital": true,
|
||||||
|
"capacities": { "Consciousness": 1.0 } },
|
||||||
|
{ "name": "eyeLeft", "parent": "head", "coverage": 0.015, "maxHp": 8,
|
||||||
|
"capacities": { "Sight": 0.5 } },
|
||||||
|
{ "name": "eyeRight", "parent": "head", "coverage": 0.015, "maxHp": 8,
|
||||||
|
"capacities": { "Sight": 0.5 } },
|
||||||
|
{ "name": "earLeft", "parent": "head", "coverage": 0.01, "maxHp": 8,
|
||||||
|
"capacities": { "Hearing": 0.5 } },
|
||||||
|
{ "name": "earRight", "parent": "head", "coverage": 0.01, "maxHp": 8,
|
||||||
|
"capacities": { "Hearing": 0.5 } },
|
||||||
|
{ "name": "nose", "parent": "head", "coverage": 0.01, "maxHp": 8,
|
||||||
|
"capacities": { "Smell": 1.0 } },
|
||||||
|
{ "name": "skin", "coverage": 0.05, "maxHp": 20,
|
||||||
|
"capacities": { "Touch": 1.0 } },
|
||||||
|
{ "name": "jaw", "parent": "head", "coverage": 0.02, "maxHp": 10,
|
||||||
|
"capacities": { "Eating": 1.0, "Talking": 0.5 } },
|
||||||
|
{ "name": "tongue", "parent": "head", "coverage": 0.01, "maxHp": 8,
|
||||||
|
"capacities": { "Talking": 0.5 } },
|
||||||
|
{ "name": "legFrontLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
|
||||||
|
{ "name": "legFrontRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
|
||||||
|
{ "name": "legBackLeft", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } },
|
||||||
|
{ "name": "legBackRight", "coverage": 0.08, "maxHp": 15, "capacities": { "Moving": 0.25 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Гены животных: организм-агностичные гены зверя (их несёт геном животного). Тело/обмен/движение/
|
||||||
|
// чувства/мозг + пол (локус) и размножение. Признаки читают системы животных (рост, ИИ, здоровье).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneMaxBodySize", "parent": "BaseNumericGene", "label": "gene.bodySize",
|
||||||
|
"default": 1.0, "min": 0.1, "max": 4.0, "tags": ["animal", "body"],
|
||||||
|
"effects": { "bodySize": "value" } },
|
||||||
|
{ "defName": "GeneMetabolism", "parent": "BaseNumericGene", "label": "gene.metabolism",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "metabolism"],
|
||||||
|
"effects": { "metabolism": "value" } },
|
||||||
|
{ "defName": "GeneMoveSpeed", "parent": "BaseNumericGene", "label": "gene.moveSpeed",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "movement"],
|
||||||
|
"effects": { "moveSpeed": "value" } },
|
||||||
|
{ "defName": "GeneBloodVolume", "parent": "BaseNumericGene", "label": "gene.bloodVolume",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "health"],
|
||||||
|
"effects": { "bloodVolume": "value" } },
|
||||||
|
{ "defName": "GeneVision", "parent": "BaseNumericGene", "label": "gene.vision",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
|
||||||
|
"effects": { "vision": "value" } },
|
||||||
|
|
||||||
|
// --- Чувства (острота = множитель радиуса восприятия) и заметность (как видят/слышат/чуют ЭТУ особь) ---
|
||||||
|
// Острота работает вместе со способностью органа (глаза/уши/нос/кожа): ген × capacity × радиус чувства.
|
||||||
|
{ "defName": "GeneHearing", "parent": "BaseNumericGene", "label": "gene.hearing",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
|
||||||
|
"effects": { "hearing": "value" } },
|
||||||
|
{ "defName": "GeneSmell", "parent": "BaseNumericGene", "label": "gene.smell",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
|
||||||
|
"effects": { "smell": "value" } },
|
||||||
|
{ "defName": "GeneTouch", "parent": "BaseNumericGene", "label": "gene.touch",
|
||||||
|
"default": 1.0, "min": 0.2, "max": 3.0, "tags": ["animal", "senses"],
|
||||||
|
"effects": { "touch": "value" } },
|
||||||
|
// Заметность цели: камуфляж снижает обнаружение зрением (1−camouflage), запах повышает обонянием,
|
||||||
|
// шум при движении повышает обнаружение слухом. См. формулу радиуса в системе восприятия.
|
||||||
|
{ "defName": "GeneCamouflage", "parent": "BaseNumericGene", "label": "gene.camouflage",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
|
||||||
|
"effects": { "camouflage": "value" } },
|
||||||
|
{ "defName": "GeneScent", "parent": "BaseNumericGene", "label": "gene.scent",
|
||||||
|
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
|
||||||
|
"effects": { "scent": "value" } },
|
||||||
|
{ "defName": "GeneNoise", "parent": "BaseNumericGene", "label": "gene.noise",
|
||||||
|
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "stealth"],
|
||||||
|
"effects": { "noise": "value" } },
|
||||||
|
{ "defName": "GeneBrainSize", "parent": "BaseNumericGene", "label": "gene.brainSize",
|
||||||
|
"default": 0.45, "min": 0.0, "max": 1.0, "spread": 0.03, "tags": ["animal", "brain"],
|
||||||
|
"effects": { "brainSize": "value" } },
|
||||||
|
{ "defName": "GeneInsulation", "parent": "BaseNumericGene", "label": "gene.insulation",
|
||||||
|
"default": 0.5, "min": 0.0, "max": 1.0, "tags": ["animal", "temperature"],
|
||||||
|
"effects": { "insulation": "value" } },
|
||||||
|
{ "defName": "GeneFurColor", "parent": "BaseNumericGene", "label": "gene.furColor",
|
||||||
|
"default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.06, "tags": ["animal", "morphology", "color"],
|
||||||
|
"effects": { "furHue": "value" } },
|
||||||
|
{ "defName": "GeneMaturityAge", "parent": "BaseNumericGene", "label": "gene.maturityAge",
|
||||||
|
"default": 90, "min": 1, "max": 400, "tags": ["animal", "lifecycle"],
|
||||||
|
"effects": { "maturityAge": "value" } },
|
||||||
|
|
||||||
|
// --- Диета как ГЕНЫ (хищничество/травоядство/всеядство) ---
|
||||||
|
// Каждый ген выражает свой признак [0..1]; рацион выводится порогами: ест растения, если
|
||||||
|
// max(herbivory, omnivory) ≥ порога; ест мясо (охота/падаль), если max(carnivory, omnivory) ≥ порога.
|
||||||
|
// Поэтому всеядность — отдельный ген, который включает ОБА источника пищи (генералист), а
|
||||||
|
// специалисты задаются высоким herbivory ЛИБО carnivory. См. AnimalFactory.Diet.
|
||||||
|
{ "defName": "GeneHerbivory", "parent": "BaseNumericGene", "label": "gene.herbivory",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||||
|
"effects": { "herbivory": "value" } },
|
||||||
|
{ "defName": "GeneCarnivory", "parent": "BaseNumericGene", "label": "gene.carnivory",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||||
|
"effects": { "carnivory": "value" } },
|
||||||
|
{ "defName": "GeneOmnivory", "parent": "BaseNumericGene", "label": "gene.omnivory",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||||
|
"effects": { "omnivory": "value" } },
|
||||||
|
|
||||||
|
// Устойчивость к растительным ядам (коэволюция): снижает дозу отравления при поедании токсичных
|
||||||
|
// растений (effectivePoison = toxicity × (1 − toxinTolerance), см. AnimalActionSystem). Растёт под
|
||||||
|
// давлением ядовитого корма — другая сторона гонки вооружений с GeneToxicity растений.
|
||||||
|
{ "defName": "GeneToxinTolerance", "parent": "BaseNumericGene", "label": "gene.toxinTolerance",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["animal", "diet"],
|
||||||
|
"effects": { "toxinTolerance": "value" } },
|
||||||
|
|
||||||
|
// Стиль питания [0..1]: 0 — пастьба (graze, частые мелкие приёмы, держит сытость высокой),
|
||||||
|
// 1 — обжорство (gorge, ищет еду лишь при сильном голоде и ест до полного — редкие крупные приёмы,
|
||||||
|
// как у хищников). Задаёт пороги начала/конца кормёжки в AnimalDecisionSystem. Эволюционирует.
|
||||||
|
{ "defName": "GeneFeedingStyle", "parent": "BaseNumericGene", "label": "gene.feedingStyle",
|
||||||
|
"default": 0.5, "min": 0.0, "max": 1.0, "spread": 0.05, "tags": ["animal", "diet"],
|
||||||
|
"effects": { "feedingStyle": "value" } },
|
||||||
|
|
||||||
|
// Пол как локус (фаза A3): аллели {X=0, Y=1}; самка XX, самец XY. Эффекта нет — пол читается из
|
||||||
|
// самих аллелей (наличие Y), не из выраженного значения. mutationChance 0 (X не мутирует в Y).
|
||||||
|
// Генерация особи-основателя задаёт пол явно (XX или XY), чтобы не возник невозможный YY.
|
||||||
|
{ "defName": "GeneSex", "kind": "Discrete", "label": "gene.sex",
|
||||||
|
"variants": 2, "variantWeights": [0.5, 0.5], "mutationChance": 0, "tags": ["animal", "sex"] },
|
||||||
|
|
||||||
|
// Социальность [0..1]: стадные/стайные виды держатся вместе (когезия при блуждании) и спокойнее в
|
||||||
|
// группе («безопасность в числе» снижает дальность реакции на хищника). См. AnimalDecisionSystem.
|
||||||
|
{ "defName": "GeneSociability", "parent": "BaseNumericGene", "label": "gene.sociability",
|
||||||
|
"default": 0.3, "min": 0.0, "max": 1.0, "spread": 0.05, "tags": ["animal", "behaviour"],
|
||||||
|
"effects": { "sociability": "value" } },
|
||||||
|
|
||||||
|
// Тип рождения (цикл зачатия): живородящие (eggLaying < 0.5) вынашивают и рожают живых детёнышей;
|
||||||
|
// яйцекладущие (≥ 0.5) после вынашивания ОТКЛАДЫВАЮТ яйца-сущности, которые инкубируются и
|
||||||
|
// вылупляются (см. AnimalPregnancySystem/EggSystem). Признак фиксирован по виду (spread/mutation 0).
|
||||||
|
{ "defName": "GeneEggLaying", "parent": "BaseNumericGene", "label": "gene.eggLaying",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "eggLaying": "value" } },
|
||||||
|
|
||||||
|
// Размножение (фаза A4): сезон гона фиксирован по виду (0 весна … 3 зима), срок вынашивания и помёт.
|
||||||
|
{ "defName": "GeneBreedingSeason", "parent": "BaseNumericGene", "label": "gene.breedingSeason",
|
||||||
|
"default": 2, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "breedingSeason": "value" } },
|
||||||
|
{ "defName": "GeneGestationDays", "parent": "BaseNumericGene", "label": "gene.gestationDays",
|
||||||
|
"default": 30, "min": 1, "max": 200, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "gestationDays": "value" } },
|
||||||
|
{ "defName": "GeneLitterSize", "parent": "BaseNumericGene", "label": "gene.litterSize",
|
||||||
|
"default": 1, "min": 1, "max": 12, "tags": ["animal", "reproduction"],
|
||||||
|
"effects": { "litterSize": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,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",
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"type": "Hediff",
|
||||||
|
// Хедифы — состояния организма (фаза A4: лёгкий каркас). Гон (Rut) навешивается сезонно на взрослых
|
||||||
|
// и поднимает половое влечение. Раны/болезни/возрастные эффекты со стадиями придут в A5/A6.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "Rut", "label": "hediff.rut" },
|
||||||
|
|
||||||
|
// Болезнь (фаза A6): прогрессирует, параллельно растёт иммунитет (гонка). Обычно иммунитет
|
||||||
|
// успевает победить (~3 дня), но больной зверь медленнее и слабее; слабые/невезучие гибнут.
|
||||||
|
{
|
||||||
|
"defName": "Fever", "label": "hediff.fever",
|
||||||
|
"initialSeverity": 0.05, "severityPerDay": 0.25, "immunityPerDay": 0.32,
|
||||||
|
"lethalSeverity": 1.0, "pain": 0.3, "ambientPerDay": 0.03,
|
||||||
|
"capMods": { "Moving": 0.6, "Consciousness": 0.85, "Digestion": 0.8 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Рана/кровотечение (предатор-кластер): первый травматический урон. Не прогрессирует сама
|
||||||
|
// (severityPerDay 0), но immunityPerDay её «сворачивает» (рана заживает ~за 2 дня). Пока активна —
|
||||||
|
// теряется кровь (bloodLossPerDay × тяжесть); кровь на нуле = смерть. Боль и капмоды от тяжёлых ран.
|
||||||
|
// Каждый укус усиливает тяжесть и сбрасывает заживление (см. HealthState.Intensify).
|
||||||
|
{
|
||||||
|
"defName": "Bleeding", "label": "hediff.bleeding",
|
||||||
|
"initialSeverity": 0.3, "immunityPerDay": 0.5, "bloodLossPerDay": 0.8,
|
||||||
|
"pain": 0.25, "capMods": { "Moving": 0.92, "Consciousness": 0.95 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Отравление растительным ядом (коэволюция): сама не прогрессирует (severityPerDay 0) — тяжесть
|
||||||
|
// копится при поедании токсичных растений (HealthState.Intensify в AnimalActionSystem). Иммунитет
|
||||||
|
// её рассасывает (~1.5 дня), но если зверь ест яд быстрее, чем выводит, тяжесть доходит до 1 → смерть.
|
||||||
|
// Это селективное давление: ядовитый корм опасен для травоядных с низкой устойчивостью.
|
||||||
|
{
|
||||||
|
"defName": "Poisoned", "label": "hediff.poisoned",
|
||||||
|
"initialSeverity": 0.0, "immunityPerDay": 0.65, "lethalSeverity": 1.0, "pain": 0.2,
|
||||||
|
"capMods": { "Moving": 0.85, "Consciousness": 0.9, "Digestion": 0.7 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Терморегуляция (как Poisoned-паттерн): сами не прогрессируют — тяжесть копится, пока зверю
|
||||||
|
// холодно/жарко (см. AnimalHealthSystem, зависит от GeneInsulation + размера тела), а в комфорте
|
||||||
|
// immunityPerDay их рассасывает. Тяжесть 1 = смерть от переохлаждения/перегрева.
|
||||||
|
{
|
||||||
|
"defName": "Hypothermia", "label": "hediff.hypothermia",
|
||||||
|
"initialSeverity": 0.0, "immunityPerDay": 0.5, "lethalSeverity": 1.0, "pain": 0.15,
|
||||||
|
"capMods": { "Moving": 0.7, "Consciousness": 0.8 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defName": "Heatstroke", "label": "hediff.heatstroke",
|
||||||
|
"initialSeverity": 0.0, "immunityPerDay": 0.5, "lethalSeverity": 1.0, "pain": 0.15,
|
||||||
|
"capMods": { "Moving": 0.75, "Consciousness": 0.8 }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Заражение раны (предатор/шипы → кровотечение → инфекция): прогрессирует, иммунитет (× фильтрация
|
||||||
|
// крови) душит. Слабая фильтрация/много ран → выше риск и хуже исход.
|
||||||
|
{
|
||||||
|
"defName": "Infection", "label": "hediff.infection",
|
||||||
|
"initialSeverity": 0.08, "severityPerDay": 0.2, "immunityPerDay": 0.28,
|
||||||
|
"lethalSeverity": 1.0, "pain": 0.25,
|
||||||
|
"capMods": { "Moving": 0.8, "Consciousness": 0.85, "BloodFiltration": 0.8 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"type": "Need",
|
||||||
|
// Нужды как данные (фаза A2, рефактор расширяемости): добавить нужду виду = добавить запись здесь.
|
||||||
|
// kind=deplete убывает и утоляется действием; kind=drive растёт под хедифом (гон) и сбрасывается им.
|
||||||
|
// action — id поведения-исполнителя (реестр в коде). minBrain — гейт интеллектом (активен с A7):
|
||||||
|
// нужда, чей minBrain выше эффективного интеллекта особи (brainSize × Consciousness), в выбор ИИ не
|
||||||
|
// входит. Пороги — тиры мозга: 0.0 голод-рефлекс, 0.25 жажда (низший позвоночный), 0.40 сон/секс
|
||||||
|
// (зверь). У оленя мозг ~0.45: здоров — весь набор; повреждение мозга/болезнь роняют сознание →
|
||||||
|
// эффективный интеллект падает → первыми отключаются сон/спаривание (обратная связь A7).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "Hunger", "label": "need.hunger", "kind": "deplete", "action": "Eat",
|
||||||
|
"decayPerDay": 0.55, "feedPerDay": 4, "lethal": true, "minBrain": 0.0 },
|
||||||
|
{ "defName": "Thirst", "label": "need.thirst", "kind": "deplete", "action": "Drink",
|
||||||
|
"decayPerDay": 0.9, "feedPerDay": 8, "lethal": true, "minBrain": 0.25 },
|
||||||
|
{ "defName": "Rest", "label": "need.rest", "kind": "deplete", "action": "Sleep",
|
||||||
|
"decayPerDay": 0.7, "feedPerDay": 3, "lethal": false, "minBrain": 0.40 },
|
||||||
|
{ "defName": "Mating", "label": "need.mating", "kind": "drive", "action": "Mate",
|
||||||
|
"decayPerDay": 1.0, "risePerDay": 1.2, "risesUnder": "Rut", "minBrain": 0.40 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
|
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
|
||||||
// количество задаётся генами (harvestAmount / fruitYield).
|
// количество задаётся генами (harvestAmount / fruitYield).
|
||||||
"defs": [
|
"defs": [
|
||||||
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
|
{ "defName": "ProductWood", "label": "product.wood", "kind": "material", "massKg": 1.0 },
|
||||||
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
|
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material", "massKg": 0.1 },
|
||||||
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material" },
|
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material", "massKg": 0.2 },
|
||||||
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food", "massKg": 0.05 },
|
||||||
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" },
|
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food", "massKg": 0.05 },
|
||||||
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" }
|
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food", "massKg": 0.1 },
|
||||||
|
|
||||||
|
// Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже.
|
||||||
|
{ "defName": "ProductMeat", "label": "product.meat", "kind": "food", "massKg": 0.5 },
|
||||||
|
{ "defName": "ProductBone", "label": "product.bone", "kind": "material", "massKg": 0.3 },
|
||||||
|
{ "defName": "ProductLeather", "label": "product.leather", "kind": "material", "massKg": 0.4 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{ "chance": 0.20, "options": ["Strawberry", "Raspberry"] },
|
{ "chance": 0.20, "options": ["Strawberry", "Raspberry"] },
|
||||||
{ "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] }
|
{ "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] }
|
||||||
] },
|
] },
|
||||||
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 1.01, "color": [44, 110, 50],
|
||||||
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
||||||
"scatter": [
|
"scatter": [
|
||||||
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA",
|
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA",
|
||||||
@@ -28,8 +28,6 @@
|
|||||||
{ "chance": 0.16, "options": ["BushA", "BushB", "BushC"] },
|
{ "chance": 0.16, "options": ["BushA", "BushB", "BushC"] },
|
||||||
{ "chance": 0.14, "options": ["Strawberry", "Raspberry"] },
|
{ "chance": 0.14, "options": ["Strawberry", "Raspberry"] },
|
||||||
{ "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] }
|
{ "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] }
|
||||||
] },
|
] }
|
||||||
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
|
|
||||||
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "Thought",
|
||||||
|
// Мысли настроения (фаза A8): событие вешает на зверя временный сдвиг настроения, затухающий за
|
||||||
|
// durationDays. Триггер события — код (реестр AnimalThoughts), сила/длительность/метка — данные.
|
||||||
|
// Настроение есть только у тира 4+ (мозг ≥ 0.40); непрерывный фон (голод/жажда/усталость/боль)
|
||||||
|
// считается отдельно, мысли — поверх него. Негативные событийные мысли (потеря детёныша) придут с
|
||||||
|
// родительской опекой (горизонт) — пока горе идёт через непрерывный фон.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GaveBirth", "label": "thought.gaveBirth", "moodOffset": 0.25, "durationDays": 3 },
|
||||||
|
{ "defName": "Mated", "label": "thought.mated", "moodOffset": 0.15, "durationDays": 1.5 },
|
||||||
|
{ "defName": "QuenchedThirst", "label": "thought.quenchedThirst", "moodOffset": 0.1, "durationDays": 0.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"type": "WorldPreset",
|
"type": "WorldPreset",
|
||||||
"defs": [
|
"defs": [
|
||||||
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 80, "height": 50, "population": 40 },
|
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 128, "height": 80, "population": 60 },
|
||||||
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 120, "height": 70, "population": 80 },
|
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 200, "height": 120, "population": 110 },
|
||||||
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 180, "height": 110, "population": 140 }
|
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 300, "height": 180, "population": 180 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,21 @@
|
|||||||
"season.autumn": "autumn",
|
"season.autumn": "autumn",
|
||||||
"season.winter": "winter",
|
"season.winter": "winter",
|
||||||
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
|
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
|
||||||
|
"hud.inspector": "F1 — inspector",
|
||||||
|
"hud.spawner": "F9 — spawner",
|
||||||
"inspect.tab.overview": "Overview",
|
"inspect.tab.overview": "Overview",
|
||||||
"inspect.tab.genes": "Genes",
|
"inspect.tab.genes": "Genes",
|
||||||
"inspect.tab.products": "Products",
|
"inspect.tab.products": "Products",
|
||||||
|
"inspect.tab.needs": "Needs",
|
||||||
|
"inspect.tab.skills": "Skills",
|
||||||
|
"inspect.tab.health": "Health",
|
||||||
|
"inspect.tab.mood": "Mood",
|
||||||
"inspect.stage": "Stage: {0} ({1}/{2})",
|
"inspect.stage": "Stage: {0} ({1}/{2})",
|
||||||
"inspect.growing": "Growth: {0:0}% to next stage",
|
"inspect.growing": "Growth: {0:0}% to next stage",
|
||||||
"inspect.mature": "Fully grown",
|
"inspect.mature": "Fully grown",
|
||||||
"inspect.age": "Age: {0:0.0} d · lives to {1:0}",
|
"inspect.age": "Age: {0:0.0} d · lives to {1:0}",
|
||||||
|
"inspect.mass": "Weight: {0:0.#} kg · carries up to {1:0.#} kg",
|
||||||
|
"inspect.plantmass": "Weight: {0:0.##} kg",
|
||||||
"inspect.state": "State: {0}",
|
"inspect.state": "State: {0}",
|
||||||
"inspect.state.growing": "growing",
|
"inspect.state.growing": "growing",
|
||||||
"inspect.state.mature": "mature",
|
"inspect.state.mature": "mature",
|
||||||
@@ -21,16 +29,67 @@
|
|||||||
"inspect.state.dormanthot": "dormant — too hot",
|
"inspect.state.dormanthot": "dormant — too hot",
|
||||||
"inspect.stress": "Stress: {0:0.0} d to death",
|
"inspect.stress": "Stress: {0:0.0} d to death",
|
||||||
"inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
|
"inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
|
||||||
"inspect.genes": "— Genes —",
|
"inspect.genes": "Genes",
|
||||||
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d",
|
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d",
|
||||||
"inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}",
|
"inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}",
|
||||||
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
|
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
|
||||||
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
|
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
|
||||||
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
|
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
|
||||||
|
"inspect.gene.defense": "Defense: toxin {0:0.00} · thorns {1:0.00}",
|
||||||
"inspect.gene.variant": "Morph: recessive variant",
|
"inspect.gene.variant": "Morph: recessive variant",
|
||||||
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
|
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
|
||||||
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
|
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
|
||||||
"inspect.barren": "Bears no fruit",
|
"inspect.barren": "Bears no fruit",
|
||||||
|
"inspect.sex.male": "male",
|
||||||
|
"inspect.sex.female": "female",
|
||||||
|
"inspect.animal.sex": "Sex: {0} · generation {1}",
|
||||||
|
"inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth",
|
||||||
|
"inspect.needline": "{0}: {1:0}%",
|
||||||
|
"inspect.skillline": "{0}: {1:0}% {2}",
|
||||||
|
"skill.foraging": "foraging",
|
||||||
|
"skill.hunting": "hunting",
|
||||||
|
"skill.evasion": "evasion",
|
||||||
|
"inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%",
|
||||||
|
"inspect.health.vitals": "Vitals",
|
||||||
|
"inspect.vital.blood": "blood",
|
||||||
|
"inspect.vital.pain": "pain",
|
||||||
|
"inspect.health.caps": "Capacities",
|
||||||
|
"inspect.health.cap": "{0}: {1:0}%",
|
||||||
|
"inspect.health.hediffs": "Conditions",
|
||||||
|
"inspect.health.hediffline": "{0}: severity {1:0}%",
|
||||||
|
"inspect.health.none": "Healthy, no wounds or illness",
|
||||||
|
"inspect.mood.value": "Mood: {0:0}%",
|
||||||
|
"inspect.mood.none": "No mood (primitive brain)",
|
||||||
|
"inspect.mood.thoughts": "Thoughts",
|
||||||
|
"inspect.mood.thoughtline": "{0}: {1}",
|
||||||
|
"inspect.mood.calm": "Calm, nothing on its mind",
|
||||||
|
"animalstage.baby": "baby",
|
||||||
|
"animalstage.juvenile": "juvenile",
|
||||||
|
"animalstage.adult": "adult",
|
||||||
|
"animalstage.senior": "senior",
|
||||||
|
"need.hunger": "hunger",
|
||||||
|
"need.thirst": "thirst",
|
||||||
|
"need.rest": "rest",
|
||||||
|
"need.mating": "mating",
|
||||||
|
"cap.consciousness": "consciousness",
|
||||||
|
"cap.moving": "moving",
|
||||||
|
"cap.sight": "sight",
|
||||||
|
"cap.hearing": "hearing",
|
||||||
|
"cap.smell": "smell",
|
||||||
|
"cap.touch": "touch",
|
||||||
|
"cap.talking": "talking",
|
||||||
|
"cap.eating": "eating",
|
||||||
|
"cap.breathing": "breathing",
|
||||||
|
"cap.bloodpumping": "blood pumping",
|
||||||
|
"cap.bloodfiltration": "blood filtration",
|
||||||
|
"cap.digestion": "digestion",
|
||||||
|
"hediff.rut": "rut",
|
||||||
|
"hediff.fever": "fever",
|
||||||
|
"hediff.bleeding": "bleeding",
|
||||||
|
"hediff.poisoned": "poisoned",
|
||||||
|
"hediff.hypothermia": "hypothermia",
|
||||||
|
"hediff.heatstroke": "heatstroke",
|
||||||
|
"hediff.infection": "infection",
|
||||||
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
||||||
"hud.paused": "PAUSED",
|
"hud.paused": "PAUSED",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
@@ -40,6 +99,7 @@
|
|||||||
"menu.settings": "Settings",
|
"menu.settings": "Settings",
|
||||||
"menu.credits": "Credits",
|
"menu.credits": "Credits",
|
||||||
"menu.quit": "Quit",
|
"menu.quit": "Quit",
|
||||||
|
"loading.world": "Generating world",
|
||||||
"newworld.title": "New World",
|
"newworld.title": "New World",
|
||||||
"newworld.name": "Name",
|
"newworld.name": "Name",
|
||||||
"newworld.defaultname": "New World",
|
"newworld.defaultname": "New World",
|
||||||
@@ -61,6 +121,18 @@
|
|||||||
"settings.resolution": "Resolution",
|
"settings.resolution": "Resolution",
|
||||||
"settings.volume": "Volume",
|
"settings.volume": "Volume",
|
||||||
"settings.uiscale": "UI scale",
|
"settings.uiscale": "UI scale",
|
||||||
|
"settings.devmode": "Developer mode",
|
||||||
|
"settings.on": "On",
|
||||||
|
"settings.off": "Off",
|
||||||
|
"dev.title": "Dev spawner",
|
||||||
|
"dev.hint": "Pick an entry · F9 to hide",
|
||||||
|
"dev.armed": "In hand: {0} · LMB to spawn, RMB to clear",
|
||||||
|
"dev.tools": "Tools",
|
||||||
|
"dev.damage": "Injure selected",
|
||||||
|
"dev.kill": "Kill selected",
|
||||||
|
"dev.infect": "Infect selected",
|
||||||
|
"dev.animals": "Animals",
|
||||||
|
"dev.plants": "Plants",
|
||||||
"settings.apply": "Apply",
|
"settings.apply": "Apply",
|
||||||
"settings.back": "Back",
|
"settings.back": "Back",
|
||||||
"pause.title": "Paused",
|
"pause.title": "Paused",
|
||||||
@@ -70,7 +142,7 @@
|
|||||||
"pause.mainmenu": "Main menu",
|
"pause.mainmenu": "Main menu",
|
||||||
"pause.quit": "Quit",
|
"pause.quit": "Quit",
|
||||||
"pause.saved": "Saved: {0}",
|
"pause.saved": "Saved: {0}",
|
||||||
"speed.pause": "⏸",
|
"speed.pause": "Pause",
|
||||||
"credits.title": "Credits",
|
"credits.title": "Credits",
|
||||||
"credits.author": "mrleo1nid",
|
"credits.author": "mrleo1nid",
|
||||||
"credits.role": "Concept, code, mrgameeng engine",
|
"credits.role": "Concept, code, mrgameeng engine",
|
||||||
@@ -83,7 +155,6 @@
|
|||||||
"terrain.sand": "sand",
|
"terrain.sand": "sand",
|
||||||
"terrain.grass": "grass",
|
"terrain.grass": "grass",
|
||||||
"terrain.forest": "forest",
|
"terrain.forest": "forest",
|
||||||
"terrain.mountain": "mountains",
|
|
||||||
"plant.oak": "oak",
|
"plant.oak": "oak",
|
||||||
"plant.birch": "birch",
|
"plant.birch": "birch",
|
||||||
"plant.pine": "pine",
|
"plant.pine": "pine",
|
||||||
@@ -121,8 +192,49 @@
|
|||||||
"pawn.hare": "hare",
|
"pawn.hare": "hare",
|
||||||
"pawn.boar": "boar",
|
"pawn.boar": "boar",
|
||||||
"pawn.wolf": "wolf",
|
"pawn.wolf": "wolf",
|
||||||
|
"pawn.chicken": "chicken",
|
||||||
"pawn.muffalo": "muffalo",
|
"pawn.muffalo": "muffalo",
|
||||||
"pawn.squirrel": "squirrel",
|
"pawn.squirrel": "squirrel",
|
||||||
|
"thought.gaveBirth": "gave birth",
|
||||||
|
"thought.mated": "mated",
|
||||||
|
"thought.quenchedThirst": "drank its fill",
|
||||||
|
"gene.feedingStyle": "feeding style",
|
||||||
|
"inspect.animal.feeding": "Feeding: {0}",
|
||||||
|
"feeding.gorge": "gorger (rare, to the full)",
|
||||||
|
"feeding.graze": "grazer (often, little by little)",
|
||||||
|
"feeding.mixed": "moderate",
|
||||||
|
"inspect.tab.log": "Log",
|
||||||
|
"log.empty": "No history yet",
|
||||||
|
"log.stamp": "D{0} {1:00}:{2:00}",
|
||||||
|
"hud.events": "Events",
|
||||||
|
"hud.perf": "Perf",
|
||||||
|
"events.title": "Event log",
|
||||||
|
"events.empty": "Nothing has happened yet",
|
||||||
|
"events.hint": "Double-click — jump to the creature",
|
||||||
|
"perf.title": "Performance",
|
||||||
|
"perf.fps": "{0} FPS · {1:0.0} ms/frame",
|
||||||
|
"perf.root": "{0}: {1:0.000} ms · systems {2}",
|
||||||
|
"perf.systemline": "{0} {1:0.000} ms {2:0}%",
|
||||||
|
"perf.hint": "Refreshes ~5/s · costliest systems shown",
|
||||||
|
"event.mated": "Pair: {0} + {1}",
|
||||||
|
"event.pregnant": "{0}: pregnant",
|
||||||
|
"event.birth": "{0}: birth ×{1}",
|
||||||
|
"event.hatch": "{0}: hatched ×{1}",
|
||||||
|
"event.hunt": "Hunt: {0} → {1}",
|
||||||
|
"event.injured": "Wound: {0} → {1}",
|
||||||
|
"event.struck": "Hit: {0} → {1} ({2:0})",
|
||||||
|
"event.tookhit": "Hit: {1} → {0} ({2:0})",
|
||||||
|
"event.killed": "Kill: {0} → {1}",
|
||||||
|
"event.ate": "{0} is feeding",
|
||||||
|
"event.drank": "{0} is drinking",
|
||||||
|
"event.rest": "{0} is resting",
|
||||||
|
"event.dig": "{0} digs for food",
|
||||||
|
"event.wallow": "{0} wallows in mud",
|
||||||
|
"event.fled": "{0} is fleeing",
|
||||||
|
"event.grew": "{0}: new stage — {1}",
|
||||||
|
"event.death.starve": "Death: {0} (hunger/thirst)",
|
||||||
|
"event.death.age": "Death: {0} (old age)",
|
||||||
|
"event.death.disease": "Death: {0} (disease)",
|
||||||
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
||||||
"net.connecting": "connecting…",
|
"net.connecting": "connecting…",
|
||||||
"net.connected": "connected",
|
"net.connected": "connected",
|
||||||
|
|||||||
@@ -7,13 +7,21 @@
|
|||||||
"season.autumn": "осень",
|
"season.autumn": "осень",
|
||||||
"season.winter": "зима",
|
"season.winter": "зима",
|
||||||
"hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
|
"hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
|
||||||
|
"hud.inspector": "F1 — инспектор",
|
||||||
|
"hud.spawner": "F9 — спавнер",
|
||||||
"inspect.tab.overview": "Обзор",
|
"inspect.tab.overview": "Обзор",
|
||||||
"inspect.tab.genes": "Гены",
|
"inspect.tab.genes": "Гены",
|
||||||
"inspect.tab.products": "Продукты",
|
"inspect.tab.products": "Продукты",
|
||||||
|
"inspect.tab.needs": "Нужды",
|
||||||
|
"inspect.tab.skills": "Навыки",
|
||||||
|
"inspect.tab.health": "Здоровье",
|
||||||
|
"inspect.tab.mood": "Настроение",
|
||||||
"inspect.stage": "Стадия: {0} ({1}/{2})",
|
"inspect.stage": "Стадия: {0} ({1}/{2})",
|
||||||
"inspect.growing": "Рост: {0:0}% до следующей стадии",
|
"inspect.growing": "Рост: {0:0}% до следующей стадии",
|
||||||
"inspect.mature": "Полностью выросло",
|
"inspect.mature": "Полностью выросло",
|
||||||
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
|
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
|
||||||
|
"inspect.mass": "Вес: {0:0.#} кг · несёт до {1:0.#} кг",
|
||||||
|
"inspect.plantmass": "Вес: {0:0.##} кг",
|
||||||
"inspect.state": "Состояние: {0}",
|
"inspect.state": "Состояние: {0}",
|
||||||
"inspect.state.growing": "растёт",
|
"inspect.state.growing": "растёт",
|
||||||
"inspect.state.mature": "созрело",
|
"inspect.state.mature": "созрело",
|
||||||
@@ -21,16 +29,67 @@
|
|||||||
"inspect.state.dormanthot": "покой — слишком жарко",
|
"inspect.state.dormanthot": "покой — слишком жарко",
|
||||||
"inspect.stress": "Стресс: {0:0.0} дн до гибели",
|
"inspect.stress": "Стресс: {0:0.0} дн до гибели",
|
||||||
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
|
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
|
||||||
"inspect.genes": "— Гены —",
|
"inspect.genes": "Гены",
|
||||||
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
|
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
|
||||||
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
|
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
|
||||||
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
|
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
|
||||||
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
|
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
|
||||||
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
|
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
|
||||||
|
"inspect.gene.defense": "Защита: яд {0:0.00} · шипы {1:0.00}",
|
||||||
"inspect.gene.variant": "Морфа: рецессивный вариант",
|
"inspect.gene.variant": "Морфа: рецессивный вариант",
|
||||||
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
|
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
|
||||||
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
|
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
|
||||||
"inspect.barren": "Не плодоносит",
|
"inspect.barren": "Не плодоносит",
|
||||||
|
"inspect.sex.male": "самец",
|
||||||
|
"inspect.sex.female": "самка",
|
||||||
|
"inspect.animal.sex": "Пол: {0} · поколение {1}",
|
||||||
|
"inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов",
|
||||||
|
"inspect.needline": "{0}: {1:0}%",
|
||||||
|
"inspect.skillline": "{0}: {1:0}% {2}",
|
||||||
|
"skill.foraging": "добыча корма",
|
||||||
|
"skill.hunting": "охота",
|
||||||
|
"skill.evasion": "уклонение",
|
||||||
|
"inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%",
|
||||||
|
"inspect.health.vitals": "Жизненные показатели",
|
||||||
|
"inspect.vital.blood": "кровь",
|
||||||
|
"inspect.vital.pain": "боль",
|
||||||
|
"inspect.health.caps": "Способности",
|
||||||
|
"inspect.health.cap": "{0}: {1:0}%",
|
||||||
|
"inspect.health.hediffs": "Состояния",
|
||||||
|
"inspect.health.hediffline": "{0}: тяжесть {1:0}%",
|
||||||
|
"inspect.health.none": "Здоров, ран и болезней нет",
|
||||||
|
"inspect.mood.value": "Настроение: {0:0}%",
|
||||||
|
"inspect.mood.none": "Настроения нет (примитивный мозг)",
|
||||||
|
"inspect.mood.thoughts": "Мысли",
|
||||||
|
"inspect.mood.thoughtline": "{0}: {1}",
|
||||||
|
"inspect.mood.calm": "Спокоен, особых мыслей нет",
|
||||||
|
"animalstage.baby": "детёныш",
|
||||||
|
"animalstage.juvenile": "подросток",
|
||||||
|
"animalstage.adult": "взрослый",
|
||||||
|
"animalstage.senior": "старый",
|
||||||
|
"need.hunger": "голод",
|
||||||
|
"need.thirst": "жажда",
|
||||||
|
"need.rest": "отдых",
|
||||||
|
"need.mating": "влечение",
|
||||||
|
"cap.consciousness": "сознание",
|
||||||
|
"cap.moving": "движение",
|
||||||
|
"cap.sight": "зрение",
|
||||||
|
"cap.hearing": "слух",
|
||||||
|
"cap.smell": "обоняние",
|
||||||
|
"cap.touch": "осязание",
|
||||||
|
"cap.talking": "речь",
|
||||||
|
"cap.eating": "питание",
|
||||||
|
"cap.breathing": "дыхание",
|
||||||
|
"cap.bloodpumping": "кровоснабжение",
|
||||||
|
"cap.bloodfiltration": "фильтрация крови",
|
||||||
|
"cap.digestion": "пищеварение",
|
||||||
|
"hediff.rut": "гон",
|
||||||
|
"hediff.fever": "лихорадка",
|
||||||
|
"hediff.bleeding": "кровотечение",
|
||||||
|
"hediff.poisoned": "отравление",
|
||||||
|
"hediff.hypothermia": "переохлаждение",
|
||||||
|
"hediff.heatstroke": "тепловой удар",
|
||||||
|
"hediff.infection": "заражение",
|
||||||
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
||||||
"hud.paused": "ПАУЗА",
|
"hud.paused": "ПАУЗА",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
@@ -40,6 +99,7 @@
|
|||||||
"menu.settings": "Настройки",
|
"menu.settings": "Настройки",
|
||||||
"menu.credits": "Авторы",
|
"menu.credits": "Авторы",
|
||||||
"menu.quit": "Выход",
|
"menu.quit": "Выход",
|
||||||
|
"loading.world": "Создание мира",
|
||||||
"newworld.title": "Новый мир",
|
"newworld.title": "Новый мир",
|
||||||
"newworld.name": "Название",
|
"newworld.name": "Название",
|
||||||
"newworld.defaultname": "Новый мир",
|
"newworld.defaultname": "Новый мир",
|
||||||
@@ -61,6 +121,18 @@
|
|||||||
"settings.resolution": "Разрешение",
|
"settings.resolution": "Разрешение",
|
||||||
"settings.volume": "Громкость",
|
"settings.volume": "Громкость",
|
||||||
"settings.uiscale": "Масштаб UI",
|
"settings.uiscale": "Масштаб UI",
|
||||||
|
"settings.devmode": "Режим разработчика",
|
||||||
|
"settings.on": "Вкл",
|
||||||
|
"settings.off": "Откл",
|
||||||
|
"dev.title": "Дев-спавнер",
|
||||||
|
"dev.hint": "Выбери пункт · F9 — скрыть",
|
||||||
|
"dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
|
||||||
|
"dev.tools": "Инструменты",
|
||||||
|
"dev.damage": "Ранить выбранного",
|
||||||
|
"dev.kill": "Убить выбранного",
|
||||||
|
"dev.infect": "Заразить выбранного",
|
||||||
|
"dev.animals": "Животные",
|
||||||
|
"dev.plants": "Растения",
|
||||||
"settings.apply": "Применить",
|
"settings.apply": "Применить",
|
||||||
"settings.back": "Назад",
|
"settings.back": "Назад",
|
||||||
"pause.title": "Пауза",
|
"pause.title": "Пауза",
|
||||||
@@ -70,7 +142,7 @@
|
|||||||
"pause.mainmenu": "Главное меню",
|
"pause.mainmenu": "Главное меню",
|
||||||
"pause.quit": "Выход",
|
"pause.quit": "Выход",
|
||||||
"pause.saved": "Сохранено: {0}",
|
"pause.saved": "Сохранено: {0}",
|
||||||
"speed.pause": "⏸",
|
"speed.pause": "Пауза",
|
||||||
"credits.title": "Авторы",
|
"credits.title": "Авторы",
|
||||||
"credits.author": "mrleo1nid",
|
"credits.author": "mrleo1nid",
|
||||||
"credits.role": "Идея, код, движок mrgameeng",
|
"credits.role": "Идея, код, движок mrgameeng",
|
||||||
@@ -83,7 +155,6 @@
|
|||||||
"terrain.sand": "песок",
|
"terrain.sand": "песок",
|
||||||
"terrain.grass": "трава",
|
"terrain.grass": "трава",
|
||||||
"terrain.forest": "лес",
|
"terrain.forest": "лес",
|
||||||
"terrain.mountain": "горы",
|
|
||||||
"plant.oak": "дуб",
|
"plant.oak": "дуб",
|
||||||
"plant.birch": "берёза",
|
"plant.birch": "берёза",
|
||||||
"plant.pine": "сосна",
|
"plant.pine": "сосна",
|
||||||
@@ -121,8 +192,49 @@
|
|||||||
"pawn.hare": "заяц",
|
"pawn.hare": "заяц",
|
||||||
"pawn.boar": "кабан",
|
"pawn.boar": "кабан",
|
||||||
"pawn.wolf": "волк",
|
"pawn.wolf": "волк",
|
||||||
|
"pawn.chicken": "курица",
|
||||||
"pawn.muffalo": "муффало",
|
"pawn.muffalo": "муффало",
|
||||||
"pawn.squirrel": "белка",
|
"pawn.squirrel": "белка",
|
||||||
|
"thought.gaveBirth": "родила потомство",
|
||||||
|
"thought.mated": "спарилась",
|
||||||
|
"thought.quenchedThirst": "напилась вволю",
|
||||||
|
"gene.feedingStyle": "стиль питания",
|
||||||
|
"inspect.animal.feeding": "Питание: {0}",
|
||||||
|
"feeding.gorge": "обжора (редко, до отвала)",
|
||||||
|
"feeding.graze": "пастьба (часто, понемногу)",
|
||||||
|
"feeding.mixed": "умеренное",
|
||||||
|
"inspect.tab.log": "История",
|
||||||
|
"log.empty": "История пуста",
|
||||||
|
"log.stamp": "Д{0} {1:00}:{2:00}",
|
||||||
|
"hud.events": "События",
|
||||||
|
"hud.perf": "Произв.",
|
||||||
|
"events.title": "Журнал событий",
|
||||||
|
"events.empty": "Пока ничего не произошло",
|
||||||
|
"events.hint": "Двойной клик — перейти к существу",
|
||||||
|
"perf.title": "Производительность",
|
||||||
|
"perf.fps": "{0} FPS · {1:0.0} мс/кадр",
|
||||||
|
"perf.root": "{0}: {1:0.000} мс · систем {2}",
|
||||||
|
"perf.systemline": "{0} {1:0.000} мс {2:0}%",
|
||||||
|
"perf.hint": "Обновляется ~5 раз/с · показаны самые дорогие системы",
|
||||||
|
"event.mated": "Пара: {0} + {1}",
|
||||||
|
"event.pregnant": "{0}: беременность",
|
||||||
|
"event.birth": "{0}: роды ×{1}",
|
||||||
|
"event.hatch": "{0}: вылупление ×{1}",
|
||||||
|
"event.hunt": "Охота: {0} → {1}",
|
||||||
|
"event.injured": "Ранение: {0} → {1}",
|
||||||
|
"event.struck": "Удар: {0} → {1} ({2:0})",
|
||||||
|
"event.tookhit": "Удар: {1} → {0} ({2:0})",
|
||||||
|
"event.killed": "Добыча: {0} → {1}",
|
||||||
|
"event.ate": "{0} кормится",
|
||||||
|
"event.drank": "{0} пьёт воду",
|
||||||
|
"event.rest": "{0} отдыхает",
|
||||||
|
"event.dig": "{0} роет землю в поисках корма",
|
||||||
|
"event.wallow": "{0} валяется в грязи",
|
||||||
|
"event.fled": "{0} спасается бегством",
|
||||||
|
"event.grew": "{0}: новая стадия — {1}",
|
||||||
|
"event.death.starve": "Гибель: {0} (голод/жажда)",
|
||||||
|
"event.death.age": "Гибель: {0} (старость)",
|
||||||
|
"event.death.disease": "Гибель: {0} (болезнь)",
|
||||||
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||||
"net.connecting": "подключение…",
|
"net.connecting": "подключение…",
|
||||||
"net.connected": "подключено",
|
"net.connected": "подключено",
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
# Животные (зоология)
|
||||||
|
|
||||||
|
> Статус: **A1–A8 реализованы целиком + сериализация животных** — базовый план зоологии и его сквозное
|
||||||
|
> требование сейв/лоада закрыты; дальше экологический горизонт (хищники, опека, эпидемии, миграция) и
|
||||||
|
> бэклог реализма. Это живой план — стиль и правила как в [концепте](concept.md) и [модах](mods.md).
|
||||||
|
>
|
||||||
|
> **Сериализация животных (готово): сейв/лоад популяции и трупов.** `WorldSave` получил `Animals`
|
||||||
|
> (`AnimalSave`: геном→признаки+пол, поколение, стадия/возраст, нужды+истощение, здоровье — кровь/HP
|
||||||
|
> частей/хедифы тяжесть+иммунитет, настроение+мысли, беременность+геном отца) и `Corpses` (`CorpseSave`:
|
||||||
|
> вид/позиция/таймер-стадия разложения/мясо). `SaveAnimals` снимает популяцию (Health/Mood/Pregnant — через
|
||||||
|
> entity, т.к. запрос ≤5 типов); `RestoreAnimals`/`RestoreAnimalState` собирают особь фабрикой из генома и
|
||||||
|
> накладывают динамическое состояние (ref-правки до структурного `AddComponent(Pregnant)`; труп
|
||||||
|
> пере-тинтится через `Stage=-1`). `Corpse` получил поле `Species` (для спрайта/разделки); `CreateCorpse`
|
||||||
|
> теперь принимает индекс вида. На загрузке стадо НЕ пересыпается, если в сейве есть животные; старый/пустой
|
||||||
|
> сейв — регенерируется из сида (не крашит). Эволюция/болезни/беременности переживают сейв/лоад. Сборка +
|
||||||
|
> `--check-content` чистые.
|
||||||
|
>
|
||||||
|
> **A8 часть 2 (готово): инспектор животных.** `InspectPanel` теперь осматривает и животных (не только
|
||||||
|
> растения): вкладки **Обзор** (стадия/возраст/пол/поколение/беременность), **Нужды** (значения нужд %),
|
||||||
|
> **Здоровье** (кровь/боль, способности Consciousness/Moving/Sight, активные хедифы с тяжестью),
|
||||||
|
> **Настроение** (значение % + список мыслей с их сдвигом). Набор вкладок зависит от типа выбранного
|
||||||
|
> (растение vs животное). `SelectionSystem` теперь пикает и животных (запрос расширен с `PlantGrowth` на
|
||||||
|
> `AnimalOrganism`). Локализация `inspect.*`/`need.*`/`hediff.*`/`cap.*`/`animalstage.*` (ru/en). Сборка +
|
||||||
|
> `--check-content` чистые.
|
||||||
|
>
|
||||||
|
> **A8 часть 1 (готово): настроение — ядро.** Тип дефа `ThoughtDef` + `thoughts.json` (мысли: роды/
|
||||||
|
> спаривание/утоление жажды — сила и длительность данными); компонент `Mood` (значение 0..1, активность по
|
||||||
|
> тиру мозга, список затухающих мыслей) на всех животных; `AnimalMoodSystem` (тир 4+: настроение из
|
||||||
|
> непрерывного фона — средняя удовлетворённость убывающих нужд минус боль из здоровья — плюс сумма мыслей;
|
||||||
|
> мысли угасают). Реестр `ThoughtSet` + id-константы `AnimalThoughts`; событийные триггеры: роды
|
||||||
|
> (`AnimalPregnancySystem`), спаривание и утоление сильной жажды (`AnimalActionSystem`). **Эффект на
|
||||||
|
> поведение**: низкое настроение (стресс/истощение) подавляет спаривание в `AnimalDecisionSystem`. Команда
|
||||||
|
> `mood [species]`. Сборка + `--check-content` чистые (11 типов дефов). Осталось A8.2: вкладки инспектора
|
||||||
|
> (needs/health/mood) — сейчас животные в инспекторе не осматриваются (он только растительный).
|
||||||
|
>
|
||||||
|
> **A7 (готово): мозг → интеллект → гейт нужд/действий.** Эффективный интеллект = `brainSize` (ген) ×
|
||||||
|
> `Consciousness` (capacity из здоровья) — `AnimalFactory.Intelligence`. `AnimalContext` гейтит выбор ИИ:
|
||||||
|
> нужда с `minBrain` выше интеллекта особи в выбор не входит (`AnimalDecisionSystem` читает `Health`
|
||||||
|
> поэлементно). Пороги в `needs.json` — тиры мозга (голод 0.0, жажда 0.25, сон/секс 0.40; у оленя мозг
|
||||||
|
> ~0.45 → здоров весь набор). **Обратная связь от повреждения мозга**: болезнь/боль/кровопотеря роняют
|
||||||
|
> сознание → эффективный интеллект падает → первыми отключаются сон/спаривание (лихорадящий олень
|
||||||
|
> сводится к еде/питью). Команда `intel <species> [consciousness]`. Сборка + `--check-content` чистые.
|
||||||
|
>
|
||||||
|
> **A5 часть 1 (готово): смерть → труп → разложение.** `Corpse`-компонент + `CorpseSystem`
|
||||||
|
> (Fresh→Rotting→Skeleton→исчезает, скорость от температуры, тинт по стадии); смерть теперь оставляет
|
||||||
|
> труп (`AnimalFactory.CreateCorpse`, мясо ∝ размер тела) вместо деспавна; `AnimalDef.CorpseTexture` +
|
||||||
|
> `corpseTexture` оленя (`Dessicated_*`, скелет — бледный тинт, своего спрайта пока нет). Продукты
|
||||||
|
> `ProductMeat/Bone/Leather` в `products.json` (разделка/падальщики — позже). Сборка/линт чистые.
|
||||||
|
> **A5 часть 2 (готово): части тела, кровь, capacities.** `BodyDef`/`BodyPartDef` + `bodies.json`
|
||||||
|
> (`Quadruped` — данные: иерархия органов, вклады в способности); `AnimalDef.Body`; `HealthState`
|
||||||
|
> расширен `Parts`/`BloodLevel`/`Pain`/`Capacities`; `CapacityCalc` (обобщённый расчёт по зависимостям
|
||||||
|
> кровь→сердце/лёгкие→сознание→движение/зрение, модовые способности — сырыми); фабрика строит части
|
||||||
|
> (HP × размер тела) и считает способности. Источников урона ещё нет → всё на полном HP, способности =
|
||||||
|
> 1 (каркас под болезни A6/хищников). Команда `body <species>`. Сборка/линт чистые (10 типов дефов).
|
||||||
|
> Осталось в A5: смерть через здоровье (vital-часть/кровь) + истощение как hediff — когда появятся раны.
|
||||||
|
>
|
||||||
|
> **A6 (готово): болезни и иммунитет.** `HediffDef` расширён (severity/immunity/lethal/pain/capMods/
|
||||||
|
> ambient); хедифы теперь инстансы (`Hediff` с тяжестью/иммунитетом). `AnimalHealthSystem` (медленный
|
||||||
|
> тик ~час): фоновое заражение, гонка тяжесть↔иммунитет (выздоровление или смерть), пересчёт способностей
|
||||||
|
> с учётом боли и capMods; смерть по здоровью (летальная болезнь/кровь/потеря сознания) → труп. **Первый
|
||||||
|
> реальный урон** — `Moving` подключён к скорости (больные звери медленнее). Болезнь `Fever` в
|
||||||
|
> `hediffs.json`; команда `infect [hediff]`. Сборка/линт чистые.
|
||||||
|
>
|
||||||
|
> **A4 (готово):** нужда `Mating`; лёгкий hediff-каркас (`HediffDef`, `hediffs.json` с `Rut`,
|
||||||
|
> компонент `Health`/`HealthState`); гены `GeneBreedingSeason`/`GeneGestationDays`/`GeneLitterSize`;
|
||||||
|
> `AnimalRutSystem` (сезонный гон-хедиф поднимает влечение — вне сезона зачатия нет); действие
|
||||||
|
> `Mate` (поиск партнёра, сближение); `Pregnant` + `AnimalPregnancySystem` (вынашивание → роды
|
||||||
|
> через `Genome.Breed`, наследование пола без YY, поколение+1, потолок численности). Сборка
|
||||||
|
> чистая, `--check-content` (33 гена).
|
||||||
|
>
|
||||||
|
> **Рефактор расширяемости (готово):** устранены хардкод-блокеры — данными стали: **геном вида**
|
||||||
|
> (`AnimalDef.Genome` = словарь `geneId→base`, шаблон строится generically); **нужды**
|
||||||
|
> (`NeedDef`/`needs.json` + `NeedSet` + динамический `AnimalNeeds.Values` + реестр поведений по id);
|
||||||
|
> **диета** (`AnimalDef.Diet`); **стадии роста** (`AnimalStageDef`/`AnimalDef.Stages` — возраст входа,
|
||||||
|
> масштаб, текстуры, флаг «взрослая»; пусто → встроенный дефолт); **спавн** (`AnimalDef.SpawnPer1000Cells`
|
||||||
|
> — все виды рассыпаются по суше из данных, не только олень). Добавить ген/нужду/рацион/стадию/спавн
|
||||||
|
> виду = правка JSON без кода. Остаются как известные: реестр действий-кода (новое *поведение* = код,
|
||||||
|
> сознательная граница), часть глобальных баланс-констант (скорость/выпас/потолок).
|
||||||
|
>
|
||||||
|
> **A3 (готово):** компонент `AnimalGrowth` (стадия + возраст); ген пола `GeneSex` (XX/XY,
|
||||||
|
> генерация основателя без YY через `AnimalSet.GenerateGenome`) и ген `GeneMaturityAge`;
|
||||||
|
> стадии Baby→Juvenile→Adult→Senior с порогами из генов; `AnimalGrowthSystem` (смена
|
||||||
|
> спрайта детёныш/самец/самка и размера по стадии) + `AnimalMortalitySystem` (смерть от
|
||||||
|
> старости и от истощения → деспавн, замыкает контур ёмкости среды). Спавн стада —
|
||||||
|
> разновозрастный; команда `animal` показывает пол/созревание. Сборка чистая, `--check-content`
|
||||||
|
> (30 генов).
|
||||||
|
>
|
||||||
|
> **A1 (готово):** тип дефа `Animal` (`AnimalDef : PawnDef`) + `animals.json` (олень);
|
||||||
|
> 8 животных генов в `genes.json`; `AnimalPhenotype`/`AnimalOrganism`; `AnimalSet`
|
||||||
|
> (шаблон генома вида) + `AnimalFactory`; детерминированный спавн стада оленей по суше в
|
||||||
|
> `WorldScene` с рендером и тинтом меха по гену; консольная команда `animal <species>`.
|
||||||
|
>
|
||||||
|
> **A2 (готово):** нужды `AnimalNeeds` (голод/жажда/отдых) с падением по гену метаболизма;
|
||||||
|
> два слоя ИИ — `AnimalDecisionSystem` (движковый `UtilityAi`, выбор + поиск ближайшей
|
||||||
|
> еды/воды) и `AnimalActionSystem` (движение к цели + утоление: выедание растений, питьё у
|
||||||
|
> кромки воды, сон); `AnimalAppearanceSystem` (яркость по острой нужде); кромка воды из
|
||||||
|
> террейна; команда `popstats [species]` (средние признаков + размах поколений). Контур
|
||||||
|
> ёмкости среды частичный (трава выедается, но смерть от голода — в A3). Сборка чистая;
|
||||||
|
> визуальная проверка (олени ищут корм/воду, пасутся) — вручную: «Новый мир».
|
||||||
|
|
||||||
|
## Видение
|
||||||
|
|
||||||
|
Животные — следующая большая система после генов. Главный принцип: животное живёт по
|
||||||
|
**тем же законам, что и растение** — гены → признаки (фенотип) → поведение систем, —
|
||||||
|
но разворачивает их в полноценную зоологию: двуполое размножение с вынашиванием,
|
||||||
|
здоровье по модели RimWorld (части тела, органы, кровь, болезни), мозг и интеллект,
|
||||||
|
нужды и настроение. Цель прежняя: **эмерджентность** (динамика популяций, отбор,
|
||||||
|
пищевые цепи складываются из правил, а не скриптуются) и **наблюдаемость** (всё видно
|
||||||
|
в мире, HUD, инспекторе, консоли).
|
||||||
|
|
||||||
|
Первое показательное животное — **олень** (богатый набор текстур: ♂/♀/детёныш/труп).
|
||||||
|
|
||||||
|
## Опора на существующий фундамент
|
||||||
|
|
||||||
|
Ничего из перечисленного не строим заново — переиспользуем:
|
||||||
|
|
||||||
|
- организм-агностичный `GeneDef`, управляемый `Genome` (диплоидные аллели),
|
||||||
|
`Genome.Breed(мать, отец, mutationChance)`, `Phenotype.Compute`, `GenomeTemplate`
|
||||||
|
(шаблон вида, ср. `PlantSet.BuildTemplate`);
|
||||||
|
- формульный движок и regex-группы (`gsum('Gene.*')`) для производных признаков;
|
||||||
|
- движковый `UtilityAi<TContext>`, `ResponseCurve`, `Suitability.Trapezoid`;
|
||||||
|
- день/ночь, сезоны, климат, террейн `water`, `ProductDef`;
|
||||||
|
- паттерн компонентов растений: `…Organism { Genome; Phenotype Traits }`,
|
||||||
|
`…Growth { species, stage, ageDays }`, `…Needs`, `…Brain`, `AppearanceSystem` (sim→`Sprite`).
|
||||||
|
- Friflo допускает managed-ссылку в `IComponent` (доказано `Genome`/`Tilemap`) —
|
||||||
|
поэтому `Health`/`Pregnant` несут managed-объекты.
|
||||||
|
|
||||||
|
## Принцип моддинга: всё расширяется данными
|
||||||
|
|
||||||
|
Главное требование — **система должна целиком модифицироваться через моды без правки
|
||||||
|
кода**. Для этого:
|
||||||
|
|
||||||
|
1. **Никаких C#-`enum` для видового контента.** Виды, части тела, органы, способности
|
||||||
|
(capacities), болезни, нужды, мысли — это **дефы со строковыми id**, интернируемые
|
||||||
|
в int при загрузке (ровно как уже сделано с `geneId`). Enum нельзя расширить модом —
|
||||||
|
строковый id можно.
|
||||||
|
2. **Формулы вместо констант** везде, где можно (как `effects` у генов): модер меняет
|
||||||
|
баланс формулой в дефе, а не пересборкой.
|
||||||
|
3. **Реестр поведенческих действий** (`actionId → исполнитель-код`). Деф нужды/ИИ
|
||||||
|
ссылается на `actionId`. Граница честная и в духе проекта (движок без сэмпла,
|
||||||
|
фичи демонстрируются игрой): **комбинировать готовые кирпичи — мод; принципиально
|
||||||
|
новое поведение — код + деф.**
|
||||||
|
4. **Patch-система (фаза G5)** уже позволяет модеру менять чужие дефы по regex
|
||||||
|
(например, «всем хищникам +агрессия»).
|
||||||
|
5. Любой деф переопределяется одноимённым из позднего мода; локализация — в `Languages`.
|
||||||
|
|
||||||
|
Итог: модер добавляет новый вид зверя, орган, болезнь, нужду или мысль одними JSON-файлами.
|
||||||
|
|
||||||
|
## Геном животного
|
||||||
|
|
||||||
|
Животные гены живут в `genes.json` рядом с растительными (один организм-агностичный
|
||||||
|
тип `Gene`). Предлагаемый набор (числовые, если не сказано иное):
|
||||||
|
|
||||||
|
| Группа | Гены |
|
||||||
|
|---|---|
|
||||||
|
| Тело/метаболизм | `GeneMaxBodySize`, `GeneMetabolism`, `GeneMoveSpeed`, `GeneBloodVolume`, `GeneVision`, `GeneInsulation` (мех/терморегуляция) |
|
||||||
|
| Жизненный цикл | `GeneMaturityAge`, `GeneLifespan` (есть), `GeneGestationDays`, `GeneLitterSize` |
|
||||||
|
| Органы | `GeneHeartEfficiency`, `GeneLungCapacity`, `GeneImmunityGain`, `GeneHealRate`, `GenePainTolerance` |
|
||||||
|
| Мозг | `GeneBrainSize` → признак `brainSize` (см. «Мозг и интеллект») |
|
||||||
|
| Поведение | `GeneAggression`, `GeneSociability`, `GeneActivityPattern` (дневной/ночной/сумеречный) |
|
||||||
|
| Среда | переиспользуем `GeneOptimalTemperature/Tolerance/Cold/HeatHardiness` — звери тоже мёрзнут/перегреваются через ту же трапецию |
|
||||||
|
| Морфология | `GeneFurColor` (тинт спрайта, как `leafHue`), `GeneSex` (дискретный, см. ниже) |
|
||||||
|
|
||||||
|
**Пол выводится из диплоидности бесплатно.** Локус с аллелями {X=0, Y=1}: самка = XX,
|
||||||
|
самец = XY. При `Genome.Breed` мать может отдать только X (у неё нет Y), отец — X/Y
|
||||||
|
поровну → потомок XX/XY ровно 50/50, **без спец-кода в скрещивании**. У гена пола
|
||||||
|
`mutationChance: 0` (как у `GeneFruitSeason`), чтобы X не мутировал в Y.
|
||||||
|
|
||||||
|
> Тонкость генерации (не скрещивания): обычный `Generate` дискретного гена выбрал бы
|
||||||
|
> два аллеля независимо и мог бы выдать невозможный **YY**. Поэтому пол особи-основателя
|
||||||
|
> назначается явно — XX или XY (по 50/50) — а дальнейшее наследование корректно само.
|
||||||
|
|
||||||
|
Признаки собираются в `AnimalPhenotype.FromTraits(traits)` — зеркало `PlantPhenotype`.
|
||||||
|
Организм: `AnimalOrganism { Genome; AnimalPhenotype Traits; int Generation }` (поколение =
|
||||||
|
max(род.) + 1, у основателей 0 — для `popstats` и видимого дрейфа).
|
||||||
|
|
||||||
|
## Жизненный цикл и стадии роста
|
||||||
|
|
||||||
|
Зеркало `PlantGrowth`: `AnimalGrowth { species, stage, ageDays }`. Стадии задаются
|
||||||
|
данными в дефе вида: `Embryo` (в утробе) → `Baby` → `Juvenile` → `Adult` → `Senior`,
|
||||||
|
пороги из генов (`MaturityAge`, `Lifespan`). `AnimalGrowthSystem` двигает стадию,
|
||||||
|
масштабирует `sizeCells`, меняет спрайт/набор текстур (Baby ↔ ♀/♂). Стадия `Senior`
|
||||||
|
повышает шанс возрастных hediff и смерти — стык со здоровьем.
|
||||||
|
|
||||||
|
> **Путь смерти по фазам.** Чтобы на A4 (размножение) популяция не взрывалась без
|
||||||
|
> убыли, простая **смерть по старости и от голода/жажды (деспавн)** появляется уже в A3.
|
||||||
|
> На A5 она «апгрейдится»: смерть идёт через здоровье (возрастные hediff, истощение, отказ
|
||||||
|
> органов) и оставляет **труп** вместо деспавна.
|
||||||
|
|
||||||
|
## Нужды и ИИ
|
||||||
|
|
||||||
|
Обобщаем `PawnNeeds` (одна энергия) → `AnimalNeeds`: голод, жажда, сон, секс (0..1).
|
||||||
|
|
||||||
|
- скорости падения из `GeneMetabolism` × размер (масштаб по Клайберу: ∝ size^0.75);
|
||||||
|
- **голод** — ест растения/продукты. **Поедание реально воздействует на сущность
|
||||||
|
растения** (важно для контура ёмкости среды): выедает накопленные плоды
|
||||||
|
(`Fruiting.RipeFruit`) и/или убавляет биомассу/стадию растения; перевыпас может убить
|
||||||
|
растение. **Диета** (`AnimalDef.diet`) — список съедобного: теги растений/`ProductDef`
|
||||||
|
(для будущих хищников — живая добыча/мясо/трупы), плюс способ (выедание плодов vs выпас);
|
||||||
|
- **жажда** — пьёт на клетках террейна `water`;
|
||||||
|
- **сон** — спит в неактивную фазу суток (`GeneActivityPattern`): дневные ночью и т.д.;
|
||||||
|
- **секс** — удовлетворяется размножением.
|
||||||
|
|
||||||
|
ИИ разбит на **два слоя — выбор и исполнение**:
|
||||||
|
|
||||||
|
1. **Выбор** — `AnimalDecisionSystem` на движковом `UtilityAi`: из активных нужд выбирает
|
||||||
|
действие `FindFood / FindWater / Sleep / SeekMate / Wander(default)` (готовые
|
||||||
|
`ResponseCurve`) и пишет его + цель в `AnimalBrain { Action; Target; DecideIn }`.
|
||||||
|
Действия и нужды описаны дефами `NeedDef` (decay-формула, `minBrain`).
|
||||||
|
2. **Исполнение** — отдельный слой систем (реестр `actionId → исполнитель`, см. «Принцип
|
||||||
|
моддинга») доводит выбранное действие до результата по схеме **найти цель → дойти →
|
||||||
|
выполнить → удовлетворить нужду**:
|
||||||
|
- *Eat*: ближайшее съедобное (плод/биомасса) → подойти → съесть (убавить растение) → ↑ сытость;
|
||||||
|
- *Drink*: ближайшая клетка `water` → подойти → попить → ↓ жажда;
|
||||||
|
- *Sleep*: остановиться → спать → ↓ усталость;
|
||||||
|
- *Mate*: подходящий партнёр → сблизиться → акт → `Pregnant` + сброс `Mating`;
|
||||||
|
- *Wander*: движение по умолчанию.
|
||||||
|
Без этого слоя нужда только падает, но не исполняется.
|
||||||
|
|
||||||
|
**Передвижение и поиск цели.** Движение к цели — рулевое смещение к точке (реюз
|
||||||
|
`Transform2D` + `WanderSystem`/`SeparationSystem` для обхода препятствий: стволы деревьев,
|
||||||
|
другие звери), без полноценного A* на старте. Вода непроходима для сухопутных → `Drink`
|
||||||
|
целит клетку суши у кромки воды. «Ближайшее съедобное/вода/партнёр» — **пространственный
|
||||||
|
запрос по сетке/тайлмапу** с радиусом из `Vision`/`Sight` (не O(n²)); ничьи в «ближайшем»
|
||||||
|
разрешаются детерминированно (по id сущности).
|
||||||
|
|
||||||
|
> Порядок фаз: формальный **гейтинг нужд интеллектом — фаза A7**. В A2 у оленя просто
|
||||||
|
> полный набор нужд зверя (тир 4); поле `minBrain` уже пишем в дефы, но активируется оно
|
||||||
|
> на A7 (см. «Мозг и интеллект»).
|
||||||
|
|
||||||
|
## Размножение (млекопитающие)
|
||||||
|
|
||||||
|
Главное отличие от растений — двуполость и отложенное во времени вынашивание:
|
||||||
|
|
||||||
|
1. **Влечение и гон (через хедиф).** Нужда `Mating` есть **круглый год** на низком фоне.
|
||||||
|
В брачный сезон вида на взрослых вешается **хедиф `Rut` (гон)**, который поднимает
|
||||||
|
влечение (множитель на рост `Mating` / снижение порога действия). **Только под `Rut`
|
||||||
|
влечение переходит порог `SeekMate` → вне сезона зачатия фактически нет**, хотя нужда
|
||||||
|
формально существует. Хедиф снимается с концом сезона и **виден в инспекторе** (как
|
||||||
|
любой hediff). Это унифицирует сезонность с hediff-механизмом, а не городит спец-флаг.
|
||||||
|
2. **Спаривание → беременность.** Два взрослых разного пола сближаются (действие `Mate`) →
|
||||||
|
акт → у самки появляется `Pregnant : IComponent { Genome FatherGenome; float
|
||||||
|
ConceivedDay; float GestationDays; int LitterSize }` — managed-ссылка на геном отца
|
||||||
|
(оплодотворение = «слепок» отца). Беременность повышает метаболизм (голод) и снижает
|
||||||
|
`Moving`. (Можно реализовать и как hediff, но держим компонентом ради managed-ссылки
|
||||||
|
на геном отца.)
|
||||||
|
3. **Роды.** По достижении срока спавним `LitterSize` детёнышей, каждому
|
||||||
|
`Genome.Breed(мать, отец, mutationRate)`, стадия `Baby`, рядом с матерью. Лактация/
|
||||||
|
опека — опционально после.
|
||||||
|
|
||||||
|
Стратегии r/K выводятся из генов: мелкие (заяц) — большой помёт, короткая жизнь;
|
||||||
|
крупные (олень/медведь) — мало потомства, долгая жизнь.
|
||||||
|
|
||||||
|
**Решённые параметры (A4):**
|
||||||
|
- **Гон = хедиф `Rut`** (а не «нужда растёт в сезон»): навешивается в брачный сезон
|
||||||
|
`GeneBreedingSeason` (как `fruitSeason`, 0..3) — у **оленя осень** (→ роды весной к
|
||||||
|
обилию корма); у r-стратегов (заяц) — круглогодично/частый гон. Под `Rut` влечение
|
||||||
|
переходит порог действия; вне `Rut` зачатия нет.
|
||||||
|
- **Hediff-каркас рождается здесь (A4).** Гон — первый хедиф, поэтому в A4 появляется
|
||||||
|
лёгкий каркас: контейнер `Health`/`HealthState` со списком хедифов, `HediffDef` со
|
||||||
|
стадиями и модификаторами, навешивание/снятие по триггеру. A5/A6 расширяют его
|
||||||
|
частями тела, кровью, болезнями.
|
||||||
|
- **Триггер акта** — взрослые, разный пол, у обоих `Mating > 0.6`, рядом (1–2 клетки),
|
||||||
|
самка не беременна, оба не спят/не убегают.
|
||||||
|
- **Послеродовый кулдаун** — самка не зачинает снова до родов + восстановления;
|
||||||
|
самец восстанавливается быстро → **полигиния** (один самец кроет нескольких самок).
|
||||||
|
- **Выбор партнёра (половой отбор)** — НЕ в A4: на старте берётся ближайший подходящий;
|
||||||
|
предпочтение по размеру/здоровью/настроению добавим позже отдельной итерацией.
|
||||||
|
|
||||||
|
## Здоровье — полная модель RimWorld
|
||||||
|
|
||||||
|
### Данные (дефы)
|
||||||
|
|
||||||
|
- **`BodyDef`** — дерево частей. У части: `coverage` (доля тела → вес попадания),
|
||||||
|
`parent`, `maxHp` (масштаб от размера тела), флаги (`vital`, `solid`, `skinCovered`),
|
||||||
|
**вклады в capacities** `{ breathing: 0.5, consciousness: 1.0, moving: 0.25, … }`.
|
||||||
|
Пример (олень): `body → torso → (heart, lungs×2, liver, stomach, kidneys×2, spine),
|
||||||
|
head → (brain, eyes×2, ears×2, jaw), legs×4 → hooves`.
|
||||||
|
- **`HediffDef`** — *всё* есть hediff (рана, потеря части, болезнь, имплант, можно и
|
||||||
|
беременность): `stages` с порогами по severity; стадия задаёт `capMods`
|
||||||
|
(offset/factor/setMax по capacity), `painOffset/painFactor`, `bleedRate`, для болезней —
|
||||||
|
параметры иммунитета.
|
||||||
|
- **`CapacityDef`** — общий каталог способностей строковыми id: `Consciousness, Moving,
|
||||||
|
Manipulation, Sight, Hearing, Breathing, BloodPumping, BloodFiltration, Digestion,
|
||||||
|
Metabolism` (расширяемо модом). Это **каталог**, а не обязательный набор: вид имеет
|
||||||
|
лишь те способности, в которые вкладываются части его `BodyDef` (у копытного оленя нет
|
||||||
|
`Manipulation` — некому вкладываться).
|
||||||
|
|
||||||
|
### Компонент
|
||||||
|
|
||||||
|
`Health : IComponent { HealthState State }` — managed-класс (как `Genome`):
|
||||||
|
`List<PartInstance>` (деф + текущий hp), `List<Hediff>` (severity, привязка к части,
|
||||||
|
иммунитет), кэш `Capacities`, `Pain`, `BloodLevel`, `Dead`.
|
||||||
|
|
||||||
|
> Контейнер растёт по фазам: **рождается в A4** с одним лишь `List<Hediff>` (под гон-хедиф
|
||||||
|
> `Rut`); части тела, кровь, capacities и `Dead` добавляются в A5, болезни/иммунитет — A6.
|
||||||
|
|
||||||
|
### Расчёт capacities (обобщённая дата-модель)
|
||||||
|
|
||||||
|
Способности зависят друг от друга, считаем в порядке зависимостей:
|
||||||
|
|
||||||
|
1. `BloodLevel` (0..1) ← запас минус кровопотеря;
|
||||||
|
2. `BloodPumping` ← сердце × BloodLevel; `Breathing` ← лёгкие × BloodLevel;
|
||||||
|
`BloodFiltration` ← печень/почки;
|
||||||
|
3. `Consciousness` ← мозг × BloodPumping × Breathing × (1 − болевой штраф);
|
||||||
|
4. `Moving` ← ноги/позвоночник × Consciousness × BloodPumping; `Sight` ← глаза ×
|
||||||
|
Consciousness; `Digestion` и пр. — домножаются на вышестоящие.
|
||||||
|
|
||||||
|
Формула одна и общая: `capacity = Σ(вклады работающих частей × partHp/maxHp) ×
|
||||||
|
вышестоящие capacity`. **Без хардкода на каждую способность** — новую способность
|
||||||
|
вводит деф. Каждая capacity — множитель на действия: `Moving`→скорость, `Sight`→радиус
|
||||||
|
поиска, `Digestion`→скорость насыщения, `Consciousness`→интеллект и сам факт сознания.
|
||||||
|
|
||||||
|
### Кровь — как hediff (реалистичная модель RW)
|
||||||
|
|
||||||
|
Кровопотеря = hediff `BloodLoss` (severity 0..1). Кровоточащие раны поднимают её через
|
||||||
|
`bleedRate`; без кровотечения медленно рассасывается; severity = 1 → смерть; влияет на
|
||||||
|
`BloodPumping`/`Consciousness` через `capMods`. `GeneBloodVolume` масштабирует скорость
|
||||||
|
роста severity (больше крови → медленнее теряет).
|
||||||
|
|
||||||
|
### Болезни и иммунитет (гонка)
|
||||||
|
|
||||||
|
severity болезни растёт за день; параллельно растёт иммунитет (0..1) со скоростью из
|
||||||
|
стата (зависит от `BloodFiltration`, отдыха, сытости, `GeneImmunityGain`). Иммунитет→1
|
||||||
|
раньше = выздоровел; severity→летальный порог раньше = смерть. Голодный/раненый зверь
|
||||||
|
борется с болезнью медленнее.
|
||||||
|
|
||||||
|
### Смерть
|
||||||
|
|
||||||
|
Триггеры: разрушена `vital`-часть (мозг/сердце), `Consciousness ≤ порога`,
|
||||||
|
`BloodLevel = 0`, летальная severity болезни, старость (возрастные hediff: катаракта,
|
||||||
|
больная спина, деменция, закупорка артерий + шанс смерти от старости). На смерти:
|
||||||
|
стоп-ИИ, переход в труп.
|
||||||
|
|
||||||
|
### Тик
|
||||||
|
|
||||||
|
Медленный «health tick» раз в игровой час + событийный пересчёт при уроне.
|
||||||
|
Детерминизм: привязка к игровому времени, не к FPS.
|
||||||
|
|
||||||
|
### Источники урона в v1
|
||||||
|
|
||||||
|
**Почти никаких.** Строим весь каркас (части, кровь, capacities, hediff-framework, условия
|
||||||
|
смерти), но не вводим *травмирующих* источников. На старте (A1–A2) смерть только **от
|
||||||
|
старости**.
|
||||||
|
|
||||||
|
Порядок источников урона по фазам: **истощение/обезвоживание** — уже в **A3** (простое
|
||||||
|
правило «нужда=0 дольше N дней → смерть», замыкает контур ёмкости среды; в A5
|
||||||
|
переоформляется в hediff истощения) → **температура** (гипотермия/тепловой удар, hediff на
|
||||||
|
той же трапеции, что у растений) → **болезни** (A6) → **хищничество** (горизонт).
|
||||||
|
|
||||||
|
## Труп и разложение
|
||||||
|
|
||||||
|
Труп — отдельная сущность с `Corpse { RotDays; Stage }`. Конвейер (скорость зависит от
|
||||||
|
температуры/сезона — реюз климата):
|
||||||
|
|
||||||
|
```
|
||||||
|
Fresh ──→ Rotting ──→ Skeleton ──→ (исчезает)
|
||||||
|
│ │ │
|
||||||
|
│ разделка │ разделка └─ остаётся ProductBone
|
||||||
|
└──────────┴─→ ProductMeat (кол-во ∝ GeneBodySize) + опц. ProductLeather
|
||||||
|
```
|
||||||
|
|
||||||
|
- свежий/гниющий можно **разделать** → мясо (объём от размера тела) + шкура;
|
||||||
|
спрайт `Dessicated_*` (для оленя уже есть) на стадии Rotting;
|
||||||
|
- **скелет** — финальная стадия разложения (как просили, скелет — часть процесса):
|
||||||
|
мясо уже не получить, остаются кости (`ProductBone`);
|
||||||
|
- после скелета труп **исчезает в ничто**. На будущее — возврат плодородия в почву
|
||||||
|
(ниточка к растениям, опционально).
|
||||||
|
- мясо/кости/падаль → кормовая база будущих хищников и падальщиков.
|
||||||
|
|
||||||
|
Новые продукты: `ProductMeat`, `ProductBone`, `ProductLeather`.
|
||||||
|
|
||||||
|
**Спрайт скелета — нужен настоящий (решено).** В проекте костяных/скелетных текстур
|
||||||
|
нет (проверено) → это **ассет-задача**: создать/найти спрайт скелета (желательно
|
||||||
|
универсальный + по-видовой при наличии), путь вида `things/pawn/animal/deer/Skeleton_*`.
|
||||||
|
До появления ассета стадию Skeleton можно временно рисовать обработкой `Dessicated_*`,
|
||||||
|
но финальная версия требует своего спрайта.
|
||||||
|
|
||||||
|
## Мозг и интеллект
|
||||||
|
|
||||||
|
`GeneBrainSize` → признак `brainSize`. **Эффективный интеллект** = `brainSize ×
|
||||||
|
Consciousness` — т.е. повреждение/болезнь мозга, боль, кровопотеря динамически снижают
|
||||||
|
интеллект (сильная связка со здоровьем).
|
||||||
|
|
||||||
|
Интеллект **гейтит набор нужд и действий**. У `NeedDef`/действия поле `minBrain`:
|
||||||
|
активный набор особи = те, у кого `minBrain ≤ brainSize`. **Без enum** — шкала
|
||||||
|
непрерывная, тиры ниже лишь ярлыки для авторов контента (числа порогов — контент,
|
||||||
|
правятся модом). `brainSize` нормирован 0..1.
|
||||||
|
|
||||||
|
| Тир | `minBrain` | Кто | Что открывается |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 — рефлекс | 0.00 | губки, простейшие | пассивный голод, реактивность |
|
||||||
|
| 2 — примитив | ~0.10 | насекомые, черви | голод; реактивное поведение |
|
||||||
|
| 3 — низший позвоночный | ~0.25 | рыбы, рептилии, амфибии | +жажда; простое избегание |
|
||||||
|
| 4 — зверь | ~0.45 | млекопитающие, птицы (**олень**, заяц, волк) | +сон, секс, Flee, SeekMate; **настроение**; факультативно стадность/память |
|
||||||
|
| 5 — умный (на будущее) | ~0.75 | приматы, врановые, люди | +социальные нужды, игра, любопытство, память о ресурсах, планирование, обучение, сложное настроение/отношения |
|
||||||
|
|
||||||
|
Олень = **тир 4**. Настроение, сон и брачное поведение включаются с тира 4.
|
||||||
|
|
||||||
|
## Настроение
|
||||||
|
|
||||||
|
`Mood` (0..1) собирается **из двух источников** (решено):
|
||||||
|
|
||||||
|
1. **Непрерывные состояния → в настроение напрямую** по кривой `ResponseCurve`:
|
||||||
|
уровни голода, жажды, усталости, боли (из здоровья), температуры вне комфорта.
|
||||||
|
Это не дискретные мысли, а текущий вклад нужд/состояния.
|
||||||
|
2. **События → `ThoughtDef { moodOffset, durationDays, trigger }`** (модель thoughts
|
||||||
|
RimWorld, данными): «дала потомство», «спарилась», «напилась после долгой жажды»,
|
||||||
|
«потеряла детёныша» — временный сдвиг настроения с затуханием.
|
||||||
|
|
||||||
|
Настроение есть только с тира 4 (зверь) — гейт интеллектом; у примитивных его нет.
|
||||||
|
Влияет на поведение (низкое → паника/бегство, не спаривается) и видно в инспекторе.
|
||||||
|
|
||||||
|
## Ориентировочные числа для оленя (старт)
|
||||||
|
|
||||||
|
Шкала времени мира: **год = 60 игровых дней**, сезон = 15 дней (для справки растения
|
||||||
|
живут ~160 дней). Числа — стартовая точка, балансируются при реализации; это значения
|
||||||
|
по умолчанию генома вида (`GenomeTemplate`), вокруг которых идёт разброс и мутации.
|
||||||
|
|
||||||
|
| Параметр | Ген/поле | Значение | Заметка |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Размер тела | `GeneMaxBodySize` | ♀ 1.4 / ♂ 1.6 sizeCells | как в текущих `DeerDoe`/`DeerBuck` |
|
||||||
|
| Продолжительность жизни | `GeneLifespan` | ~360 дн (~6 лет) | дольше растений, видно несколько поколений |
|
||||||
|
| Взросление (→Adult) | `GeneMaturityAge` | ~90 дн (~1.5 года) | Baby 0–20, Juvenile 20–90, Adult 90–~290, Senior 290+ |
|
||||||
|
| Срок беременности | `GeneGestationDays` | ~30 дн (2 сезона) | гон осенью → роды весной |
|
||||||
|
| Помёт | `GeneLitterSize` | 1 (изредка 2) | K-стратег |
|
||||||
|
| Сезон гона | `GeneBreedingSeason` | осень | у зайца — круглый год |
|
||||||
|
| Размер мозга | `GeneBrainSize` | ~0.45 (тир 4) | открывает сон/секс/настроение |
|
||||||
|
| Объём крови | `GeneBloodVolume` | ~1.0 (×размер) | масштабирует скорость кровопотери |
|
||||||
|
| Скорость | `GeneMoveSpeed` | ~1.1–1.3 | олень быстрый |
|
||||||
|
| Голод | `NeedDef` decay | полный голод за ~1.5 дн | по Клайберу ∝ size^0.75 |
|
||||||
|
| Жажда | `NeedDef` decay | полная жажда за ~0.75 дн | быстрее голода |
|
||||||
|
| Сон | `NeedDef` decay | суточный цикл | спит в неактивную фазу |
|
||||||
|
|
||||||
|
## Идеи для реализма (бэклог)
|
||||||
|
|
||||||
|
Опционально, по мере фаз — каждая ложится на дата-модель:
|
||||||
|
|
||||||
|
- **Гон/брачный сезон** — хедиф `Rut`, навешиваемый в сезон, поднимает влечение (реализовано в A4).
|
||||||
|
- **Терморегуляция мехом** — `GeneInsulation` сдвигает температурную трапецию;
|
||||||
|
новорождённые мёрзнут сильнее.
|
||||||
|
- **Метаболизм по Клайберу** — расход ∝ size^0.75 (крупные едят больше абсолютно,
|
||||||
|
меньше на кг).
|
||||||
|
- **Циркадные ритмы** — дневные/ночные/сумеречные виды (`GeneActivityPattern`).
|
||||||
|
- **Стадность** — `GeneSociability`: стадные держатся группой (реюз `SeparationSystem`
|
||||||
|
+ когезия) → эмерджентные стада.
|
||||||
|
- **Сезонный окрас** — арктические лиса/заяц/волк белеют зимой (текстуры `*_arctic`
|
||||||
|
есть) — сезонный свитч спрайта/тинта.
|
||||||
|
- **Половой диморфизм** — рога/размер у самцов (ген, проявляется при XY).
|
||||||
|
- **r/K-стратегии** — выводятся из `litterSize/gestation/lifespan`.
|
||||||
|
- **Инбридинг-депрессия** (продвинутое) — родственное скрещивание снижает fitness
|
||||||
|
через накопление рецессивных аллелей.
|
||||||
|
- **Память о ресурсах/территория** (для умных) — водопои, кормовые места.
|
||||||
|
- **Возврат нутриентов** — полное разложение трупа повышает плодородие клетки.
|
||||||
|
|
||||||
|
## Новые типы дефов (для модеров)
|
||||||
|
|
||||||
|
| Тип | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `AnimalDef : PawnDef` (подтип) | вид: геном-шаблон (ссылки на гены), `BodyDef`, диета, текстуры по стадиям/полу, размеры. Наследник `PawnDef` — существующие 9 животных-`PawnDef` остаются валидными; поведение на компонентах, не на типе дефа |
|
||||||
|
| `BodyDef` | дерево частей тела с coverage/parent/maxHp/флагами/вкладами в capacities |
|
||||||
|
| `HediffDef` | раны/болезни/состояния: стадии, capMods, боль, кровотечение, иммунитет |
|
||||||
|
| `CapacityDef` | именованная способность (строковый id) |
|
||||||
|
| `NeedDef` | нужда: формула расхода, `minBrain`, привязка к действию |
|
||||||
|
| `ThoughtDef` | мысль настроения: offset, длительность, условие-триггер |
|
||||||
|
| `ProductDef` (есть) | + `ProductMeat`, `ProductBone`, `ProductLeather` |
|
||||||
|
|
||||||
|
CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип = класс + `RegisterType` в
|
||||||
|
`GameContent.Load` (см. [mods.md](mods.md)).
|
||||||
|
|
||||||
|
## Отбор, экология и наблюдаемость
|
||||||
|
|
||||||
|
Смысл генетики — чтобы за поколениями популяция **реально адаптировалась к климату
|
||||||
|
сида** (дрейф признаков под отбором). Чтобы это было видно (столп «наблюдаемость»):
|
||||||
|
|
||||||
|
- **Команда `popstats <species>`** — среднее и разброс ключевых признаков популяции +
|
||||||
|
счётчик поколений. Без неё отбор «есть, но невидим». Появляется, как только в мире
|
||||||
|
есть популяция (≈A1–A2), расширяется по мере добавления признаков.
|
||||||
|
- **Контур ёмкости среды (Лотка–Вольтерра через корм).** Даже без хищников травоядные
|
||||||
|
могут **выесть растения → голод → крах популяции → восстановление флоры → новый рост**.
|
||||||
|
Это бесплатная эмерджентность уже на A2, **если поедание реально уменьшает растения**.
|
||||||
|
Явно проверяем этот контур на A2 (поедание убавляет плод/растение, голод растёт при
|
||||||
|
нехватке корма). **Важно:** полный «крах» популяции требует **смерти от голода** — она
|
||||||
|
появляется в **A3** (простое правило). На A2 контур наблюдается частично (звери голодают
|
||||||
|
и тормозят, но не вымирают), полностью замыкается с A3.
|
||||||
|
|
||||||
|
## Сквозные требования (каждая фаза)
|
||||||
|
|
||||||
|
- **Детерминизм.** Любая случайность (спавн, выбор партнёра, мутации, выбор части под
|
||||||
|
урон, шанс инфекции) — из seeded `Random`, выведенного из сида мира (+ id сущности),
|
||||||
|
никогда `Random.Shared`. Один сид = воспроизводимый мир. Ничьи в пространственных
|
||||||
|
запросах («ближайший корм/вода/партнёр») разрешаются по id сущности, не по порядку обхода.
|
||||||
|
- **Сериализация — чек-лист по каждой фазе.** Каждая фаза сразу делает сейв/лоад своих
|
||||||
|
данных: A1 геном (уже умеет, geneId→Allele) → A3 стадия/возраст/пол → A4 беременность
|
||||||
|
(срок + **геном отца**) + список хедифов (гон `Rut`) → A5 здоровье (части тела, кровь,
|
||||||
|
capacities) + труп (стадия/таймер разложения) → A8 настроение/мысли. Пустой/старый
|
||||||
|
сейв — регенерируется, не крашит.
|
||||||
|
- **Sim/presentation split.** Системы симуляции пишут только компоненты; спрайт/тинт
|
||||||
|
(пол, стадия, мех, гниль) ставит `AppearanceSystem`. Никаких draw/UI из симуляции.
|
||||||
|
|
||||||
|
## План реализации (фазы A1–A8)
|
||||||
|
|
||||||
|
По аналогии с фазами генов G1–G5. Каждая фаза — отдельный коммит, дев-команда для
|
||||||
|
проверки, проверка через `--check-content` (где можно), и пункты сквозных требований выше.
|
||||||
|
**Вкладки инспектора добавляются по мере появления данных:** needs@A2, health@A5, mood@A8.
|
||||||
|
|
||||||
|
1. **A1 — фундамент.** `AnimalDef` + животные гены + `AnimalPhenotype`/`AnimalOrganism`
|
||||||
|
+ per-species `GenomeTemplate` + спавн оленя в `WorldScene` + рендер + команда
|
||||||
|
`animal <species>`.
|
||||||
|
2. **A2 — нужды + ИИ (выбор + исполнение).** `AnimalNeeds` + `AnimalDecisionSystem`
|
||||||
|
(выбор) + **слой исполнения** (Eat/Drink/Sleep: найти→дойти→выполнить→удовлетворить) +
|
||||||
|
еда-растения / вода-террейн / сон по фазе суток + `AppearanceSystem`. **Проверить
|
||||||
|
контур ёмкости среды** (поедание убавляет растения → колебания). Команда `popstats`.
|
||||||
|
3. **A3 — жизненный цикл.** Стадии Baby→Adult→Senior, ген пола (XX/XY, генерация без
|
||||||
|
YY), спрайт по стадии/полу, **простая смерть по старости и от голода/жажды (деспавн)**
|
||||||
|
— последняя замыкает контур ёмкости среды.
|
||||||
|
4. **A4 — размножение.** Нужда `Mating` + **лёгкий hediff-каркас** + гон-хедиф `Rut`
|
||||||
|
(сезон) + исполнение `Mate` → `Pregnant` → роды через `Genome.Breed`.
|
||||||
|
5. **A5 — здоровье I.** `BodyDef`, части тела, кровь (hediff), capacities, **апгрейд
|
||||||
|
смерти**: через здоровье + **труп** с разложением до скелета (вместо деспавна A3).
|
||||||
|
6. **A6 — здоровье II.** Болезни/`HediffDef` с иммунитетом, боль.
|
||||||
|
7. **A7 — мозг/интеллект.** Ген→интеллект→гейт нужд/действий + обратная связь от
|
||||||
|
повреждения мозга.
|
||||||
|
8. **A8 — настроение.** `Mood` + `ThoughtDef` + вкладки инспектора (needs/health/mood —
|
||||||
|
`InspectPanel` уже умеет вкладки overview/genes/products).
|
||||||
|
|
||||||
|
## После A8 — экологический горизонт
|
||||||
|
|
||||||
|
То, ради чего вся глубина: превращает «зоопарк» в живую экосистему. Каждая фича опирается
|
||||||
|
на уже построенные системы.
|
||||||
|
|
||||||
|
> **ГОТОВО — хищный кластер (часть 1): диета-гены + охота + урон + бегство.** Диета теперь
|
||||||
|
> ДАННЫЕ-через-ГЕНЫ: `GeneHerbivory`/`GeneCarnivory`/`GeneOmnivory` → признаки
|
||||||
|
> `herbivory/carnivory/omnivory`; рацион выводит `AnimalFactory.Diet` (ест растения, если
|
||||||
|
> max(herb,omni) ≥ 0.5; ест мясо, если max(carn,omni) ≥ 0.5 — всеядность включает оба; откат
|
||||||
|
> на `AnimalDef.Diet` у видов без генов диеты). Новые виды: **Wolf** (хищник, охотится) и
|
||||||
|
> **WildBoar** (всеядный); у оленя добавлен `GeneHerbivory`. Действие `Hunt`
|
||||||
|
> (`AnimalDecisionSystem` ищет ближайший труп-падаль ИЛИ живую добычу не крупнее себя;
|
||||||
|
> `AnimalActionSystem.Hunt` ведёт к цели и при контакте кусает или ест труп). **Первый
|
||||||
|
> травматический урон**: `HealthState.ApplyInjury` (урон части тела по весу попадания + острая
|
||||||
|
> кровопотеря + хедиф `Bleeding`); `Bleeding` (`bloodLossPerDay`) точит кровь в
|
||||||
|
> `AnimalHealthSystem` (+восстановление крови в покое), кровь на нуле → смерть → труп → падаль.
|
||||||
|
> Боевые параметры — данные на `AnimalDef` (`attackDamage/attackBleed/attackBloodLoss/
|
||||||
|
> attackRangeCells`). **Бегство**: `AnimalActions.Flee` — жертва, заметившая хищника не мельче
|
||||||
|
> себя в радиусе восприятия, убегает (приоритет над нуждами); сам хищник бежит лишь от заметно
|
||||||
|
> более крупного мясоеда. Общий путь смерти — `AnimalFactory.Die`. Консоль: `diet [species]`;
|
||||||
|
> `--check-content` печатает роль каждого вида. **Не покрыто (горизонт):** падальщики как
|
||||||
|
> отдельная роль, удар по настроению от страха, снижение страха стадом, спрайт скелета.
|
||||||
|
|
||||||
|
- **Хищный кластер.** Хищничество (волк→олень): диета = живая добыча; действие `Hunt`
|
||||||
|
(выследить → преследовать → атака → **урон через здоровье A5** → добить → съесть труп).
|
||||||
|
Это **первый «активный» (травмирующий) источник урона** для системы здоровья и
|
||||||
|
полноценная Лотка–Вольтерра с хищником. Рядом: **падальщики** (едят трупы Fresh/Rotting)
|
||||||
|
и **угроза/бегство** (хищник в радиусе `Sight` → страх → действие `Flee`, удар по
|
||||||
|
настроению; стадо снижает страх).
|
||||||
|
- **Родительская опека / импринтинг.** Детёныш ходит за матерью, голод гасится близостью
|
||||||
|
к лактирующей матери до ювенильной стадии; мать ведёт/защищает. Реализм + детёныши не
|
||||||
|
мрут от голода в одиночку.
|
||||||
|
- **Эпидемии.** Болезни (A6) распространяются по контакту/плотности → плотностно-зависимый
|
||||||
|
регулятор популяции (вспышки-крахи). Исход решает иммунитет (`GeneImmunityGain`).
|
||||||
|
- **Миграция.** При локальном истощении корма / некомфортном климате — уход к лучшим
|
||||||
|
клеткам (концепт: «голод+неурожай→миграция»). Эмерджентно из нужд + пространственного
|
||||||
|
поиска с большим радиусом.
|
||||||
|
|
||||||
|
Сюда же тяготеют пункты бэклога: половой отбор (выбор партнёра), сезонный окрас,
|
||||||
|
инбридинг-депрессия, возврат нутриентов в почву.
|
||||||
|
|
||||||
|
## Открытые вопросы
|
||||||
|
|
||||||
|
- Конкретный список `ThoughtDef`-событий для оленя (набор и величины offset/длительности)
|
||||||
|
— детализируется при реализации A8.
|
||||||
|
- Ассет: создать/найти спрайт скелета (`things/pawn/animal/deer/Skeleton_*`) к фазе A5.
|
||||||
+1
-1
Submodule engine updated: 96e19c7c61...46f3931f85
@@ -0,0 +1,17 @@
|
|||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
|
||||||
|
public enum ConnectionStatus
|
||||||
|
{
|
||||||
|
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
|
||||||
|
Idle,
|
||||||
|
|
||||||
|
/// <summary>Идёт первая попытка подключения.</summary>
|
||||||
|
Connecting,
|
||||||
|
|
||||||
|
/// <summary>Соединение установлено, снапшоты приходят.</summary>
|
||||||
|
Connected,
|
||||||
|
|
||||||
|
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
|
||||||
|
Reconnecting,
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<Compile Include="Pages\Index.razor.cs" />
|
<Compile Include="Pages\Index.razor.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="LittleSimWebGame.cs" />
|
<Compile Include="LittleSimWebGame.cs" />
|
||||||
|
<Compile Include="ConnectionStatus.cs" />
|
||||||
<Compile Include="NetContract.cs" />
|
<Compile Include="NetContract.cs" />
|
||||||
<Compile Include="WorldViewScene.cs" />
|
<Compile Include="WorldViewScene.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using Friflo.Engine.ECS;
|
using Friflo.Engine.ECS;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.Net;
|
using MrGameEng.Net;
|
||||||
@@ -36,7 +38,34 @@ public static class WebNetSchema
|
|||||||
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
|
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
|
||||||
public const int DefaultPort = 9050;
|
public const int DefaultPort = 9050;
|
||||||
|
|
||||||
|
// Те же wire-размеры, что и в десктопном NetSchema.cs — проверяем, что зеркало не разошлось по раскладке.
|
||||||
|
private const int TransformWireSize = 20; // Vector2(8) + float(4) + Vector2(8)
|
||||||
|
private const int PawnNeedsWireSize = 4; // float(4)
|
||||||
|
|
||||||
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
|
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
|
||||||
public static ReplicationSchema Create() =>
|
public static ReplicationSchema Create()
|
||||||
new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
|
{
|
||||||
|
VerifyLayout();
|
||||||
|
return new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сверяет байт-раскладку зеркальных структур с десктопным контрактом (NetSchema.cs).</summary>
|
||||||
|
private static void VerifyLayout()
|
||||||
|
{
|
||||||
|
Require<NetTransform>(TransformWireSize);
|
||||||
|
Require<NetPawnNeeds>(PawnNeedsWireSize);
|
||||||
|
|
||||||
|
static void Require<T>(int expected)
|
||||||
|
where T : struct
|
||||||
|
{
|
||||||
|
var actual = Unsafe.SizeOf<T>();
|
||||||
|
if (actual != expected)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Размер зеркального {typeof(T).Name} = {actual} Б, ожидался {expected} Б: "
|
||||||
|
+ "wire-контракт разошёлся — синхронизируй NetContract.cs и LittleSim/Net/NetSchema.cs."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,22 +9,6 @@ using MrGameEng.Net;
|
|||||||
|
|
||||||
namespace LittleSim.Web;
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
|
|
||||||
public enum ConnectionStatus
|
|
||||||
{
|
|
||||||
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
|
|
||||||
Idle,
|
|
||||||
|
|
||||||
/// <summary>Идёт первая попытка подключения.</summary>
|
|
||||||
Connecting,
|
|
||||||
|
|
||||||
/// <summary>Соединение установлено, снапшоты приходят.</summary>
|
|
||||||
Connected,
|
|
||||||
|
|
||||||
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
|
|
||||||
Reconnecting,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
||||||
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Audio;
|
|
||||||
using MrGameEng.Mods;
|
|
||||||
|
|
||||||
namespace LittleSim.App;
|
namespace LittleSim.App;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,63 +26,7 @@ public sealed class GameSettings
|
|||||||
|
|
||||||
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
|
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
|
||||||
public float UiScale { get; set; } = 1f;
|
public float UiScale { get; set; } = 1f;
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
/// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
|
||||||
public static class GameSettingsStore
|
public bool DeveloperMode { get; set; } = true;
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary>
|
|
||||||
public static GameSettings Load()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(GamePaths.SettingsFile))
|
|
||||||
{
|
|
||||||
return JsonSerializer.Deserialize<GameSettings>(
|
|
||||||
File.ReadAllText(GamePaths.SettingsFile),
|
|
||||||
JsonOptions
|
|
||||||
) ?? new GameSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or JsonException)
|
|
||||||
{
|
|
||||||
// Битый файл настроек не должен ронять запуск — откатываемся к умолчаниям.
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GameSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Сохраняет настройки в <see cref="GamePaths.SettingsFile"/>.</summary>
|
|
||||||
public static void Save(GameSettings settings)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(GamePaths.Root);
|
|
||||||
File.WriteAllText(GamePaths.SettingsFile, JsonSerializer.Serialize(settings, JsonOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Применяет настройки к движку: язык (<paramref name="languages"/>), полноэкранный
|
|
||||||
/// режим/VSync/размер окна (<paramref name="graphics"/>) и громкость (<paramref name="audio"/>).
|
|
||||||
/// </summary>
|
|
||||||
public static void Apply(
|
|
||||||
GameSettings settings,
|
|
||||||
GraphicsDeviceManager graphics,
|
|
||||||
LanguageManager languages,
|
|
||||||
AudioManager audio
|
|
||||||
)
|
|
||||||
{
|
|
||||||
languages.SetLanguage(settings.Language);
|
|
||||||
audio.MasterVolume = settings.Volume;
|
|
||||||
|
|
||||||
graphics.IsFullScreen = settings.Fullscreen;
|
|
||||||
graphics.SynchronizeWithVerticalRetrace = settings.VSync;
|
|
||||||
graphics.PreferredBackBufferWidth = settings.Width;
|
|
||||||
graphics.PreferredBackBufferHeight = settings.Height;
|
|
||||||
graphics.ApplyChanges();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Audio;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||||
|
public static class GameSettingsStore
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Загружает настройки из <see cref="GamePaths.SettingsFile"/> или возвращает значения по умолчанию.</summary>
|
||||||
|
public static GameSettings Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(GamePaths.SettingsFile))
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<GameSettings>(
|
||||||
|
File.ReadAllText(GamePaths.SettingsFile),
|
||||||
|
JsonOptions
|
||||||
|
) ?? new GameSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or JsonException)
|
||||||
|
{
|
||||||
|
// Битый файл настроек не должен ронять запуск — откатываемся к умолчаниям.
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GameSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сохраняет настройки в <see cref="GamePaths.SettingsFile"/>.</summary>
|
||||||
|
public static void Save(GameSettings settings)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(GamePaths.Root);
|
||||||
|
File.WriteAllText(GamePaths.SettingsFile, JsonSerializer.Serialize(settings, JsonOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Применяет настройки к движку: язык (<paramref name="languages"/>), полноэкранный
|
||||||
|
/// режим/VSync/размер окна (<paramref name="graphics"/>) и громкость (<paramref name="audio"/>).
|
||||||
|
/// </summary>
|
||||||
|
public static void Apply(
|
||||||
|
GameSettings settings,
|
||||||
|
GraphicsDeviceManager graphics,
|
||||||
|
LanguageManager languages,
|
||||||
|
AudioManager audio
|
||||||
|
)
|
||||||
|
{
|
||||||
|
languages.SetLanguage(settings.Language);
|
||||||
|
audio.MasterVolume = settings.Volume;
|
||||||
|
|
||||||
|
graphics.IsFullScreen = settings.Fullscreen;
|
||||||
|
graphics.SynchronizeWithVerticalRetrace = settings.VSync;
|
||||||
|
graphics.PreferredBackBufferWidth = settings.Width;
|
||||||
|
graphics.PreferredBackBufferHeight = settings.Height;
|
||||||
|
graphics.ApplyChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дев-инструмент: per-system тайминги Friflo. Включает мониторинг (<c>SetMonitorPerf</c>) на корнях
|
||||||
|
/// систем и печатает в дев-консоль командой <c>perf</c> отсортированную по времени таблицу систем
|
||||||
|
/// (среднее мс/кадр по последним кадрам + доля от корня). Доступно только в режиме разработчика.
|
||||||
|
/// </summary>
|
||||||
|
public static class PerfReport
|
||||||
|
{
|
||||||
|
private const int DefaultTop = 15; // сколько самых дорогих систем печатать по умолчанию
|
||||||
|
|
||||||
|
/// <summary>Окно усреднения времени (последние N обновлений) — используют и консоль, и perf-окно.</summary>
|
||||||
|
public const int AvgFrames = 30;
|
||||||
|
|
||||||
|
private static readonly List<(string Name, double AvgMs, int Updates)> Scratch = []; // без аллокаций на вызов
|
||||||
|
|
||||||
|
/// <summary>Включает мониторинг на корнях и регистрирует команду <c>perf [N]</c> (топ-N систем).</summary>
|
||||||
|
public static void Register(DevConsole console, params SystemRoot[] roots)
|
||||||
|
{
|
||||||
|
EnableMonitoring(roots);
|
||||||
|
console.Register(
|
||||||
|
"perf",
|
||||||
|
"perf [N] — тайминги систем (среднее мс/кадр, топ-N по времени)",
|
||||||
|
(c, args) =>
|
||||||
|
{
|
||||||
|
// Мониторинг пере-утверждаем на случай систем, добавленных после Register (идемпотентно).
|
||||||
|
EnableMonitoring(roots);
|
||||||
|
var top =
|
||||||
|
args.Length > 0 && int.TryParse(args[0], out var n) && n > 0 ? n : DefaultTop;
|
||||||
|
foreach (var root in roots)
|
||||||
|
{
|
||||||
|
WriteRoot(c, root, top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Включает per-system мониторинг Friflo на корнях (идемпотентно; можно звать повторно для
|
||||||
|
/// систем, добавленных позже). Публично — чтобы perf-окно тоже могло его утвердить.</summary>
|
||||||
|
public static void EnableMonitoring(params SystemRoot[] roots)
|
||||||
|
{
|
||||||
|
foreach (var root in roots)
|
||||||
|
{
|
||||||
|
root.SetMonitorPerf(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Заполняет <paramref name="into"/> строками (имя, среднее мс/кадр, число обновлений) по листовым
|
||||||
|
/// системам корня, сортированными по времени убыв.; возвращает суммарное среднее мс/кадр корня
|
||||||
|
/// (0, если данных ещё нет). Общий сбор для консольной команды и perf-окна — без дублирования.
|
||||||
|
/// </summary>
|
||||||
|
public static double Snapshot(
|
||||||
|
SystemRoot root,
|
||||||
|
List<(string Name, double AvgMs, int Updates)> into
|
||||||
|
)
|
||||||
|
{
|
||||||
|
into.Clear();
|
||||||
|
Collect(root, into);
|
||||||
|
into.Sort((a, b) => b.AvgMs.CompareTo(a.AvgMs));
|
||||||
|
return root.Perf.UpdateCount == 0 ? 0.0 : root.Perf.LastAvgMs(AvgFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteRoot(DevConsole console, SystemRoot root, int top)
|
||||||
|
{
|
||||||
|
var total = Snapshot(root, Scratch);
|
||||||
|
if (root.Perf.UpdateCount == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine($"[{root.Name}] нет данных — подожди кадр и повтори 'perf'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.WriteLine($"[{root.Name}] {total:0.000} мс/кадр · систем: {Scratch.Count}");
|
||||||
|
var shown = System.Math.Min(top, Scratch.Count);
|
||||||
|
for (var i = 0; i < shown; i++)
|
||||||
|
{
|
||||||
|
var (name, avg, _) = Scratch[i];
|
||||||
|
var pct = total > 0.0001 ? avg / total * 100.0 : 0.0;
|
||||||
|
console.WriteLine($" {name, -30} {avg, 8:0.000} мс {pct, 3:0}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Scratch.Count > shown)
|
||||||
|
{
|
||||||
|
console.WriteLine($" … ещё {Scratch.Count - shown} систем(ы)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рекурсивно собирает листовые системы (группы разворачиваются — интересны конкретные системы).
|
||||||
|
private static void Collect(SystemGroup group, List<(string, double, int)> into)
|
||||||
|
{
|
||||||
|
foreach (var child in group.ChildSystems)
|
||||||
|
{
|
||||||
|
if (child is SystemGroup sub)
|
||||||
|
{
|
||||||
|
Collect(sub, into);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
into.Add((child.Name, child.Perf.LastAvgMs(AvgFrames), child.Perf.UpdateCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сериализуемое состояние одного животного (фазы A1–A8): вид, позиция, геном (источник признаков и
|
||||||
|
/// пола), поколение, стадия/возраст, нужды, здоровье (кровь, HP частей, хедифы), настроение и
|
||||||
|
/// беременность. Спрайт пересобирается фабрикой из дефа. Чтобы популяция (эволюция/состояние)
|
||||||
|
/// переживала сейв/лоад, а не пересыпалась заново из сида.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа вида — по нему берётся индекс/стадии/текстуры/тело.</summary>
|
||||||
|
public string Species { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Номер поколения (для popstats/дрейфа).</summary>
|
||||||
|
public int Generation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Текущая стадия роста.</summary>
|
||||||
|
public int Stage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||||
|
public float AgeDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Геном особи: аллели по id генов (переменного состава; несёт и пол через GeneSex).</summary>
|
||||||
|
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Значения нужд по индексам NeedSet.</summary>
|
||||||
|
public float[] Needs { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Накоплено дней истощения.</summary>
|
||||||
|
public float Starve { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Уровень крови [0,1].</summary>
|
||||||
|
public float BloodLevel { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>HP частей тела по порядку анатомии вида (пусто — у вида нет тела).</summary>
|
||||||
|
public float[] PartHp { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Активные хедифы (гон/болезни).</summary>
|
||||||
|
public List<HediffSave> Hediffs { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Текущее настроение [0,1].</summary>
|
||||||
|
public float Mood { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Активно ли настроение (тир 4+).</summary>
|
||||||
|
public bool MoodActive { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Активные событийные мысли.</summary>
|
||||||
|
public List<ThoughtSave> Thoughts { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Беременна ли самка.</summary>
|
||||||
|
public bool Pregnant { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Геном отца (если беременна) — «слепок» на момент зачатия.</summary>
|
||||||
|
public Dictionary<string, Allele> FatherGenome { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||||
|
public float DueInDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Размер помёта.</summary>
|
||||||
|
public int Litter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Уровни навыков [0..1] по индексам SkillSet.</summary>
|
||||||
|
public float[] SkillLevels { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Страсть к навыкам (0/1/2) по индексам SkillSet.</summary>
|
||||||
|
public int[] SkillPassions { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Сериализуемый труп (фаза A5): вид (для спрайта), позиция, таймер/стадия разложения, мясо.</summary>
|
||||||
|
public sealed class CorpseSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа вида — по нему берётся спрайт трупа.</summary>
|
||||||
|
public string Species { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||||
|
public float RotDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Размер тела (для масштаба спрайта).</summary>
|
||||||
|
public float BodySize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Доступное мясо.</summary>
|
||||||
|
public float Meat { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.</summary>
|
||||||
|
public sealed class EggSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.</summary>
|
||||||
|
public string Species { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Поколение будущего детёныша.</summary>
|
||||||
|
public int Generation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Остаток инкубации в игровых днях.</summary>
|
||||||
|
public float IncubateDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Геном будущего детёныша (уже скрещён на момент кладки).</summary>
|
||||||
|
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Сериализуемый хедиф особи (фаза A6): деф (по имени), тяжесть и набранный иммунитет.</summary>
|
||||||
|
public sealed class HediffSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа хедифа (гон/болезнь).</summary>
|
||||||
|
public string DefName { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Текущая тяжесть.</summary>
|
||||||
|
public float Severity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Набранный иммунитет [0,1].</summary>
|
||||||
|
public float Immunity { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
|
||||||
|
public sealed class PlantSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
|
||||||
|
public string Species { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||||
|
public float AgeDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Текущая стадия роста.</summary>
|
||||||
|
public int Stage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Плодородность клетки.</summary>
|
||||||
|
public float CellFertility { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
|
||||||
|
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
|
||||||
|
public sealed class SaveStore
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly string _directory;
|
||||||
|
|
||||||
|
/// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
|
||||||
|
public SaveStore(string? directory = null) =>
|
||||||
|
_directory = directory ?? GamePaths.SavesDirectory;
|
||||||
|
|
||||||
|
/// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
|
||||||
|
public readonly record struct Entry(string FileName, WorldSave Save);
|
||||||
|
|
||||||
|
/// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
|
||||||
|
public IReadOnlyList<Entry> List()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_directory))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new List<Entry>();
|
||||||
|
foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var save = JsonSerializer.Deserialize<WorldSave>(
|
||||||
|
File.ReadAllText(file),
|
||||||
|
JsonOptions
|
||||||
|
);
|
||||||
|
// Сейв из будущей (несовместимой) версии прочитать корректно нельзя — пропускаем,
|
||||||
|
// чтобы не выдать пользователю молча битый мир.
|
||||||
|
if (save is not null && save.Version <= WorldSave.CurrentVersion)
|
||||||
|
{
|
||||||
|
entries.Add(new Entry(Path.GetFileName(file), save));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (exception is IOException or JsonException)
|
||||||
|
{
|
||||||
|
// Битый файл сохранения не должен ломать список — пропускаем.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
|
||||||
|
public string Write(WorldSave save)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_directory);
|
||||||
|
var fileName = Sanitize(save.Name) + ".json";
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(_directory, fileName),
|
||||||
|
JsonSerializer.Serialize(save, JsonOptions)
|
||||||
|
);
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Удаляет файл сохранения по имени.</summary>
|
||||||
|
public void Delete(string fileName)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_directory, fileName);
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Набор недопустимых для имени файла символов — кэшируем один раз (раньше массив аллоцировался
|
||||||
|
// и линейно сканировался на КАЖДЫЙ символ имени).
|
||||||
|
private static readonly HashSet<char> InvalidFileNameChars = new(
|
||||||
|
Path.GetInvalidFileNameChars()
|
||||||
|
);
|
||||||
|
|
||||||
|
private static string Sanitize(string name)
|
||||||
|
{
|
||||||
|
var trimmed = name.Trim();
|
||||||
|
var safe = string.Concat(trimmed.Select(c => InvalidFileNameChars.Contains(c) ? '_' : c));
|
||||||
|
return string.IsNullOrEmpty(safe) ? "world" : safe;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания.</summary>
|
||||||
|
public sealed class ThoughtSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа мысли.</summary>
|
||||||
|
public string DefName { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||||
|
public float RemainingDays { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace LittleSim.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||||
|
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||||
|
/// детерминированно регенерируется из сида при загрузке.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldSave
|
||||||
|
{
|
||||||
|
/// <summary>Текущая версия схемы сейва (поднимать при несовместимых изменениях полей).</summary>
|
||||||
|
public const int CurrentVersion = 1;
|
||||||
|
|
||||||
|
/// <summary>Версия схемы файла. Старые сейвы без поля совместимы (схема аддитивна) и читаются как
|
||||||
|
/// текущая версия; проверка нужна, чтобы отклонить файл из БУДУЩЕЙ (несовместимой) версии.</summary>
|
||||||
|
public int Version { get; set; } = CurrentVersion;
|
||||||
|
|
||||||
|
/// <summary>Имя мира (и основа имени файла).</summary>
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Ширина мира в клетках.</summary>
|
||||||
|
public int Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Высота мира в клетках.</summary>
|
||||||
|
public int Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Сид генерации.</summary>
|
||||||
|
public int Seed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Число проходов сглаживания рельефа.</summary>
|
||||||
|
public int SmoothPasses { get; set; } = 4;
|
||||||
|
|
||||||
|
/// <summary>Население на момент сохранения.</summary>
|
||||||
|
public int Population { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Момент сохранения (UTC).</summary>
|
||||||
|
public DateTime SavedUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Прошедшее время симуляции в секундах.</summary>
|
||||||
|
public double ElapsedSeconds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||||
|
public List<PlantSave> Plants { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность).</summary>
|
||||||
|
public List<AnimalSave> Animals { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
|
||||||
|
public List<CorpseSave> Corpses { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Снимок яиц (вид, позиция, геном детёныша, поколение, инкубация).</summary>
|
||||||
|
public List<EggSave> Eggs { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||||
|
public WorldConfig ToConfig() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Width = Width,
|
||||||
|
Height = Height,
|
||||||
|
Seed = Seed,
|
||||||
|
SmoothPasses = SmoothPasses,
|
||||||
|
Population = Population,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using MrGameEng.Genetics;
|
|
||||||
|
|
||||||
namespace LittleSim.App;
|
|
||||||
|
|
||||||
/// <summary>Сериализуемое состояние одного растения: вид (деф), позиция, возраст, стадия, почва и геном.</summary>
|
|
||||||
public sealed class PlantSave
|
|
||||||
{
|
|
||||||
/// <summary>Имя дефа вида — по нему при загрузке берётся индекс/стадии/текстуры.</summary>
|
|
||||||
public string Species { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>Позиция X в мировых координатах.</summary>
|
|
||||||
public float X { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
|
||||||
public float Y { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Накопленный возраст в игровых днях.</summary>
|
|
||||||
public float AgeDays { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Текущая стадия роста.</summary>
|
|
||||||
public int Stage { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Плодородность клетки.</summary>
|
|
||||||
public float CellFertility { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
|
|
||||||
public Dictionary<string, Allele> Genome { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
|
|
||||||
public sealed class PawnSave
|
|
||||||
{
|
|
||||||
/// <summary>Имя дефа жителя — по нему при загрузке берётся текстура из атласов.</summary>
|
|
||||||
public string DefName { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>Позиция X в мировых координатах.</summary>
|
|
||||||
public float X { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Позиция Y в мировых координатах.</summary>
|
|
||||||
public float Y { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Масштаб трансформа (зависит от размера дефа и клетки).</summary>
|
|
||||||
public float Scale { get; set; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Запас энергии [0, 1].</summary>
|
|
||||||
public float Energy { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Текущее действие (значение <see cref="Sim.PawnAction"/>).</summary>
|
|
||||||
public int Action { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
|
||||||
public float DecideIn { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Направление блуждания X.</summary>
|
|
||||||
public float DirX { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Направление блуждания Y.</summary>
|
|
||||||
public float DirY { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Секунды до смены направления.</summary>
|
|
||||||
public float ChangeIn { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
|
||||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
|
||||||
/// детерминированно регенерируется из сида при загрузке.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WorldSave
|
|
||||||
{
|
|
||||||
/// <summary>Имя мира (и основа имени файла).</summary>
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
|
|
||||||
/// <summary>Ширина мира в клетках.</summary>
|
|
||||||
public int Width { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Высота мира в клетках.</summary>
|
|
||||||
public int Height { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Сид генерации.</summary>
|
|
||||||
public int Seed { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Число проходов сглаживания рельефа.</summary>
|
|
||||||
public int SmoothPasses { get; set; } = 4;
|
|
||||||
|
|
||||||
/// <summary>Население на момент сохранения.</summary>
|
|
||||||
public int Population { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Момент сохранения (UTC).</summary>
|
|
||||||
public DateTime SavedUtc { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Прошедшее время симуляции в секундах.</summary>
|
|
||||||
public double ElapsedSeconds { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Снимок жителей.</summary>
|
|
||||||
public List<PawnSave> Pawns { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
|
||||||
public List<PlantSave> Plants { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
|
||||||
public WorldConfig ToConfig() =>
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Name = Name,
|
|
||||||
Width = Width,
|
|
||||||
Height = Height,
|
|
||||||
Seed = Seed,
|
|
||||||
SmoothPasses = SmoothPasses,
|
|
||||||
Population = Population,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Список/чтение/запись/удаление файлов сохранений <c>Saves/*.json</c>.</summary>
|
|
||||||
public sealed class SaveStore
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly string _directory;
|
|
||||||
|
|
||||||
/// <summary>Создаёт хранилище поверх каталога (по умолчанию <see cref="GamePaths.SavesDirectory"/>).</summary>
|
|
||||||
public SaveStore(string? directory = null) =>
|
|
||||||
_directory = directory ?? GamePaths.SavesDirectory;
|
|
||||||
|
|
||||||
/// <summary>Запись сохранения: один файл на имя мира (повторное сохранение перезаписывает).</summary>
|
|
||||||
public readonly record struct Entry(string FileName, WorldSave Save);
|
|
||||||
|
|
||||||
/// <summary>Все сохранения, новые сверху. Битые файлы пропускаются.</summary>
|
|
||||||
public IReadOnlyList<Entry> List()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(_directory))
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries = new List<Entry>();
|
|
||||||
foreach (var file in Directory.EnumerateFiles(_directory, "*.json"))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var save = JsonSerializer.Deserialize<WorldSave>(
|
|
||||||
File.ReadAllText(file),
|
|
||||||
JsonOptions
|
|
||||||
);
|
|
||||||
if (save is not null)
|
|
||||||
{
|
|
||||||
entries.Add(new Entry(Path.GetFileName(file), save));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is IOException or JsonException)
|
|
||||||
{
|
|
||||||
// Битый файл сохранения не должен ломать список — пропускаем.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.Sort((a, b) => b.Save.SavedUtc.CompareTo(a.Save.SavedUtc));
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Пишет сохранение в файл по имени мира и возвращает имя файла.</summary>
|
|
||||||
public string Write(WorldSave save)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_directory);
|
|
||||||
var fileName = Sanitize(save.Name) + ".json";
|
|
||||||
File.WriteAllText(
|
|
||||||
Path.Combine(_directory, fileName),
|
|
||||||
JsonSerializer.Serialize(save, JsonOptions)
|
|
||||||
);
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Удаляет файл сохранения по имени.</summary>
|
|
||||||
public void Delete(string fileName)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(_directory, fileName);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Sanitize(string name)
|
|
||||||
{
|
|
||||||
var trimmed = name.Trim();
|
|
||||||
var safe = string.Concat(
|
|
||||||
trimmed.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)
|
|
||||||
);
|
|
||||||
return string.IsNullOrEmpty(safe) ? "world" : safe;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Готовая (resolved) таблица животных для сцены: по каждому <see cref="AnimalDef"/> — загруженный
|
||||||
|
/// регион атласа и набор генов вида (<see cref="GenomeTemplate"/>) для генерации особей. Строится
|
||||||
|
/// один раз при загрузке сцены (нужны атлас и устройство), как <see cref="PlantSet"/>. Системы и
|
||||||
|
/// фабрика индексируются в неё без словарей. Несёт направленные спрайты (восток/север/юг + флип) по
|
||||||
|
/// полу/стадии, спрайты трупа/яйца, анатомию, шаблон генома и предпосчёты для горячих путей.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalSet
|
||||||
|
{
|
||||||
|
/// <summary>Направленные спрайты одной «внешности» (пол/стадия): восток, север, юг. Запад = зеркало
|
||||||
|
/// востока (флип по X). Север/юг берутся по конвенции имени (_east→_north/_south), иначе = восток.</summary>
|
||||||
|
public readonly record struct Directional(
|
||||||
|
Texture2DRegion East,
|
||||||
|
Texture2DRegion North,
|
||||||
|
Texture2DRegion South
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи.</summary>
|
||||||
|
public sealed class Species
|
||||||
|
{
|
||||||
|
/// <summary>Исходный деф животного.</summary>
|
||||||
|
public required AnimalDef Def { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Направленные спрайты взрослой самки (она же общий/fallback).</summary>
|
||||||
|
public required Directional FemaleDirs { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Направленные спрайты взрослого самца (fallback — самка).</summary>
|
||||||
|
public required Directional MaleDirs { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Направленные спрайты детёныша/подростка (fallback — самка).</summary>
|
||||||
|
public required Directional BabyDirs { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Спрайт трупа (fallback — самка, восток).</summary>
|
||||||
|
public required Texture2DRegion Corpse { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
|
||||||
|
public required Texture2DRegion Egg { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Стадии роста (из дефа или встроенный дефолт), по возрастанию возраста входа.</summary>
|
||||||
|
public required AnimalStageDef[] Stages { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Анатомия вида (части тела) или null, если вид без тела.</summary>
|
||||||
|
public required BodyDef? Body { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Набор генов вида: базовые значения для генерации особи.</summary>
|
||||||
|
public required GenomeTemplate Template { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей
|
||||||
|
/// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле.</summary>
|
||||||
|
public required float RefBodySize { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Направленные спрайты для стадии/пола: детёныш → BabyDirs; иначе по полу (самец/самка).</summary>
|
||||||
|
public Directional LookDirs(int stage, bool isMale) =>
|
||||||
|
string.Equals(Stages[stage].Texture, "baby", StringComparison.Ordinal) ? BabyDirs
|
||||||
|
: isMale ? MaleDirs
|
||||||
|
: FemaleDirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
|
||||||
|
private static readonly AnimalStageDef[] DefaultStages =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Baby",
|
||||||
|
EnterAt = 0f,
|
||||||
|
RelativeTo = "maturity",
|
||||||
|
Scale = 0.45f,
|
||||||
|
Texture = "baby",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Juvenile",
|
||||||
|
EnterAt = 0.22f,
|
||||||
|
RelativeTo = "maturity",
|
||||||
|
Scale = 0.7f,
|
||||||
|
Texture = "baby",
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Adult",
|
||||||
|
EnterAt = 1f,
|
||||||
|
RelativeTo = "maturity",
|
||||||
|
Scale = 1f,
|
||||||
|
Texture = "adult",
|
||||||
|
Adult = true,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = "Senior",
|
||||||
|
EnterAt = 0.8f,
|
||||||
|
RelativeTo = "lifespan",
|
||||||
|
Scale = 0.92f,
|
||||||
|
Texture = "adult",
|
||||||
|
Adult = true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly Species[] _species;
|
||||||
|
private readonly Dictionary<AnimalDef, int> _index = new();
|
||||||
|
|
||||||
|
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков животных.</summary>
|
||||||
|
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
|
||||||
|
|
||||||
|
/// <summary>Строит таблицу из всех (не-abstract) <see cref="AnimalDef"/> мода.</summary>
|
||||||
|
public AnimalSet(GameContent content, ModAtlases atlases, GraphicsDevice device)
|
||||||
|
{
|
||||||
|
var genes = content
|
||||||
|
.Defs.All<GeneDef>()
|
||||||
|
.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||||||
|
GeneRegistry = genes;
|
||||||
|
|
||||||
|
var defs = content.Defs.All<AnimalDef>().ToArray();
|
||||||
|
_species = new Species[defs.Length];
|
||||||
|
for (var i = 0; i < defs.Length; i++)
|
||||||
|
{
|
||||||
|
var def = defs[i];
|
||||||
|
var femaleKey = def.Texture;
|
||||||
|
var maleKey = string.IsNullOrEmpty(def.MaleTexture) ? femaleKey : def.MaleTexture;
|
||||||
|
var babyKey = string.IsNullOrEmpty(def.BabyTexture) ? femaleKey : def.BabyTexture;
|
||||||
|
_species[i] = new Species
|
||||||
|
{
|
||||||
|
Def = def,
|
||||||
|
FemaleDirs = LoadDirs(atlases, device, femaleKey),
|
||||||
|
MaleDirs = LoadDirs(atlases, device, maleKey),
|
||||||
|
BabyDirs = LoadDirs(atlases, device, babyKey),
|
||||||
|
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
|
||||||
|
? atlases.GetRegion(device, femaleKey)
|
||||||
|
: atlases.GetRegion(device, def.CorpseTexture),
|
||||||
|
Egg = string.IsNullOrEmpty(def.EggTexture)
|
||||||
|
? atlases.GetRegion(device, femaleKey)
|
||||||
|
: atlases.GetRegion(device, def.EggTexture),
|
||||||
|
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
|
||||||
|
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
|
||||||
|
Template = BuildTemplate(def.Genome, genes),
|
||||||
|
RefBodySize =
|
||||||
|
def.Genome.TryGetValue("GeneMaxBodySize", out var refSize) && refSize > 0f
|
||||||
|
? refSize
|
||||||
|
: 1f,
|
||||||
|
};
|
||||||
|
_index[def] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Грузит направленный набор по восточному ключу: север/юг — по конвенции имени (_east→_north/_south),
|
||||||
|
// если такой регион есть в атласе; иначе откат на восток (вид с одним спрайтом просто не разворачивается
|
||||||
|
// вверх/вниз, но влево/вправо — флипом востока). Запад всегда = зеркало востока.
|
||||||
|
private static Directional LoadDirs(ModAtlases atlases, GraphicsDevice device, string eastKey)
|
||||||
|
{
|
||||||
|
var east = atlases.GetRegion(device, eastKey);
|
||||||
|
return new Directional(
|
||||||
|
east,
|
||||||
|
TryDir(atlases, device, eastKey, "_north", east),
|
||||||
|
TryDir(atlases, device, eastKey, "_south", east)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture2DRegion TryDir(
|
||||||
|
ModAtlases atlases,
|
||||||
|
GraphicsDevice device,
|
||||||
|
string eastKey,
|
||||||
|
string suffix,
|
||||||
|
Texture2DRegion fallback
|
||||||
|
) =>
|
||||||
|
eastKey.Contains("_east", StringComparison.Ordinal)
|
||||||
|
&& atlases.TryGetRegion(
|
||||||
|
device,
|
||||||
|
eastKey.Replace("_east", suffix, StringComparison.Ordinal),
|
||||||
|
out var region
|
||||||
|
)
|
||||||
|
? region
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
/// <summary>Число видов в таблице.</summary>
|
||||||
|
public int Count => _species.Length;
|
||||||
|
|
||||||
|
/// <summary>Вид по индексу.</summary>
|
||||||
|
public Species this[int index] => _species[index];
|
||||||
|
|
||||||
|
/// <summary>Индекс вида по дефу.</summary>
|
||||||
|
public int IndexOf(AnimalDef def) => _index[def];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Генерирует геном новой особи-основателя вида: аллели из шаблона + явно назначенный пол
|
||||||
|
/// (XX самка / XY самец, 50/50) — чтобы не возник невозможный YY (см. Defs/Genes/Animal.json GeneSex).
|
||||||
|
/// Скрещивание (A4) наследует пол само (мать XX отдаёт только X).
|
||||||
|
/// </summary>
|
||||||
|
public Genome GenerateGenome(int species, Random random)
|
||||||
|
{
|
||||||
|
var genome = _species[species].Template.Generate(random);
|
||||||
|
genome["GeneSex"] = random.NextSingle() < 0.5f ? new Allele(0f, 0f) : new Allele(0f, 1f);
|
||||||
|
return genome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ОСНОВА создания гибрида (полноценная команда/UI — позже): геном помеси, привязанный к базовому виду
|
||||||
|
/// <paramref name="baseSpecies"/> (его тело/спрайт/набор генов), но с общими генами, скрещёнными с
|
||||||
|
/// <paramref name="otherSpecies"/> (по одной аллели-гамете от каждого родителя, как при размножении).
|
||||||
|
/// Пол берётся от базового вида; гены, которых нет у базового, игнорируются (его тело их не читает).
|
||||||
|
/// Сущность создаётся обычной <see cref="Sim.AnimalFactory.Create"/> с этим геномом и базовым видом —
|
||||||
|
/// то есть «создать любой вид» = Create(вид, GenerateGenome), «любой гибрид» = Create(вид, HybridGenome).
|
||||||
|
/// </summary>
|
||||||
|
public Genome HybridGenome(int baseSpecies, int otherSpecies, Random random)
|
||||||
|
{
|
||||||
|
var hybrid = GenerateGenome(baseSpecies, random);
|
||||||
|
var other = _species[otherSpecies].Template.Generate(random);
|
||||||
|
foreach (var (geneId, allele) in other.ToDictionary())
|
||||||
|
{
|
||||||
|
if (string.Equals(geneId, "GeneSex", StringComparison.Ordinal) || !hybrid.Has(geneId))
|
||||||
|
{
|
||||||
|
continue; // пол — от базового вида; чужие гены базовое тело не читает
|
||||||
|
}
|
||||||
|
|
||||||
|
var mine = hybrid[geneId];
|
||||||
|
hybrid[geneId] = new Allele(
|
||||||
|
random.NextSingle() < 0.5f ? mine.A : mine.B,
|
||||||
|
random.NextSingle() < 0.5f ? allele.A : allele.B
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hybrid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Строит шаблон генома вида из ДАННЫХ (geneId→base из Defs/Animals/) generically: разброс берётся из
|
||||||
|
// самого гена. Состав открыт — модер добавляет ген строкой JSON, без правки кода. Дискретные гены
|
||||||
|
// (база/разброс не нужны — варианты из GeneDef) тоже поддержаны.
|
||||||
|
private static GenomeTemplate BuildTemplate(
|
||||||
|
IReadOnlyDictionary<string, float> genome,
|
||||||
|
IReadOnlyDictionary<string, GeneDef> genes
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var entries = new List<GenomeTemplate.Entry>(genome.Count);
|
||||||
|
foreach (var (geneId, baseValue) in genome)
|
||||||
|
{
|
||||||
|
if (!genes.TryGetValue(geneId, out var gene))
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException(
|
||||||
|
$"Animal genome references unknown gene '{geneId}' — check Defs/Genes/."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var spread = gene.Kind == GeneKind.Discrete ? 0f : gene.Spread;
|
||||||
|
entries.Add(new GenomeTemplate.Entry(gene, baseValue, spread));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenomeTemplate(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вид-животное (Defs/Animals/): подтип <see cref="PawnDef"/> (текстура/размер/вид) плюс геном
|
||||||
|
/// вида. Поведение, здоровье и стадии живут на компонентах сущности (фазы A2+), а не на типе дефа —
|
||||||
|
/// существующие 9 животных-<see cref="PawnDef"/> остаются валидными.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalDef : PawnDef
|
||||||
|
{
|
||||||
|
/// <summary>Текстура взрослого самца (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/> (самка/общая).</summary>
|
||||||
|
public string MaleTexture { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Текстура детёныша/подростка (фаза A3); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||||
|
public string BabyTexture { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Текстура трупа (фаза A5); пусто — берётся <see cref="PawnDef.Texture"/>.</summary>
|
||||||
|
public string CorpseTexture { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Текстура яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт семени-точки.</summary>
|
||||||
|
public string EggTexture { get; init; } = "things/plant/seed_default";
|
||||||
|
|
||||||
|
/// <summary>Эталонная масса (кг) взрослой особи базового размера вида; масса особи ∝ (размер/эталон)³.</summary>
|
||||||
|
public float BaseMassKg { get; init; } = 40f;
|
||||||
|
|
||||||
|
/// <summary>Имя <see cref="BodyDef"/> — анатомия вида (части тела/органы); пусто — без частей тела.</summary>
|
||||||
|
public string Body { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Геном вида как ДАННЫЕ: ключ — id гена (<see cref="MrGameEng.Genetics.GeneDef"/>), значение —
|
||||||
|
/// центр аллелей особи. Состав открыт — модер добавляет/убирает ген одной строкой JSON, без правки
|
||||||
|
/// кода; разброс берётся из самого гена (<c>GeneDef.Spread</c>). Дискретные гены (напр. пол) сюда
|
||||||
|
/// не пишутся — назначаются генерацией.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, float> Genome { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Диета как ДАННЫЕ: что вид ест ("plant" — травоядное; "meat" — хищник/падальщик; оба — всеядное).
|
||||||
|
/// Поиск корма гейтится этим списком — модер задаёт рацион без правки кода.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Diet { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Стадии роста (данные); пусто — встроенный набор Baby→Juvenile→Adult→Senior по умолчанию.
|
||||||
|
/// </summary>
|
||||||
|
public AnimalStageDef[] Stages { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>Плотность стартового спавна: голов на 1000 клеток суши (0 — вид не спавнится сам).</summary>
|
||||||
|
public float SpawnPer1000Cells { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Базовая скорость передвижения (мировых единиц/сек при гене moveSpeed = 1).</summary>
|
||||||
|
public float BaseSpeed { get; init; } = 28f;
|
||||||
|
|
||||||
|
/// <summary>Базовый радиус восприятия в клетках (× ген vision) — поиск корма/воды/партнёра.</summary>
|
||||||
|
public float VisionCells { get; init; } = 14f;
|
||||||
|
|
||||||
|
/// <summary>Сила выедания: на сколько игровых дней роста убавляется растение за день кормёжки.</summary>
|
||||||
|
public float ForageBiteDays { get; init; } = 40f;
|
||||||
|
|
||||||
|
/// <summary>Дальность атаки в клетках: на этом расстоянии хищник кусает добычу (контакт).</summary>
|
||||||
|
public float AttackRangeCells { get; init; } = 1.2f;
|
||||||
|
|
||||||
|
/// <summary>Игровые секунды между ударами (кулдаун дискретной атаки, как в RimWorld).</summary>
|
||||||
|
public float AttackIntervalSec { get; init; } = 1.8f;
|
||||||
|
|
||||||
|
/// <summary>Урон части тела добычи ЗА ОДИН УДАР (×силу: размер×навык). 0 — вид не атакует.</summary>
|
||||||
|
public float AttackDamage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Прирост тяжести раны-кровотечения у добычи за один удар (×силу).</summary>
|
||||||
|
public float AttackBleed { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Острая кровопотеря добычи за один удар (доля объёма крови, ×силу) — основной путь к смерти.</summary>
|
||||||
|
public float AttackBloodLoss { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Стадия роста животного как ДАННЫЕ (вложенный объект <see cref="AnimalDef.Stages"/>): возраст входа =
|
||||||
|
/// <see cref="EnterAt"/> × якорь (<see cref="RelativeTo"/> — созревание или продолжительность жизни),
|
||||||
|
/// масштаб тела и набор текстур ("baby"/"adult"). Стадии перечисляются по возрастанию возраста входа.
|
||||||
|
/// Модер задаёт любой набор стадий (личинка/куколка/имаго и т.п.) без правки кода.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalStageDef
|
||||||
|
{
|
||||||
|
/// <summary>Имя стадии (для тултипов/отладки).</summary>
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Множитель к якорю: возраст входа = EnterAt × (созревание | продолжительность жизни).</summary>
|
||||||
|
public float EnterAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Якорь возраста входа: "maturity" (созревание) или "lifespan" (продолжительность жизни).</summary>
|
||||||
|
public string RelativeTo { get; init; } = "maturity";
|
||||||
|
|
||||||
|
/// <summary>Масштаб размера тела на этой стадии (× ген размера).</summary>
|
||||||
|
public float Scale { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Набор текстур: "baby" (детёныш) или "adult" (по полу — самец/самка).</summary>
|
||||||
|
public string Texture { get; init; } = "adult";
|
||||||
|
|
||||||
|
/// <summary>Взрослая стадия — способна размножаться.</summary>
|
||||||
|
public bool Adult { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Анатомия вида (Defs/Bodies/): дерево частей тела/органов. Используется системой здоровья (фаза
|
||||||
|
/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BodyDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Части тела/органы вида.</summary>
|
||||||
|
public BodyPartDef[] Parts { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Часть тела/орган (вложенный объект <see cref="BodyDef.Parts"/>) как ДАННЫЕ: иерархия (родитель),
|
||||||
|
/// вес попадания, максимум HP (масштабируется размером тела), флаг жизненной важности и вклады в
|
||||||
|
/// способности (capacity id → доля). Модер задаёт любую анатомию (8 ног, два сердца) без правки кода.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BodyPartDef
|
||||||
|
{
|
||||||
|
/// <summary>Имя части (уникально в теле); на него ссылается <see cref="Parent"/> детей.</summary>
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Имя родительской части ("" — корневая, напр. торс).</summary>
|
||||||
|
public string Parent { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Вес попадания (доля тела) — для будущего выбора задетой части.</summary>
|
||||||
|
public float Coverage { get; init; } = 0.1f;
|
||||||
|
|
||||||
|
/// <summary>Базовый максимум HP (× размер тела при создании особи).</summary>
|
||||||
|
public float MaxHp { get; init; } = 10f;
|
||||||
|
|
||||||
|
/// <summary>Жизненно важная: уничтожение → смерть.</summary>
|
||||||
|
public bool Vital { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Вклады в способности: capacity id (см. <c>AnimalCapacities</c>) → доля (сумма по телу ≈ 1).</summary>
|
||||||
|
public Dictionary<string, float> Capacities { get; init; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пункт дев-инструмента (Defs/DevTools/) — ЧТОБЫ МОДЫ ДОБАВЛЯЛИ кнопки в дев-спавнер без правки кода.
|
||||||
|
/// <see cref="Kind"/> — id обработчика из реестра (spawn-animal/spawn-plant/infect/damage/kill/…),
|
||||||
|
/// <see cref="Target"/> — параметр (defName вида/хедифа). Виды животных/растений спавнер берёт из их
|
||||||
|
/// дефов сам (модовый контент появляется автоматически); DevToolDef добавляет ПРОЧИЕ действия.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevToolDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Категория для группировки в панели (например "tool").</summary>
|
||||||
|
public string Category { get; init; } = "tool";
|
||||||
|
|
||||||
|
/// <summary>Id обработчика-инструмента из реестра кода (spawn-animal/spawn-plant/infect/damage/kill).</summary>
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Параметр инструмента: defName вида/хедифа и т.п. (по смыслу Kind).</summary>
|
||||||
|
public string Target { get; init; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
|
||||||
|
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
|
||||||
|
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GenomeDef
|
||||||
|
{
|
||||||
|
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
|
||||||
|
public float OptimalLight { get; init; } = 0.7f;
|
||||||
|
|
||||||
|
/// <summary>Ширина толерантности по свету.</summary>
|
||||||
|
public float LightTolerance { get; init; } = 0.4f;
|
||||||
|
|
||||||
|
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
||||||
|
public float OptimalTemperature { get; init; } = 18f;
|
||||||
|
|
||||||
|
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
|
||||||
|
public float TemperatureTolerance { get; init; } = 12f;
|
||||||
|
|
||||||
|
/// <summary>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
|
||||||
|
public float ColdHardiness { get; init; } = 8f;
|
||||||
|
|
||||||
|
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
|
||||||
|
public float HeatHardiness { get; init; } = 10f;
|
||||||
|
|
||||||
|
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
||||||
|
public float OptimalFertility { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Ширина толерантности по плодородности.</summary>
|
||||||
|
public float FertilityTolerance { get; init; } = 0.8f;
|
||||||
|
|
||||||
|
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
|
||||||
|
public float Vigor { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
||||||
|
public float Lifespan { get; init; } = 80f;
|
||||||
|
|
||||||
|
/// <summary>Дальность расселения семян (в клетках).</summary>
|
||||||
|
public float DispersalRange { get; init; } = 2f;
|
||||||
|
|
||||||
|
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
|
||||||
|
public float ReproduceInterval { get; init; } = 8f;
|
||||||
|
|
||||||
|
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
|
||||||
|
public float SelfPollination { get; init; } = 0.4f;
|
||||||
|
|
||||||
|
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
|
||||||
|
public float MutationRate { get; init; } = 0.05f;
|
||||||
|
|
||||||
|
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
|
||||||
|
public float VariantChance { get; init; } = 0.15f;
|
||||||
|
|
||||||
|
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||||
|
public float Spread { get; init; } = 0.08f;
|
||||||
|
|
||||||
|
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
|
||||||
|
public float FruitYield { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
|
||||||
|
public float FruitSeason { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
|
||||||
|
public float HarvestAmount { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||||
|
public float LeafHue { get; init; } = 0.33f;
|
||||||
|
|
||||||
|
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
|
||||||
|
public float Toxicity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
|
||||||
|
public float Thorns { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||||
|
public float Palatability { get; init; } = 1f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние/хедиф организма (Defs/Hediffs/): гон, болезни, позже раны/возрастные эффекты. Фаза A6:
|
||||||
|
/// болезнь прогрессирует (<see cref="SeverityPerDay"/>), параллельно растёт иммунитет
|
||||||
|
/// (<see cref="ImmunityPerDay"/>) — гонка: иммунитет→1 раньше = выздоровление, severity→
|
||||||
|
/// <see cref="LethalSeverity"/> раньше = смерть. Снижает способности (<see cref="CapMods"/>) и даёт боль.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HediffDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Начальная тяжесть при навешивании (гон — 1; болезнь — малая, напр. 0.05).</summary>
|
||||||
|
public float InitialSeverity { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Прирост тяжести за игровой день (0 — статичный хедиф вроде гона).</summary>
|
||||||
|
public float SeverityPerDay { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Прирост иммунитета за день (0 — иммунитета нет, не выздоравливает сам).</summary>
|
||||||
|
public float ImmunityPerDay { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Тяжесть, при которой наступает смерть (0 — не летально).</summary>
|
||||||
|
public float LethalSeverity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Боль при тяжести 1 (масштабируется тяжестью) — вход в сознание/настроение.</summary>
|
||||||
|
public float Pain { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Шанс подхватить фоном за день (0 — не фоновая болезнь).</summary>
|
||||||
|
public float AmbientPerDay { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кровопотеря за игровой день при тяжести 1 (масштабируется тяжестью): доля объёма крови, которую
|
||||||
|
/// рана теряет в сутки. 0 — хедиф не вызывает кровотечения. Кровь на нуле → смерть (см. система здоровья).
|
||||||
|
/// </summary>
|
||||||
|
public float BloodLossPerDay { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
|
||||||
|
public Dictionary<string, float> CapMods { get; init; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Нужда (Defs/Needs/) как ДАННЫЕ: имя, тип (deplete — убывает, низкое = срочно; drive — растёт под
|
||||||
|
/// условием, высокое = срочно), действие-исполнитель (по id, см. реестр поведений), темпы и порог
|
||||||
|
/// интеллекта. Модер добавляет нужду виду/всем одной записью; если она ссылается на существующее
|
||||||
|
/// действие — без правки кода. Хранилище нужд особи — динамическое (см. <c>NeedSet</c>/<c>AnimalNeeds</c>).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NeedDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Тип: "deplete" (убывает, утоляется действием) или "drive" (растёт под условием).</summary>
|
||||||
|
public string Kind { get; init; } = "deplete";
|
||||||
|
|
||||||
|
/// <summary>Id действия-исполнителя, удовлетворяющего нужду (Eat/Drink/Sleep/Mate/…).</summary>
|
||||||
|
public string Action { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Темп убывания за игровой день (drive: темп спада, когда условие отсутствует).</summary>
|
||||||
|
public float DecayPerDay { get; init; } = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>drive: темп роста за день, пока активно условие <see cref="RisesUnder"/>.</summary>
|
||||||
|
public float RisePerDay { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>deplete: насколько за день восполняет нужду исполнение действия.</summary>
|
||||||
|
public float FeedPerDay { get; init; } = 4f;
|
||||||
|
|
||||||
|
/// <summary>drive: id хедифа, под которым нужда растёт (напр. "Rut"); null — растёт всегда.</summary>
|
||||||
|
public string? RisesUnder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>deplete: на нуле копит счётчик голодной смерти (A3).</summary>
|
||||||
|
public bool Lethal { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Порог размера мозга, с которого нужда активна (гейт интеллектом — фаза A7).</summary>
|
||||||
|
public float MinBrain { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное). Не sealed — <see cref="AnimalDef"/> наследует.</summary>
|
||||||
|
public class PawnDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
||||||
|
public const string KindBeing = "being";
|
||||||
|
|
||||||
|
/// <summary>Вид животного.</summary>
|
||||||
|
public const string KindAnimal = "animal";
|
||||||
|
|
||||||
|
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
|
||||||
|
public string Texture { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Размер спрайта в клетках.</summary>
|
||||||
|
public float SizeCells { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||||
|
public sealed class PlantDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
||||||
|
public string Texture { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
|
||||||
|
public float SizeCells { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
|
||||||
|
public float TrunkRadiusCells { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
|
||||||
|
public List<PlantGrowthStage> Stages { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
|
||||||
|
public GenomeDef Genome { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
|
||||||
|
public string? HarvestProduct { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||||
|
public string? FruitProduct { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
|
||||||
|
public float BaseMassKg { get; init; } = 20f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Одна стадия роста растения (вложенный объект <see cref="PlantDef.Stages"/>): свой спрайт и
|
||||||
|
/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
|
||||||
|
/// Растёт на месте — последняя стадия терминальная (её <see cref="GrowDays"/> игнорируется).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PlantGrowthStage
|
||||||
|
{
|
||||||
|
/// <summary>Ключ текстуры стадии; null — берётся базовая <see cref="PlantDef.Texture"/>.</summary>
|
||||||
|
public string? Texture { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Размер спрайта в клетках на этой стадии; 0 — берётся базовый <see cref="PlantDef.SizeCells"/>.</summary>
|
||||||
|
public float SizeCells { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней расти на этой стадии до перехода к следующей.</summary>
|
||||||
|
public float GrowDays { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Ключ локализации названия стадии (опционально, для будущих тултипов).</summary>
|
||||||
|
public string? Label { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||||
|
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||||
|
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProductDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
|
||||||
|
public float MassKg { get; init; } = 1f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>Запись скаттера: с вероятностью <see cref="Chance"/> на клетку ставится одно растение из <see cref="Options"/>.</summary>
|
||||||
|
public sealed class ScatterEntry
|
||||||
|
{
|
||||||
|
/// <summary>Вероятность на клетку (0..1); записи бросаются независимо.</summary>
|
||||||
|
public float Chance { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Имена <see cref="PlantDef"/>, из которых выбирается равновероятно.</summary>
|
||||||
|
public List<string> Options { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
|
||||||
|
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
|
||||||
|
/// код по его id (как у нужд/действий): добыча→выпас, охота→урон, уклонение→бегство. Модер добавляет
|
||||||
|
/// навык строкой JSON; новый эффект — код. <see cref="Def.Label"/> — ключ локализации (skill.*).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SkillDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Категория (для группировки в UI), напр. "survival".</summary>
|
||||||
|
public string Category { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Скорость обучения за игровой день непрерывной практики (при страсти ×1).</summary>
|
||||||
|
public float LearnRate { get; init; } = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>Угасание уровня за игровой день без практики.</summary>
|
||||||
|
public float DecayPerDay { get; init; } = 0.02f;
|
||||||
|
|
||||||
|
/// <summary>Пол угасания (ниже не падает) — «не забывается совсем».</summary>
|
||||||
|
public float FloorLevel { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
||||||
|
public sealed class TerrainDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
||||||
|
public float MaxHeight { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Цвет клетки [r, g, b] для сцен с тонированными квадратами и тайлов без поверхности.</summary>
|
||||||
|
[JsonPropertyName("color")]
|
||||||
|
public List<int> Rgb { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
|
||||||
|
public bool IsLand { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Светонепроницаемый террейн — базовый окклюдер лайтмапа/теней (сейчас такого нет).</summary>
|
||||||
|
public bool BlocksLight { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
|
||||||
|
public string? Surface { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
|
||||||
|
public List<ScatterEntry> Scatter { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
|
||||||
|
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
|
||||||
|
/// </summary>
|
||||||
|
public float Fertility { get; init; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
|
||||||
|
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Мысль настроения (Defs/Thoughts/) как ДАННЫЕ (фаза A8, модель thoughts RimWorld): событие
|
||||||
|
/// (роды/спаривание/утоление жажды/…) вешает на особь временный сдвиг настроения, затухающий за
|
||||||
|
/// <see cref="DurationDays"/>. Триггеры событий — код (реестр <c>AnimalThoughts</c>), а величины/метка —
|
||||||
|
/// данные: модер правит силу и длительность мысли одной записью JSON, без пересборки.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThoughtDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Сдвиг настроения [-1,1] (положительный — радость, отрицательный — горе).</summary>
|
||||||
|
public float MoodOffset { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль живёт, прежде чем угаснуть.</summary>
|
||||||
|
public float DurationDays { get; init; } = 1f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using MrGameEng.Mods;
|
||||||
|
|
||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
|
||||||
|
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
||||||
|
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldPresetDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Ширина мира в клетках.</summary>
|
||||||
|
public int Width { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Высота мира в клетках.</summary>
|
||||||
|
public int Height { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Число жителей при генерации.</summary>
|
||||||
|
public int Population { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Порядок в списке UI (по возрастанию).</summary>
|
||||||
|
public int Order { get; init; }
|
||||||
|
}
|
||||||
@@ -63,6 +63,13 @@ public sealed class GameContent
|
|||||||
defs.RegisterType<ProductDef>("Product");
|
defs.RegisterType<ProductDef>("Product");
|
||||||
defs.RegisterType<PlantDef>("Plant");
|
defs.RegisterType<PlantDef>("Plant");
|
||||||
defs.RegisterType<PawnDef>("Pawn");
|
defs.RegisterType<PawnDef>("Pawn");
|
||||||
|
defs.RegisterType<AnimalDef>("Animal");
|
||||||
|
defs.RegisterType<HediffDef>("Hediff");
|
||||||
|
defs.RegisterType<NeedDef>("Need");
|
||||||
|
defs.RegisterType<ThoughtDef>("Thought");
|
||||||
|
defs.RegisterType<BodyDef>("Body");
|
||||||
|
defs.RegisterType<SkillDef>("Skill");
|
||||||
|
defs.RegisterType<DevToolDef>("DevTool");
|
||||||
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||||
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||||
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using MrGameEng.Mods;
|
|
||||||
|
|
||||||
namespace LittleSim.Content;
|
|
||||||
|
|
||||||
/// <summary>Запись скаттера: с вероятностью <see cref="Chance"/> на клетку ставится одно растение из <see cref="Options"/>.</summary>
|
|
||||||
public sealed class ScatterEntry
|
|
||||||
{
|
|
||||||
/// <summary>Вероятность на клетку (0..1); записи бросаются независимо.</summary>
|
|
||||||
public float Chance { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Имена <see cref="PlantDef"/>, из которых выбирается равновероятно.</summary>
|
|
||||||
public List<string> Options { get; init; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
|
||||||
public sealed class TerrainDef : Def
|
|
||||||
{
|
|
||||||
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
|
||||||
public float MaxHeight { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Цвет клетки [r, g, b] для сцен с тонированными квадратами и тайлов без поверхности.</summary>
|
|
||||||
[JsonPropertyName("color")]
|
|
||||||
public List<int> Rgb { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
|
|
||||||
public bool IsLand { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.</summary>
|
|
||||||
public bool BlocksLight { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
|
|
||||||
public string? Surface { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Растительность, рассыпаемая по клеткам этого террейна.</summary>
|
|
||||||
public List<ScatterEntry> Scatter { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Плодородность почвы — множитель скорости роста растений на этом террейне (1 — норма,
|
|
||||||
/// больше — быстрее). Богатая лесная почва растит быстрее песка/камня.
|
|
||||||
/// </summary>
|
|
||||||
public float Fertility { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Цвет тонировки из <see cref="Rgb"/>.</summary>
|
|
||||||
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Одна стадия роста растения (вложенный объект <see cref="PlantDef.Stages"/>): свой спрайт и
|
|
||||||
/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
|
|
||||||
/// Растёт на месте — последняя стадия терминальная (её <see cref="GrowDays"/> игнорируется).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PlantGrowthStage
|
|
||||||
{
|
|
||||||
/// <summary>Ключ текстуры стадии; null — берётся базовая <see cref="PlantDef.Texture"/>.</summary>
|
|
||||||
public string? Texture { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Размер спрайта в клетках на этой стадии; 0 — берётся базовый <see cref="PlantDef.SizeCells"/>.</summary>
|
|
||||||
public float SizeCells { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Сколько игровых дней расти на этой стадии до перехода к следующей.</summary>
|
|
||||||
public float GrowDays { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Ключ локализации названия стадии (опционально, для будущих тултипов).</summary>
|
|
||||||
public string? Label { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
|
|
||||||
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
|
||||||
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProductDef : Def
|
|
||||||
{
|
|
||||||
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
|
||||||
public string Kind { get; init; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
|
|
||||||
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
|
|
||||||
/// (разброс — <see cref="Spread"/>); фенотип = среднее аллелей.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GenomeDef
|
|
||||||
{
|
|
||||||
/// <summary>Оптимальная освещённость (0..1) и её толерантность.</summary>
|
|
||||||
public float OptimalLight { get; init; } = 0.7f;
|
|
||||||
|
|
||||||
/// <summary>Ширина толерантности по свету.</summary>
|
|
||||||
public float LightTolerance { get; init; } = 0.4f;
|
|
||||||
|
|
||||||
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
|
||||||
public float OptimalTemperature { get; init; } = 18f;
|
|
||||||
|
|
||||||
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
|
|
||||||
public float TemperatureTolerance { get; init; } = 12f;
|
|
||||||
|
|
||||||
/// <summary>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
|
|
||||||
public float ColdHardiness { get; init; } = 8f;
|
|
||||||
|
|
||||||
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
|
|
||||||
public float HeatHardiness { get; init; } = 10f;
|
|
||||||
|
|
||||||
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
|
||||||
public float OptimalFertility { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Ширина толерантности по плодородности.</summary>
|
|
||||||
public float FertilityTolerance { get; init; } = 0.8f;
|
|
||||||
|
|
||||||
/// <summary>Базовая бодрость роста (множитель скорости в идеальных условиях).</summary>
|
|
||||||
public float Vigor { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Продолжительность жизни (игровых дней) до смерти от старости.</summary>
|
|
||||||
public float Lifespan { get; init; } = 80f;
|
|
||||||
|
|
||||||
/// <summary>Дальность расселения семян (в клетках).</summary>
|
|
||||||
public float DispersalRange { get; init; } = 2f;
|
|
||||||
|
|
||||||
/// <summary>Средний интервал между попытками дать семя (игровых дней).</summary>
|
|
||||||
public float ReproduceInterval { get; init; } = 8f;
|
|
||||||
|
|
||||||
/// <summary>Способность к самоопылению (0..1): шанс дать семя без партнёра.</summary>
|
|
||||||
public float SelfPollination { get; init; } = 0.4f;
|
|
||||||
|
|
||||||
/// <summary>Темп мутаций (0..1): вероятность сдвига аллеля у потомка.</summary>
|
|
||||||
public float MutationRate { get; init; } = 0.05f;
|
|
||||||
|
|
||||||
/// <summary>Доля рецессивного аллеля морфы в стартовой популяции (0..1).</summary>
|
|
||||||
public float VariantChance { get; init; } = 0.15f;
|
|
||||||
|
|
||||||
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
|
||||||
public float Spread { get; init; } = 0.08f;
|
|
||||||
|
|
||||||
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
|
|
||||||
public float FruitYield { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
|
|
||||||
public float FruitSeason { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
|
|
||||||
public float HarvestAmount { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
|
||||||
public float LeafHue { get; init; } = 0.33f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
|
||||||
public sealed class PlantDef : Def
|
|
||||||
{
|
|
||||||
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
|
||||||
public string Texture { get; init; } = "";
|
|
||||||
|
|
||||||
/// <summary>Базовый размер спрайта в клетках; база для стадий без своего размера.</summary>
|
|
||||||
public float SizeCells { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).</summary>
|
|
||||||
public float TrunkRadiusCells { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.</summary>
|
|
||||||
public List<PlantGrowthStage> Stages { get; init; } = [];
|
|
||||||
|
|
||||||
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
|
|
||||||
public GenomeDef Genome { get; init; } = new();
|
|
||||||
|
|
||||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
|
|
||||||
public string? HarvestProduct { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
|
||||||
public string? FruitProduct { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
|
|
||||||
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
|
||||||
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WorldPresetDef : Def
|
|
||||||
{
|
|
||||||
/// <summary>Ширина мира в клетках.</summary>
|
|
||||||
public int Width { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Высота мира в клетках.</summary>
|
|
||||||
public int Height { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Число жителей при генерации.</summary>
|
|
||||||
public int Population { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Порядок в списке UI (по возрастанию).</summary>
|
|
||||||
public int Order { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное).</summary>
|
|
||||||
public sealed class PawnDef : Def
|
|
||||||
{
|
|
||||||
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
|
||||||
public const string KindBeing = "being";
|
|
||||||
|
|
||||||
/// <summary>Вид животного.</summary>
|
|
||||||
public const string KindAnimal = "animal";
|
|
||||||
|
|
||||||
/// <summary>Ключ текстуры (путь региона в атласах).</summary>
|
|
||||||
public string Texture { get; init; } = "";
|
|
||||||
|
|
||||||
/// <summary>Размер спрайта в клетках.</summary>
|
|
||||||
public float SizeCells { get; init; } = 1f;
|
|
||||||
|
|
||||||
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
|
||||||
public string Kind { get; init; } = "";
|
|
||||||
}
|
|
||||||
@@ -48,6 +48,15 @@ public sealed class ModAtlases : IDisposable
|
|||||||
return Get(device, name).GetRegion(textureKey);
|
return Get(device, name).GetRegion(textureKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Регион по ключу, если он есть (для опциональных вариантов, напр. направленных спрайтов).</summary>
|
||||||
|
public bool TryGetRegion(GraphicsDevice device, string textureKey, out Texture2DRegion region)
|
||||||
|
{
|
||||||
|
var segments = textureKey.Split('/');
|
||||||
|
var depth = Math.Min(GroupDepth, segments.Length - 1);
|
||||||
|
var name = depth == 0 ? "Atlas" : string.Join('.', segments[..depth]);
|
||||||
|
return Get(device, name).TryGetRegion(textureKey, out region);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Готовый реестр нужд из <see cref="NeedDef"/>: присваивает каждой нужде индекс (по нему системы
|
||||||
|
/// читают значения в <c>AnimalNeeds.Values</c> без словарей), знает тип (deplete/drive), начальное
|
||||||
|
/// значение и какое действие какую нужду утоляет. Делает набор нужд расширяемым данными: добавил
|
||||||
|
/// <see cref="NeedDef"/> — особи получают новую нужду, без правки кода (если действие уже есть).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NeedSet
|
||||||
|
{
|
||||||
|
private readonly NeedDef[] _defs;
|
||||||
|
private readonly bool[] _isDrive; // предрасчёт типа нужды — горячие циклы не сравнивают строки на тик
|
||||||
|
private readonly Dictionary<string, int> _byAction = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Строит реестр из всех <see cref="NeedDef"/> мода (порядок = индексы нужд).</summary>
|
||||||
|
public NeedSet(GameContent content)
|
||||||
|
{
|
||||||
|
_defs = content.Defs.All<NeedDef>().ToArray();
|
||||||
|
_isDrive = new bool[_defs.Length];
|
||||||
|
for (var i = 0; i < _defs.Length; i++)
|
||||||
|
{
|
||||||
|
_isDrive[i] = string.Equals(_defs[i].Kind, "drive", StringComparison.Ordinal);
|
||||||
|
if (!string.IsNullOrEmpty(_defs[i].Action))
|
||||||
|
{
|
||||||
|
_byAction[_defs[i].Action] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Число нужд.</summary>
|
||||||
|
public int Count => _defs.Length;
|
||||||
|
|
||||||
|
/// <summary>Деф нужды по индексу.</summary>
|
||||||
|
public NeedDef this[int index] => _defs[index];
|
||||||
|
|
||||||
|
/// <summary>Нужда-влечение (растёт под условием), а не убывающая.</summary>
|
||||||
|
public bool IsDrive(int index) => _isDrive[index];
|
||||||
|
|
||||||
|
/// <summary>Убывающая нужда (голод/жажда/сон) — низкое значение срочно.</summary>
|
||||||
|
public bool IsDeplete(int index) => !IsDrive(index);
|
||||||
|
|
||||||
|
/// <summary>Индекс нужды, которую утоляет действие, или -1.</summary>
|
||||||
|
public int NeedForAction(string actionId) =>
|
||||||
|
_byAction.TryGetValue(actionId, out var index) ? index : -1;
|
||||||
|
|
||||||
|
/// <summary>Новый массив значений нужд особи: deplete стартуют полными (1), drive — пустыми (0).</summary>
|
||||||
|
public float[] NewValues()
|
||||||
|
{
|
||||||
|
var values = new float[_defs.Length];
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
values[i] = IsDrive(i) ? 0f : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace LittleSim.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реестр мыслей настроения (<see cref="ThoughtDef"/>) с доступом по id (фаза A8): системы-источники
|
||||||
|
/// событий (роды/спаривание/жажда) берут нужную мысль по строковому id (см. <c>AnimalThoughts</c>) и
|
||||||
|
/// вешают её на особь. Метка/сила/длительность мысли — данные; модер правит их в thoughts.json. Зеркало
|
||||||
|
/// <see cref="NeedSet"/> по духу. Удалил модер мысль из дефов — <see cref="Get"/> вернёт null (no-op).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ThoughtSet
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ThoughtDef> _byId;
|
||||||
|
|
||||||
|
/// <summary>Строит реестр из всех <see cref="ThoughtDef"/> мода.</summary>
|
||||||
|
public ThoughtSet(GameContent content) =>
|
||||||
|
_byId = content.Defs.All<ThoughtDef>().ToDictionary(t => t.DefName, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Число мыслей.</summary>
|
||||||
|
public int Count => _byId.Count;
|
||||||
|
|
||||||
|
/// <summary>Деф мысли по id или null, если такой нет (мод убрал).</summary>
|
||||||
|
public ThoughtDef? Get(string id) => _byId.GetValueOrDefault(id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
|
||||||
|
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
|
||||||
|
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
|
||||||
|
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
|
||||||
|
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
|
||||||
|
/// </summary>
|
||||||
|
public struct NetLerp : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
|
||||||
|
public Vector2 Visual;
|
||||||
|
|
||||||
|
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
|
||||||
|
public Vector2 Target;
|
||||||
|
|
||||||
|
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
|
||||||
|
public bool Initialized;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
using LittleSim.Sim;
|
using LittleSim.Sim;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
using MrGameEng.Net;
|
using MrGameEng.Net;
|
||||||
@@ -17,7 +18,36 @@ public static class NetSchema
|
|||||||
/// <summary>Порт сервера по умолчанию.</summary>
|
/// <summary>Порт сервера по умолчанию.</summary>
|
||||||
public const int DefaultPort = 9050;
|
public const int DefaultPort = 9050;
|
||||||
|
|
||||||
|
// Задокументированный wire-размер реплицируемых структур (см. NetContract.cs веб-зеркала). Репликация
|
||||||
|
// блитит сырые байты структуры, поэтому добавленное/переставленное поле молча испортит десериализацию —
|
||||||
|
// проверяем размеры при построении схемы, чтобы дрейф упал на старте, а не выдал битый мир.
|
||||||
|
private const int TransformWireSize = 20; // Vector2(8) + float(4) + Vector2(8)
|
||||||
|
private const int PawnNeedsWireSize = 4; // float(4)
|
||||||
|
|
||||||
/// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary>
|
/// <summary>Схема репликации: позиция/масштаб жителя и его потребности.</summary>
|
||||||
public static ReplicationSchema Create() =>
|
public static ReplicationSchema Create()
|
||||||
new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
|
{
|
||||||
|
VerifyLayout();
|
||||||
|
return new ReplicationSchema().Register<Transform2D>().Register<PawnNeeds>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сверяет фактическую байт-раскладку реплицируемых структур с контрактом (см. NetContract.cs).</summary>
|
||||||
|
private static void VerifyLayout()
|
||||||
|
{
|
||||||
|
Require<Transform2D>(TransformWireSize);
|
||||||
|
Require<PawnNeeds>(PawnNeedsWireSize);
|
||||||
|
|
||||||
|
static void Require<T>(int expected)
|
||||||
|
where T : struct
|
||||||
|
{
|
||||||
|
var actual = Unsafe.SizeOf<T>();
|
||||||
|
if (actual != expected)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Размер реплицируемого {typeof(T).Name} = {actual} Б, ожидался {expected} Б: "
|
||||||
|
+ "wire-контракт изменился — синхронизируй NetSchema и LittleSim.Web/NetContract.cs."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,25 +5,6 @@ using MrGameEng.Graphics;
|
|||||||
|
|
||||||
namespace LittleSim.Net;
|
namespace LittleSim.Net;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
|
|
||||||
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
|
|
||||||
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
|
|
||||||
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
|
|
||||||
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
|
|
||||||
/// </summary>
|
|
||||||
public struct NetLerp : IComponent
|
|
||||||
{
|
|
||||||
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
|
|
||||||
public Vector2 Visual;
|
|
||||||
|
|
||||||
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
|
|
||||||
public Vector2 Target;
|
|
||||||
|
|
||||||
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
|
|
||||||
public bool Initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
||||||
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
using LittleSim.Net;
|
using LittleSim.Net;
|
||||||
using LittleSim.Scenes;
|
using LittleSim.Scenes;
|
||||||
|
using LittleSim.Sim;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.Genetics;
|
using MrGameEng.Genetics;
|
||||||
using MrGameEng.Host;
|
using MrGameEng.Host;
|
||||||
@@ -20,6 +21,36 @@ if (args.Contains("--check-content"))
|
|||||||
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
|
+ $"{content.Defs.NamesOf("Plant").Count} plants, {traits.Count} traits "
|
||||||
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
|
+ $"(hardiness={traits.GetValueOrDefault("hardiness"):0.##})"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Диета животных из генов (предатор-кластер): для каждого вида строим центральный геном из его
|
||||||
|
// баз, считаем признаки формулами и выводим рацион (травоядное/хищник/всеядное) — проверка без
|
||||||
|
// GPU, что хищники заданы и роль выводится из генов herbivory/carnivory/omnivory.
|
||||||
|
foreach (var def in content.Defs.All<AnimalDef>())
|
||||||
|
{
|
||||||
|
var entries = new List<GenomeTemplate.Entry>();
|
||||||
|
foreach (var (geneId, baseValue) in def.Genome)
|
||||||
|
{
|
||||||
|
if (registry.TryGetValue(geneId, out var gene))
|
||||||
|
{
|
||||||
|
var spread = gene.Kind == GeneKind.Discrete ? 0f : gene.Spread;
|
||||||
|
entries.Add(new GenomeTemplate.Entry(gene, baseValue, spread));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var genome = new GenomeTemplate(entries).Generate(new Random(7));
|
||||||
|
var t = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, registry));
|
||||||
|
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(t, def);
|
||||||
|
var role =
|
||||||
|
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
|
||||||
|
: eatsPlants ? "herbivore"
|
||||||
|
: "none";
|
||||||
|
var hunt = eatsMeat && def.AttackDamage > 0f ? $", attack {def.AttackDamage:0}/d" : "";
|
||||||
|
Console.WriteLine(
|
||||||
|
$" {def.DefName}: {role} (herb {t.Herbivory:0.##}/carn {t.Carnivory:0.##}/omni {t.Omnivory:0.##}),"
|
||||||
|
+ $" spawn {def.SpawnPer1000Cells:0.##}/1000{hunt}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch (Exception error)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public sealed class BootScene : Scene
|
|||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
};
|
};
|
||||||
desktop.Root = Ui.Screen(_label);
|
desktop.Root = Ui.MenuScreen(_label);
|
||||||
|
|
||||||
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
|
// Сборка атласов и дефов — без GPU, поэтому безопасно вне главного потока.
|
||||||
_load = Task.Run(() => GameContent.Load());
|
_load = Task.Run(() => GameContent.Load());
|
||||||
|
|||||||
@@ -2,58 +2,14 @@ using Friflo.Engine.ECS;
|
|||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
using LittleSim.Sim;
|
using LittleSim.Sim;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
using MrGameEng.DevConsole;
|
using MrGameEng.DevConsole;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
|
||||||
namespace LittleSim.Scenes;
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
|
||||||
internal static class ScatterSpawner
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
|
||||||
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
|
||||||
/// разновозрастный. Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением
|
|
||||||
/// управляет <see cref="PlantGrowthSystem"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static void Spawn(
|
|
||||||
Scene scene,
|
|
||||||
GameContent content,
|
|
||||||
PlantSet plants,
|
|
||||||
TerrainDef terrain,
|
|
||||||
Point cell,
|
|
||||||
int cellSize,
|
|
||||||
Random random
|
|
||||||
)
|
|
||||||
{
|
|
||||||
foreach (var entry in terrain.Scatter)
|
|
||||||
{
|
|
||||||
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
|
||||||
var index = plants.IndexOf(def);
|
|
||||||
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
|
||||||
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
|
||||||
var age = random.NextSingle() * plants[index].MaturityDays;
|
|
||||||
|
|
||||||
PlantFactory.Create(
|
|
||||||
scene.Store,
|
|
||||||
plants,
|
|
||||||
index,
|
|
||||||
new Vector2(px, py),
|
|
||||||
age,
|
|
||||||
plants[index].Template.Generate(random),
|
|
||||||
terrain.Fertility,
|
|
||||||
cellSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary>
|
/// <summary>Консольные команды контента: моды, язык, дефы, атласы (общие для сцен).</summary>
|
||||||
internal static class ContentCommands
|
internal static class ContentCommands
|
||||||
{
|
{
|
||||||
@@ -17,15 +17,15 @@ public sealed class CreditsScene : Scene
|
|||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var column = Ui.Column(12);
|
var column = Ui.Card(10);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
||||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
column.Widgets.Add(Ui.AccentRule());
|
||||||
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
|
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.author")));
|
||||||
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
|
column.Widgets.Add(Ui.Subtitle(lang.Get("credits.role")));
|
||||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 12 });
|
||||||
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
|
column.Widgets.Add(Ui.Button(lang.Get("credits.back"), Back));
|
||||||
|
|
||||||
desktop.Root = Ui.Screen(column);
|
desktop.Root = Ui.MenuScreen(column);
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new CallbackSystem(() =>
|
new CallbackSystem(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public static class GameLayers
|
|||||||
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
|
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
|
||||||
public static LayerId Selection { get; private set; }
|
public static LayerId Selection { get; private set; }
|
||||||
|
|
||||||
/// <summary>Регистрирует слои при первом вызове.</summary>
|
/// <summary>Регистрирует слои при первом вызове (порядок = порядок отрисовки).</summary>
|
||||||
public static void EnsureRegistered(Renderer2D renderer)
|
public static void EnsureRegistered(Renderer2D renderer)
|
||||||
{
|
{
|
||||||
if (_registered)
|
if (_registered)
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ public sealed class LoadGameScene : Scene
|
|||||||
private void Rebuild()
|
private void Rebuild()
|
||||||
{
|
{
|
||||||
var lang = _content.Languages;
|
var lang = _content.Languages;
|
||||||
var column = Ui.Column(8);
|
var column = Ui.Card(8);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("load.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("load.title")));
|
||||||
column.Widgets.Add(new Label { Height = 8 });
|
column.Widgets.Add(Ui.AccentRule());
|
||||||
|
|
||||||
var saves = _store.List();
|
var saves = _store.List();
|
||||||
if (saves.Count == 0)
|
if (saves.Count == 0)
|
||||||
@@ -73,7 +73,7 @@ public sealed class LoadGameScene : Scene
|
|||||||
if (!Context.Scenes.IsTransitioning)
|
if (!Context.Scenes.IsTransitioning)
|
||||||
{
|
{
|
||||||
Context.Scenes.Switch(
|
Context.Scenes.Switch(
|
||||||
new WorldScene(save.ToConfig(), save),
|
new WorldLoadingScene(save.ToConfig(), save),
|
||||||
Transitions.Fade(0.6f)
|
Transitions.Fade(0.6f)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ public sealed class LoadGameScene : Scene
|
|||||||
column.Widgets.Add(new Label { Height = 8 });
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
|
column.Widgets.Add(Ui.Button(lang.Get("load.back"), Back));
|
||||||
|
|
||||||
_desktop.Root = Ui.Screen(column);
|
_desktop.Root = Ui.MenuScreen(column);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Back()
|
private void Back()
|
||||||
|
|||||||
@@ -19,19 +19,19 @@ public sealed class MainMenuScene : Scene
|
|||||||
var lang = content.Languages;
|
var lang = content.Languages;
|
||||||
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var column = Ui.Column(12);
|
var card = Ui.Card(8);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
card.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||||
column.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
card.Widgets.Add(Ui.Subtitle(lang.Get("menu.subtitle")));
|
||||||
column.Widgets.Add(new Myra.Graphics2D.UI.Label { Height = 16 });
|
card.Widgets.Add(Ui.AccentRule());
|
||||||
column.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
card.Widgets.Add(Ui.Button(lang.Get("menu.newworld"), () => Go(new NewWorldScene())));
|
||||||
column.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
card.Widgets.Add(Ui.Button(lang.Get("menu.load"), () => Go(new LoadGameScene())));
|
||||||
column.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
card.Widgets.Add(Ui.Button(lang.Get("menu.settings"), () => Go(new SettingsScene())));
|
||||||
column.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
card.Widgets.Add(Ui.Button(lang.Get("menu.credits"), () => Go(new CreditsScene())));
|
||||||
column.Widgets.Add(
|
card.Widgets.Add(
|
||||||
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
|
Ui.Button(lang.Get("menu.quit"), () => Context.Services.Get<Game>().Exit())
|
||||||
);
|
);
|
||||||
|
|
||||||
desktop.Root = Ui.Screen(column);
|
desktop.Root = Ui.MenuScreen(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Go(Scene scene)
|
private void Go(Scene scene)
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ public sealed class NewWorldScene : Scene
|
|||||||
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
||||||
_preset = presets[0];
|
_preset = presets[0];
|
||||||
|
|
||||||
var column = Ui.Column(10);
|
var column = Ui.Card(10);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("newworld.title")));
|
||||||
column.Widgets.Add(new Label { Height = 8 });
|
column.Widgets.Add(Ui.AccentRule());
|
||||||
|
|
||||||
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
|
_name = new TextBox { Text = lang.Get("newworld.defaultname"), Width = 280 };
|
||||||
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
|
column.Widgets.Add(LabeledRow(lang.Get("newworld.name"), _name));
|
||||||
@@ -106,7 +106,7 @@ public sealed class NewWorldScene : Scene
|
|||||||
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
|
buttons.Widgets.Add(Ui.Button(lang.Get("newworld.back"), Back, width: 150));
|
||||||
column.Widgets.Add(buttons);
|
column.Widgets.Add(buttons);
|
||||||
|
|
||||||
desktop.Root = Ui.Screen(column);
|
desktop.Root = Ui.MenuScreen(column);
|
||||||
Refresh();
|
Refresh();
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
@@ -154,7 +154,7 @@ public sealed class NewWorldScene : Scene
|
|||||||
Seed = seed,
|
Seed = seed,
|
||||||
SmoothPasses = _smoothing,
|
SmoothPasses = _smoothing,
|
||||||
};
|
};
|
||||||
Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f));
|
Context.Scenes.Switch(new WorldLoadingScene(config), Transitions.Fade(0.6f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Back()
|
private void Back()
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
||||||
|
internal static class ScatterSpawner
|
||||||
|
{
|
||||||
|
// Ниже этой световой пригодности (при полном дневном свете клетки) растение не сеем: теневому виду
|
||||||
|
// (грибы, optimalLight≈0.15) в открытой клетке не место — без полога крон ему тут не выжить, поэтому
|
||||||
|
// он и не должен появляться, пока такой ниши нет. Солнечные виды (≥0.7 пригодности) проходят свободно.
|
||||||
|
private const float ShadeSpawnThreshold = 0.15f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
||||||
|
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
||||||
|
/// разновозрастный. Теневые виды не сеются в ярко освещённые клетки (см. <see cref="ShadeSpawnThreshold"/>).
|
||||||
|
/// Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением управляет <see cref="PlantGrowthSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static void Spawn(
|
||||||
|
Scene scene,
|
||||||
|
GameContent content,
|
||||||
|
PlantSet plants,
|
||||||
|
TerrainDef terrain,
|
||||||
|
Point cell,
|
||||||
|
int cellSize,
|
||||||
|
Random random
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Статичная освещённость клетки в полдень: затенена только светонепроницаемым террейном (полог
|
||||||
|
// крон сейчас не учитывается). По ней решаем, годится ли клетка теневому виду.
|
||||||
|
var exposure = terrain.BlocksLight ? LightmapBuilder.OccluderShade : 1f;
|
||||||
|
|
||||||
|
foreach (var entry in terrain.Scatter)
|
||||||
|
{
|
||||||
|
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
||||||
|
if (
|
||||||
|
Suitability.Gaussian(exposure, def.Genome.OptimalLight, def.Genome.LightTolerance)
|
||||||
|
< ShadeSpawnThreshold
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue; // теневому виду в ярко освещённой клетке не место
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = plants.IndexOf(def);
|
||||||
|
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||||
|
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||||
|
var age = random.NextSingle() * plants[index].MaturityDays;
|
||||||
|
|
||||||
|
PlantFactory.Create(
|
||||||
|
scene.Store,
|
||||||
|
plants,
|
||||||
|
index,
|
||||||
|
new Vector2(px, py),
|
||||||
|
age,
|
||||||
|
plants[index].Template.Generate(random),
|
||||||
|
terrain.Fertility,
|
||||||
|
cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,9 @@ public sealed class SettingsScene : Scene
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void Rebuild() =>
|
private void Rebuild() =>
|
||||||
_desktop.Root = Ui.Screen(SettingsPanel.Build(_settings, _content.Languages, Apply, Back));
|
_desktop.Root = Ui.MenuScreen(
|
||||||
|
SettingsPanel.Build(_settings, _content.Languages, Apply, Back)
|
||||||
|
);
|
||||||
|
|
||||||
private void Apply()
|
private void Apply()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Экран загрузки между окном создания/выбора мира и игровой сценой. Генерация мира
|
||||||
|
/// (<see cref="WorldScene.OnLoad"/>: рельеф, атласы, спавн) идёт синхронно и блокирует кадр; раньше под
|
||||||
|
/// ней был лишь сплошной фейд без индикатора. Эта сцена рисует надпись «Создание мира…», даёт ей
|
||||||
|
/// прорисоваться, затем переключается на <see cref="WorldScene"/> БЕЗ перехода — её последний кадр
|
||||||
|
/// (надпись) и остаётся на экране, пока идёт генерация, давая игроку понятную обратную связь.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldLoadingScene : Scene
|
||||||
|
{
|
||||||
|
private readonly WorldConfig _config;
|
||||||
|
private readonly WorldSave? _save;
|
||||||
|
private GameSettings _settings = new();
|
||||||
|
private Label _label = null!;
|
||||||
|
private int _frames;
|
||||||
|
|
||||||
|
/// <summary>Новый мир (<paramref name="save"/> = null) или загрузка сохранения.</summary>
|
||||||
|
public WorldLoadingScene(WorldConfig config, WorldSave? save = null)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_save = save;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
_settings = GameSettingsStore.Load();
|
||||||
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
_label = new Label
|
||||||
|
{
|
||||||
|
TextColor = Ui.Accent,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
desktop.Root = Ui.MenuScreen(_label);
|
||||||
|
UpdateSystems.Add(new CallbackSystem(Tick));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
var content = Context.Services.Get<GameContent>();
|
||||||
|
var word = content.Languages.Get("loading.world");
|
||||||
|
var dots = new string('.', (int)(Context.Clock.UnscaledTotalTime * 2) % 4);
|
||||||
|
_label.Text = word + dots;
|
||||||
|
|
||||||
|
// Дождаться, пока надпись точно прорисована (переход въезда завершён + пара кадров), затем
|
||||||
|
// мгновенно переключиться: блокирующий WorldScene.OnLoad оставит на экране кадр с надписью.
|
||||||
|
_frames++;
|
||||||
|
if (Context.Scenes.IsTransitioning || _frames < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Context.Scenes.Switch(new WorldScene(_config, _save));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,669 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Assets;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
using MrGameEng.Formulas;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Inspector;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
using MrGameEng.Tilemaps;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
// Часть WorldScene: регистрация и обработчики команд дев-консоли.
|
||||||
|
public sealed partial class WorldScene
|
||||||
|
{
|
||||||
|
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
|
||||||
|
{
|
||||||
|
ContentCommands.Register(console, content, atlases);
|
||||||
|
console.Register(
|
||||||
|
"regen",
|
||||||
|
"regen [seed] — regenerate the world with a new seed",
|
||||||
|
(c, args) =>
|
||||||
|
{
|
||||||
|
if (Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||||
|
c.WriteLine($"regenerating world, seed {seed}");
|
||||||
|
Context.Scenes.Switch(
|
||||||
|
new WorldScene(_config with { Seed = seed }),
|
||||||
|
Transitions.Fade(0.6f)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"formula",
|
||||||
|
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
|
||||||
|
(c, args) =>
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
c.WriteLine(
|
||||||
|
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expression = string.Join(' ', args);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
|
||||||
|
}
|
||||||
|
catch (FormulaException e)
|
||||||
|
{
|
||||||
|
c.WriteLine($"error: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"gene",
|
||||||
|
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
|
||||||
|
(c, args) => RunGeneDemo(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"plant",
|
||||||
|
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
||||||
|
(c, args) => RunPlantDemo(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"animal",
|
||||||
|
"animal <species> [seed] — sample an animal genome and show its gene-driven traits",
|
||||||
|
(c, args) => RunAnimalDemo(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"diet",
|
||||||
|
"diet [species] — gene-derived diet (herbivory/carnivory/omnivory), role and hunting stats",
|
||||||
|
(c, args) => RunDiet(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"popstats",
|
||||||
|
"popstats [species] — live population trait means and generation span (selection drift)",
|
||||||
|
(c, args) => RunPopStats(c, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"body",
|
||||||
|
"body <species> — dump a species body parts and a healthy individual's capacities",
|
||||||
|
(c, args) => RunBodyDemo(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"infect",
|
||||||
|
"infect [hediff] — give every animal a disease (default Fever) to test the health system",
|
||||||
|
(c, args) => RunInfect(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"intel",
|
||||||
|
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
|
||||||
|
(c, args) => RunIntel(c, content, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"mood",
|
||||||
|
"mood [species] — live population mood (avg/min/max) and content/stressed counts",
|
||||||
|
(c, args) => RunMood(c, args)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"skills",
|
||||||
|
"skills — mean skill levels across living animals (grow from practice, decay unused)",
|
||||||
|
(c, _) => RunSkills(c)
|
||||||
|
);
|
||||||
|
console.Register(
|
||||||
|
"menu",
|
||||||
|
"menu — return to the main menu",
|
||||||
|
(_, _) => Switch(new MainMenuScene())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
|
||||||
|
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
|
||||||
|
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var genes = content.Defs.All<GeneDef>();
|
||||||
|
if (genes.Count == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("no Gene defs loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||||||
|
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||||||
|
var random = new Random(seed);
|
||||||
|
|
||||||
|
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
|
||||||
|
var parentA = Genome.Generate(genes, random);
|
||||||
|
foreach (var gene in genes)
|
||||||
|
{
|
||||||
|
var allele = parentA[gene.DefName];
|
||||||
|
console.WriteLine(
|
||||||
|
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.WriteLine("traits:");
|
||||||
|
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
|
||||||
|
{
|
||||||
|
console.WriteLine($" {trait} = {value:0.###}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentB = Genome.Generate(genes, random);
|
||||||
|
var child = Genome.Breed(parentA, parentB, registry, random);
|
||||||
|
var childTraits = Phenotype.Compute(child, registry);
|
||||||
|
console.WriteLine(
|
||||||
|
$"bred child: {child.Alleles.Count} genes, "
|
||||||
|
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
|
||||||
|
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
|
||||||
|
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
|
||||||
|
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
|
||||||
|
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var species = content.Defs.NamesOf("Plant");
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
|
||||||
|
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
|
||||||
|
{
|
||||||
|
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _plants.IndexOf(def);
|
||||||
|
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||||
|
var genome = _plants[index].Template.Generate(new Random(seed));
|
||||||
|
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
|
||||||
|
|
||||||
|
console.WriteLine(
|
||||||
|
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
||||||
|
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
||||||
|
);
|
||||||
|
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||||
|
console.WriteLine(
|
||||||
|
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
|
||||||
|
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" defense: toxicity {traits.Toxicity:0.##}, thorns {traits.Thorns:0.##}, "
|
||||||
|
+ $"palatability {traits.Palatability:0.##} (growth ×{traits.DefenseGrowthFactor():0.##})"
|
||||||
|
);
|
||||||
|
console.WriteLine($" mass (mature): {def.BaseMassKg:0.##} kg");
|
||||||
|
|
||||||
|
if (def.HarvestProduct is { } harvest)
|
||||||
|
{
|
||||||
|
console.WriteLine(
|
||||||
|
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||||||
|
{
|
||||||
|
var season = content.Languages.Get(
|
||||||
|
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.WriteLine(" barren (no fruit)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Демонстрация животного (фаза A1): по виду берём его набор генов, генерируем особь и печатаем
|
||||||
|
// признаки, вычисленные формулами генов (тело/мозг/метаболизм/чувства/мех).
|
||||||
|
private void RunAnimalDemo(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var species = content.Defs.NamesOf("Animal");
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("usage: animal <species> [seed] e.g. animal Deer");
|
||||||
|
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||||
|
{
|
||||||
|
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _animals.IndexOf(def);
|
||||||
|
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||||
|
var genome = _animals.GenerateGenome(index, new Random(seed));
|
||||||
|
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
|
||||||
|
var sex = AnimalFactory.IsMaleGenome(genome) ? "male" : "female";
|
||||||
|
|
||||||
|
console.WriteLine($"{def.DefName} ({sex}, seed {seed}):");
|
||||||
|
console.WriteLine(
|
||||||
|
$" body {traits.BodySize:0.##} cells, maturity {traits.MaturityAge:0} d, "
|
||||||
|
+ $"lifespan {traits.Lifespan:0} d, brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
|
||||||
|
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" diet: herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / omni {traits.Omnivory:0.##}, "
|
||||||
|
+ $"toxinTolerance {traits.ToxinTolerance:0.##}"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" reproduction: {(traits.Oviparous ? "oviparous (lays eggs)" : "viviparous (live birth)")}, "
|
||||||
|
+ $"gestation {traits.GestationDays:0} d, litter {traits.LitterSize:0.#}"
|
||||||
|
);
|
||||||
|
console.WriteLine(
|
||||||
|
$" behaviour: insulation {traits.Insulation:0.##}, sociability {traits.Sociability:0.##}"
|
||||||
|
);
|
||||||
|
var adultMass = Mass.OfAnimal(def, traits, 1f);
|
||||||
|
console.WriteLine(
|
||||||
|
$" mass: {adultMass:0.#} kg (adult), carries ~{Mass.CarryCapacity(adultMass, 1f):0.#} kg"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Диета из генов (предатор-кластер): по каждому виду печатает herbivory/carnivory/omnivory,
|
||||||
|
// выведенный рацион (ест растения/мясо), роль и боевые параметры — проверка, что хищники заданы.
|
||||||
|
private void RunDiet(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var species = content.Defs.NamesOf("Animal");
|
||||||
|
for (var s = 0; s < _animals.Count; s++)
|
||||||
|
{
|
||||||
|
var sp = _animals[s];
|
||||||
|
if (
|
||||||
|
args.Length > 0
|
||||||
|
&& !string.Equals(sp.Def.DefName, args[0], StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var genome = _animals.GenerateGenome(s, new Random(0x01E7 + s));
|
||||||
|
var traits = AnimalPhenotype.FromTraits(
|
||||||
|
Phenotype.Compute(genome, _animals.GeneRegistry)
|
||||||
|
);
|
||||||
|
var (eatsPlants, eatsMeat) = AnimalFactory.Diet(traits, sp.Def);
|
||||||
|
var role =
|
||||||
|
eatsMeat ? (eatsPlants ? "omnivore" : "carnivore")
|
||||||
|
: eatsPlants ? "herbivore"
|
||||||
|
: "none";
|
||||||
|
console.WriteLine(
|
||||||
|
$"{sp.Def.DefName}: {role} — herb {traits.Herbivory:0.##} / carn {traits.Carnivory:0.##} / "
|
||||||
|
+ $"omni {traits.Omnivory:0.##} -> plants={eatsPlants} meat={eatsMeat}, spawn {sp.Def.SpawnPer1000Cells:0.##}/1000"
|
||||||
|
);
|
||||||
|
if (eatsMeat && sp.Def.AttackDamage > 0f)
|
||||||
|
{
|
||||||
|
console.WriteLine(
|
||||||
|
$" hunt: dmg {sp.Def.AttackDamage:0}/d, bleed {sp.Def.AttackBleed:0.#}/d, "
|
||||||
|
+ $"bloodloss {sp.Def.AttackBloodLoss:0.#}/d, range {sp.Def.AttackRangeCells:0.#} cells"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наблюдаемость отбора (фаза A2): средние ключевых признаков живой популяции и размах поколений —
|
||||||
|
// видно дрейф генов под отбором. Без аргумента — все виды; с аргументом — один вид.
|
||||||
|
private void RunPopStats(DevConsole console, string[] args)
|
||||||
|
{
|
||||||
|
if (_animals.Count == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("no animal species");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = new int[_animals.Count];
|
||||||
|
var body = new double[_animals.Count];
|
||||||
|
var brain = new double[_animals.Count];
|
||||||
|
var life = new double[_animals.Count];
|
||||||
|
var move = new double[_animals.Count];
|
||||||
|
var tox = new double[_animals.Count];
|
||||||
|
var maxGen = new int[_animals.Count];
|
||||||
|
|
||||||
|
Store
|
||||||
|
.Query<AnimalOrganism>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref AnimalOrganism o, Entity _) =>
|
||||||
|
{
|
||||||
|
var s = o.Species;
|
||||||
|
count[s]++;
|
||||||
|
body[s] += o.Traits.BodySize;
|
||||||
|
brain[s] += o.Traits.BrainSize;
|
||||||
|
life[s] += o.Traits.Lifespan;
|
||||||
|
move[s] += o.Traits.MoveSpeed;
|
||||||
|
tox[s] += o.Traits.ToxinTolerance;
|
||||||
|
if (o.Generation > maxGen[s])
|
||||||
|
{
|
||||||
|
maxGen[s] = o.Generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var filter = args.Length > 0 ? args[0] : null;
|
||||||
|
var any = false;
|
||||||
|
for (var s = 0; s < _animals.Count; s++)
|
||||||
|
{
|
||||||
|
var name = _animals[s].Def.DefName;
|
||||||
|
if (
|
||||||
|
count[s] == 0
|
||||||
|
|| (
|
||||||
|
filter is not null
|
||||||
|
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
any = true;
|
||||||
|
var c = count[s];
|
||||||
|
console.WriteLine(
|
||||||
|
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, "
|
||||||
|
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}, "
|
||||||
|
+ $"toxinTol {tox[s] / c:0.##}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any)
|
||||||
|
{
|
||||||
|
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Другая сторона гонки вооружений: средняя защита живых растений (коэволюция C2). Без фильтра.
|
||||||
|
if (filter is null)
|
||||||
|
{
|
||||||
|
var pn = 0;
|
||||||
|
double pTox = 0,
|
||||||
|
pThorn = 0,
|
||||||
|
pPalat = 0;
|
||||||
|
Store
|
||||||
|
.Query<PlantOrganism>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref PlantOrganism o, Entity _) =>
|
||||||
|
{
|
||||||
|
pn++;
|
||||||
|
pTox += o.Traits.Toxicity;
|
||||||
|
pThorn += o.Traits.Thorns;
|
||||||
|
pPalat += o.Traits.Palatability;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (pn > 0)
|
||||||
|
{
|
||||||
|
console.WriteLine(
|
||||||
|
$"plants: n={pn}, toxicity {pTox / pn:0.###}, thorns {pThorn / pn:0.###}, "
|
||||||
|
+ $"palatability {pPalat / pn:0.###}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
|
||||||
|
private void RunBodyDemo(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var species = content.Defs.NamesOf("Animal");
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("usage: body <species> e.g. body Deer");
|
||||||
|
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||||
|
{
|
||||||
|
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(def.Body) || !content.Defs.TryGet<BodyDef>(def.Body, out var body))
|
||||||
|
{
|
||||||
|
console.WriteLine($"{def.DefName}: no body");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.WriteLine($"{def.DefName} body '{body.DefName}': {body.Parts.Length} parts");
|
||||||
|
foreach (var part in body.Parts)
|
||||||
|
{
|
||||||
|
var caps =
|
||||||
|
part.Capacities.Count > 0
|
||||||
|
? " ["
|
||||||
|
+ string.Join(
|
||||||
|
", ",
|
||||||
|
part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")
|
||||||
|
)
|
||||||
|
+ "]"
|
||||||
|
: "";
|
||||||
|
console.WriteLine(
|
||||||
|
$" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = HealthState.Create(body, 1f);
|
||||||
|
console.WriteLine(
|
||||||
|
"capacities (healthy): "
|
||||||
|
+ string.Join(
|
||||||
|
", ",
|
||||||
|
state.Capacities.OrderBy(k => k.Key).Select(kv => $"{kv.Key} {kv.Value:0.##}")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тест системы здоровья (фаза A6): навешивает болезнь всем живым животным.
|
||||||
|
private void RunInfect(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var name = args.Length > 0 ? args[0] : "Fever";
|
||||||
|
if (!content.Defs.TryGet<HediffDef>(name, out var def))
|
||||||
|
{
|
||||||
|
console.WriteLine($"no hediff '{name}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
Store
|
||||||
|
.Query<Health>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref Health h, Entity _) =>
|
||||||
|
{
|
||||||
|
h.State?.Add(def);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.WriteLine($"infected {count} animals with {def.DefName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наблюдаемость интеллекта (фаза A7): мозг особи (ген) × сознание = эффективный интеллект, который
|
||||||
|
// гейтит набор нужд/действий. Показывает активные/отключённые нужды при заданном уровне сознания
|
||||||
|
// (по умолчанию 1 — здоров; меньше — имитация повреждения мозга/болезни/боли).
|
||||||
|
private void RunIntel(DevConsole console, GameContent content, string[] args)
|
||||||
|
{
|
||||||
|
var species = content.Defs.NamesOf("Animal");
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine(
|
||||||
|
"usage: intel <species> [consciousness 0..1] [seed] e.g. intel Deer 0.6"
|
||||||
|
);
|
||||||
|
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||||
|
{
|
||||||
|
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var consciousness =
|
||||||
|
args.Length > 1
|
||||||
|
? Math.Clamp(
|
||||||
|
float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
0f,
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
: 1f;
|
||||||
|
var seed = args.Length > 2 ? int.Parse(args[2]) : Random.Shared.Next();
|
||||||
|
var index = _animals.IndexOf(def);
|
||||||
|
var genome = _animals.GenerateGenome(index, new Random(seed));
|
||||||
|
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
|
||||||
|
var intelligence = traits.BrainSize * consciousness;
|
||||||
|
|
||||||
|
console.WriteLine(
|
||||||
|
$"{def.DefName} (seed {seed}): brain {traits.BrainSize:0.##} x consciousness {consciousness:0.##} "
|
||||||
|
+ $"= intelligence {intelligence:0.###}"
|
||||||
|
);
|
||||||
|
var active = new List<string>();
|
||||||
|
var gated = new List<string>();
|
||||||
|
for (var k = 0; k < _needs.Count; k++)
|
||||||
|
{
|
||||||
|
var need = _needs[k];
|
||||||
|
(need.MinBrain > intelligence ? gated : active).Add(
|
||||||
|
$"{need.DefName}({need.MinBrain:0.##})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.WriteLine($" active: {(active.Count > 0 ? string.Join(", ", active) : "—")}");
|
||||||
|
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "—")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наблюдаемость настроения (фаза A8): среднее/мин/макс настроения живой популяции по виду и счётчики
|
||||||
|
// довольных (>0.7) / угнетённых (<0.35). Считает только особей с активным настроением (тир 4+).
|
||||||
|
private void RunMood(DevConsole console, string[] args)
|
||||||
|
{
|
||||||
|
if (_animals.Count == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("no animal species");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = new int[_animals.Count];
|
||||||
|
var sum = new double[_animals.Count];
|
||||||
|
var min = new float[_animals.Count];
|
||||||
|
var max = new float[_animals.Count];
|
||||||
|
var content_ = new int[_animals.Count];
|
||||||
|
var stressed = new int[_animals.Count];
|
||||||
|
Array.Fill(min, 1f);
|
||||||
|
|
||||||
|
Store
|
||||||
|
.Query<AnimalOrganism, Mood>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref AnimalOrganism o, ref Mood m, Entity _) =>
|
||||||
|
{
|
||||||
|
if (!m.Active)
|
||||||
|
{
|
||||||
|
return; // тир ниже зверя — настроения нет
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = o.Species;
|
||||||
|
count[s]++;
|
||||||
|
sum[s] += m.Value;
|
||||||
|
min[s] = MathF.Min(min[s], m.Value);
|
||||||
|
max[s] = MathF.Max(max[s], m.Value);
|
||||||
|
if (m.Value > 0.7f)
|
||||||
|
{
|
||||||
|
content_[s]++;
|
||||||
|
}
|
||||||
|
else if (m.Value < 0.35f)
|
||||||
|
{
|
||||||
|
stressed[s]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var filter = args.Length > 0 ? args[0] : null;
|
||||||
|
var any = false;
|
||||||
|
for (var s = 0; s < _animals.Count; s++)
|
||||||
|
{
|
||||||
|
var name = _animals[s].Def.DefName;
|
||||||
|
if (
|
||||||
|
count[s] == 0
|
||||||
|
|| (
|
||||||
|
filter is not null
|
||||||
|
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
any = true;
|
||||||
|
console.WriteLine(
|
||||||
|
$"{name}: n={count[s]}, mood {sum[s] / count[s]:0.##} (min {min[s]:0.##}, max {max[s]:0.##}), "
|
||||||
|
+ $"content {content_[s]}, stressed {stressed[s]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any)
|
||||||
|
{
|
||||||
|
console.WriteLine(filter is null ? "no animals with mood" : $"no '{filter}' with mood");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наблюдаемость навыков: средний уровень каждого навыка по живым животным (растут от практики).
|
||||||
|
private void RunSkills(DevConsole console)
|
||||||
|
{
|
||||||
|
if (_skills.Count == 0)
|
||||||
|
{
|
||||||
|
console.WriteLine("no skill defs loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = new double[_skills.Count];
|
||||||
|
var n = 0;
|
||||||
|
Store
|
||||||
|
.Query<Skills>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref Skills s, Entity _) =>
|
||||||
|
{
|
||||||
|
if (s.Levels is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
n++;
|
||||||
|
for (var i = 0; i < _skills.Count && i < s.Levels.Length; i++)
|
||||||
|
{
|
||||||
|
sum[i] += s.Levels[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.WriteLine($"skills over {n} animals (mean level):");
|
||||||
|
for (var i = 0; i < _skills.Count; i++)
|
||||||
|
{
|
||||||
|
console.WriteLine(
|
||||||
|
$" {_skills[i].DefName}: {(n > 0 ? sum[i] / n : 0):0.###} "
|
||||||
|
+ $"(learn {_skills[i].LearnRate:0.##}/d, decay {_skills[i].DecayPerDay:0.###}/d)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProductLabel(GameContent content, string productDefName) =>
|
||||||
|
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||||
|
? content.Languages.Get(product.Label)
|
||||||
|
: productDefName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Assets;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
using MrGameEng.Formulas;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Inspector;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
using MrGameEng.Tilemaps;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
// Часть WorldScene: сохранение/загрузка мира и стартовый спавн животных.
|
||||||
|
public sealed partial class WorldScene
|
||||||
|
{
|
||||||
|
private string SaveWorld()
|
||||||
|
{
|
||||||
|
var save = new WorldSave
|
||||||
|
{
|
||||||
|
Name = _config.Name,
|
||||||
|
Width = _config.Width,
|
||||||
|
Height = _config.Height,
|
||||||
|
Seed = _config.Seed,
|
||||||
|
SmoothPasses = _config.SmoothPasses,
|
||||||
|
SavedUtc = DateTime.UtcNow,
|
||||||
|
ElapsedSeconds = Context.Clock.TotalTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
|
||||||
|
Store
|
||||||
|
.Query<PlantGrowth, PlantOrganism, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(
|
||||||
|
ref PlantGrowth grow,
|
||||||
|
ref PlantOrganism org,
|
||||||
|
ref Transform2D transform,
|
||||||
|
Entity _
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
save.Plants.Add(
|
||||||
|
new PlantSave
|
||||||
|
{
|
||||||
|
Species = _plants[grow.Species].Def.DefName,
|
||||||
|
X = transform.Position.X,
|
||||||
|
Y = transform.Position.Y,
|
||||||
|
AgeDays = grow.AgeDays,
|
||||||
|
Stage = grow.Stage,
|
||||||
|
CellFertility = grow.CellFertility,
|
||||||
|
Genome = org.Genome.ToDictionary(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SaveAnimals(save);
|
||||||
|
|
||||||
|
// Население — живой счётчик животных на момент сохранения (а не статичный пресет конфига).
|
||||||
|
save.Population = save.Animals.Count;
|
||||||
|
|
||||||
|
new SaveStore().Write(save);
|
||||||
|
Log.Info(
|
||||||
|
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
|
||||||
|
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses, {save.Eggs.Count} eggs)"
|
||||||
|
);
|
||||||
|
return _config.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Снимок популяции животных и трупов: всё нужное, чтобы эволюция/состояние пережили сейв/лоад
|
||||||
|
// (а не пересыпались из сида). Health/Mood/Pregnant берём через entity — запрос ограничен 5 типами.
|
||||||
|
private void SaveAnimals(WorldSave save)
|
||||||
|
{
|
||||||
|
Store
|
||||||
|
.Query<AnimalOrganism, AnimalGrowth, AnimalNeeds, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(
|
||||||
|
ref AnimalOrganism org,
|
||||||
|
ref AnimalGrowth grow,
|
||||||
|
ref AnimalNeeds needs,
|
||||||
|
ref Transform2D transform,
|
||||||
|
Entity entity
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
var record = new AnimalSave
|
||||||
|
{
|
||||||
|
Species = _animals[org.Species].Def.DefName,
|
||||||
|
X = transform.Position.X,
|
||||||
|
Y = transform.Position.Y,
|
||||||
|
Generation = org.Generation,
|
||||||
|
Stage = grow.Stage,
|
||||||
|
AgeDays = grow.AgeDays,
|
||||||
|
Genome = org.Genome.ToDictionary(),
|
||||||
|
Needs = needs.Values,
|
||||||
|
Starve = needs.Starve,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entity.GetComponent<Health>().State is { } health)
|
||||||
|
{
|
||||||
|
record.BloodLevel = health.BloodLevel;
|
||||||
|
record.PartHp = health.Parts.Select(p => p.Hp).ToArray();
|
||||||
|
foreach (var hediff in health.Hediffs)
|
||||||
|
{
|
||||||
|
record.Hediffs.Add(
|
||||||
|
new HediffSave
|
||||||
|
{
|
||||||
|
DefName = hediff.Def.DefName,
|
||||||
|
Severity = hediff.Severity,
|
||||||
|
Immunity = hediff.Immunity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mood = entity.GetComponent<Mood>();
|
||||||
|
record.Mood = mood.Value;
|
||||||
|
record.MoodActive = mood.Active;
|
||||||
|
if (mood.Thoughts is not null)
|
||||||
|
{
|
||||||
|
foreach (var thought in mood.Thoughts)
|
||||||
|
{
|
||||||
|
record.Thoughts.Add(
|
||||||
|
new ThoughtSave
|
||||||
|
{
|
||||||
|
DefName = thought.Def.DefName,
|
||||||
|
RemainingDays = thought.RemainingDays,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.TryGetComponent<Pregnant>(out var preg))
|
||||||
|
{
|
||||||
|
record.Pregnant = true;
|
||||||
|
record.FatherGenome = preg.FatherGenome.ToDictionary();
|
||||||
|
record.DueInDays = preg.DueInDays;
|
||||||
|
record.Litter = preg.Litter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.TryGetComponent<Skills>(out var sk))
|
||||||
|
{
|
||||||
|
record.SkillLevels = sk.Levels ?? [];
|
||||||
|
record.SkillPassions = sk.Passions is null
|
||||||
|
? []
|
||||||
|
: Array.ConvertAll(sk.Passions, p => (int)p);
|
||||||
|
}
|
||||||
|
|
||||||
|
save.Animals.Add(record);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Store
|
||||||
|
.Query<Corpse, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref Corpse corpse, ref Transform2D transform, Entity _) =>
|
||||||
|
save.Corpses.Add(
|
||||||
|
new CorpseSave
|
||||||
|
{
|
||||||
|
Species = _animals[corpse.Species].Def.DefName,
|
||||||
|
X = transform.Position.X,
|
||||||
|
Y = transform.Position.Y,
|
||||||
|
RotDays = corpse.RotDays,
|
||||||
|
BodySize = corpse.BodySize,
|
||||||
|
Meat = corpse.Meat,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Store
|
||||||
|
.Query<Egg, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref Egg egg, ref Transform2D transform, Entity _) =>
|
||||||
|
save.Eggs.Add(
|
||||||
|
new EggSave
|
||||||
|
{
|
||||||
|
Species = _animals[egg.Species].Def.DefName,
|
||||||
|
X = transform.Position.X,
|
||||||
|
Y = transform.Position.Y,
|
||||||
|
Generation = egg.Generation,
|
||||||
|
IncubateDays = egg.IncubateDays,
|
||||||
|
Genome = egg.Genome?.ToDictionary() ?? new(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestorePlants(GameContent content)
|
||||||
|
{
|
||||||
|
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
|
||||||
|
foreach (var plant in _save!.Plants)
|
||||||
|
{
|
||||||
|
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
|
||||||
|
{
|
||||||
|
continue; // вид пропал (мод убрали) — пропускаем растение
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _plants.IndexOf(def);
|
||||||
|
var genome = plant.Genome is { Count: > 0 }
|
||||||
|
? new Genome(plant.Genome)
|
||||||
|
: _plants[index].Template.Generate(fallback);
|
||||||
|
PlantFactory.Create(
|
||||||
|
Store,
|
||||||
|
_plants,
|
||||||
|
index,
|
||||||
|
new Vector2(plant.X, plant.Y),
|
||||||
|
plant.AgeDays,
|
||||||
|
genome,
|
||||||
|
plant.CellFertility,
|
||||||
|
CellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Восстанавливает популяцию животных и трупы из сейва: фабрика собирает особь из генома (геном
|
||||||
|
// несёт и пол), затем динамическое состояние (нужды/стадия/здоровье/настроение/беременность)
|
||||||
|
// накладывается поверх. Старый/битый сейв без генома — особь регенерируется из шаблона вида.
|
||||||
|
private void RestoreAnimals(GameContent content)
|
||||||
|
{
|
||||||
|
var fallback = new Random(_config.Seed);
|
||||||
|
foreach (var saved in _save!.Animals)
|
||||||
|
{
|
||||||
|
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||||
|
{
|
||||||
|
continue; // вид пропал (мод убрали) — пропускаем особь
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _animals.IndexOf(def);
|
||||||
|
var genome = saved.Genome is { Count: > 0 }
|
||||||
|
? new Genome(saved.Genome)
|
||||||
|
: _animals.GenerateGenome(index, fallback);
|
||||||
|
var entity = AnimalFactory.Create(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
index,
|
||||||
|
new Vector2(saved.X, saved.Y),
|
||||||
|
saved.AgeDays,
|
||||||
|
genome,
|
||||||
|
saved.Generation,
|
||||||
|
CellSize
|
||||||
|
);
|
||||||
|
RestoreAnimalState(entity, saved, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var saved in _save.Corpses)
|
||||||
|
{
|
||||||
|
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var corpse = AnimalFactory.CreateCorpse(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_animals.IndexOf(def),
|
||||||
|
new Vector2(saved.X, saved.Y),
|
||||||
|
saved.BodySize,
|
||||||
|
saved.Meat,
|
||||||
|
CellSize
|
||||||
|
);
|
||||||
|
ref var data = ref corpse.GetComponent<Corpse>();
|
||||||
|
data.RotDays = saved.RotDays;
|
||||||
|
data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var saved in _save.Eggs)
|
||||||
|
{
|
||||||
|
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||||
|
{
|
||||||
|
continue; // вид пропал (мод убрали) — пропускаем яйцо
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _animals.IndexOf(def);
|
||||||
|
var genome = saved.Genome is { Count: > 0 }
|
||||||
|
? new Genome(saved.Genome)
|
||||||
|
: _animals.GenerateGenome(index, fallback);
|
||||||
|
AnimalFactory.CreateEgg(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
index,
|
||||||
|
new Vector2(saved.X, saved.Y),
|
||||||
|
genome,
|
||||||
|
saved.Generation,
|
||||||
|
saved.IncubateDays,
|
||||||
|
CellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info(
|
||||||
|
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses, {_save.Eggs.Count} eggs from save"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Накладывает на свежесозданную особь сохранённое динамическое состояние. Ref-компоненты правим до
|
||||||
|
// AddComponent(Pregnant) (структурное изменение инвалидирует ссылки); Health — managed, безопасен.
|
||||||
|
private void RestoreAnimalState(Entity entity, AnimalSave saved, GameContent content)
|
||||||
|
{
|
||||||
|
ref var needs = ref entity.GetComponent<AnimalNeeds>();
|
||||||
|
if (saved.Needs is { Length: > 0 } && saved.Needs.Length == _needs.Count)
|
||||||
|
{
|
||||||
|
needs.Values = saved.Needs; // состав совпал — иначе остаются свежие из фабрики
|
||||||
|
}
|
||||||
|
|
||||||
|
needs.Starve = saved.Starve;
|
||||||
|
entity.GetComponent<AnimalGrowth>().Stage = saved.Stage;
|
||||||
|
|
||||||
|
ref var mood = ref entity.GetComponent<Mood>();
|
||||||
|
mood.Value = saved.Mood;
|
||||||
|
mood.Active = saved.MoodActive;
|
||||||
|
mood.Thoughts.Clear();
|
||||||
|
foreach (var thought in saved.Thoughts)
|
||||||
|
{
|
||||||
|
if (content.Defs.TryGet<ThoughtDef>(thought.DefName, out var tdef))
|
||||||
|
{
|
||||||
|
mood.Thoughts.Add(
|
||||||
|
new ThoughtInstance { Def = tdef, RemainingDays = thought.RemainingDays }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.GetComponent<Health>().State is { } health)
|
||||||
|
{
|
||||||
|
health.BloodLevel = saved.BloodLevel;
|
||||||
|
// HP частей применяем только при точном совпадении анатомии (как для нужд/навыков): иначе
|
||||||
|
// мод сменил состав/порядок частей тела, и значения легли бы не на те части — оставляем свежее.
|
||||||
|
if (saved.PartHp.Length == health.Parts.Length)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < health.Parts.Length; i++)
|
||||||
|
{
|
||||||
|
health.Parts[i].Hp = saved.PartHp[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (saved.PartHp.Length > 0)
|
||||||
|
{
|
||||||
|
Log.Warning(
|
||||||
|
$"Скелет вида '{saved.Species}' изменился ({saved.PartHp.Length}→{health.Parts.Length} частей) — HP частей не восстановлены."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var hediff in saved.Hediffs)
|
||||||
|
{
|
||||||
|
if (content.Defs.TryGet<HediffDef>(hediff.DefName, out var hdef))
|
||||||
|
{
|
||||||
|
health.AddHediff(hdef, hediff.Severity, hediff.Immunity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
health.RecomputeCapacities();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saved.Pregnant && saved.FatherGenome is { Count: > 0 })
|
||||||
|
{
|
||||||
|
entity.AddComponent(
|
||||||
|
new Pregnant
|
||||||
|
{
|
||||||
|
FatherGenome = new Genome(saved.FatherGenome),
|
||||||
|
DueInDays = saved.DueInDays,
|
||||||
|
Litter = saved.Litter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Навыки: накладываем сохранённые уровни/страсть, если состав совпал (иначе — свежие из фабрики).
|
||||||
|
if (entity.TryGetComponent<Skills>(out var skills) && skills.Levels is not null)
|
||||||
|
{
|
||||||
|
if (saved.SkillLevels.Length == skills.Levels.Length)
|
||||||
|
{
|
||||||
|
saved.SkillLevels.CopyTo(skills.Levels, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skills.Passions is not null && saved.SkillPassions.Length == skills.Passions.Length)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < skills.Passions.Length; i++)
|
||||||
|
{
|
||||||
|
skills.Passions[i] = (byte)Math.Clamp(saved.SkillPassions[i], 0, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0
|
||||||
|
// рассыпается по клеткам суши с плотностью из дефа; геном и возраст особи — из шаблона вида.
|
||||||
|
private void SpawnAnimals()
|
||||||
|
{
|
||||||
|
var landCells = 0;
|
||||||
|
for (var i = 0; i < _cellLand.Length; i++)
|
||||||
|
{
|
||||||
|
if (_cellLand[i])
|
||||||
|
{
|
||||||
|
landCells++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_animals.Count == 0 || landCells == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Свой офсет сида (0x5EED занят системой решений животных — нельзя связывать их RNG-потоки).
|
||||||
|
var random = new Random(_config.Seed + 0x5A1D);
|
||||||
|
var spawned = 0;
|
||||||
|
for (var s = 0; s < _animals.Count; s++)
|
||||||
|
{
|
||||||
|
var def = _animals[s].Def;
|
||||||
|
if (def.SpawnPer1000Cells <= 0f)
|
||||||
|
{
|
||||||
|
continue; // вид не спавнится сам (напр. появляется только в сейве/событием)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Math.Max(1, (int)MathF.Round(landCells / 1000f * def.SpawnPer1000Cells));
|
||||||
|
var maxAge =
|
||||||
|
(def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f)
|
||||||
|
* 0.85f;
|
||||||
|
var placed = 0;
|
||||||
|
var attempts = 0;
|
||||||
|
while (placed < count && attempts < count * 50)
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
var cell = random.Next(_cellLand.Length);
|
||||||
|
if (!_cellLand[cell])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cx = cell % _config.Width;
|
||||||
|
var cy = cell / _config.Width;
|
||||||
|
var px = (cx + 0.2f + random.NextSingle() * 0.6f) * CellSize;
|
||||||
|
var py = (cy + 0.2f + random.NextSingle() * 0.6f) * CellSize;
|
||||||
|
AnimalFactory.Create(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
s,
|
||||||
|
new Vector2(px, py),
|
||||||
|
ageDays: random.NextSingle() * maxAge,
|
||||||
|
_animals.GenerateGenome(s, random),
|
||||||
|
generation: 0,
|
||||||
|
CellSize
|
||||||
|
);
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawned += placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info($"Spawned {spawned} animals ({_animals.Count} species)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Assets;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
using MrGameEng.Formulas;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Inspector;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
using MrGameEng.Tilemaps;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
// Часть WorldScene: генерация рельефа, поле берега и окклюдеры освещения.
|
||||||
|
public sealed partial class WorldScene
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
|
||||||
|
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
|
||||||
|
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
|
||||||
|
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
|
||||||
|
/// </summary>
|
||||||
|
private void BuildTerrain(
|
||||||
|
GameContent content,
|
||||||
|
ModAtlases atlases,
|
||||||
|
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||||||
|
AssetManager assets,
|
||||||
|
Random? scatterRandom
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||||||
|
|
||||||
|
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
|
||||||
|
var tiles = new TileSet();
|
||||||
|
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
||||||
|
foreach (var terrain in content.Terrains.All)
|
||||||
|
{
|
||||||
|
tileByDef[terrain] = terrain.Surface is { } surface
|
||||||
|
? tiles.Add(atlases.GetRegion(device, surface))
|
||||||
|
: tiles.Add(white, terrain.Tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
|
||||||
|
var heights = WorldGenerator.Generate(
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
_config.Seed,
|
||||||
|
_config.SmoothPasses
|
||||||
|
);
|
||||||
|
var grid = new TileGrid(_config.Width, _config.Height);
|
||||||
|
var cells = _config.Width * _config.Height;
|
||||||
|
_cellFertility = new float[cells];
|
||||||
|
_cellLand = new bool[cells];
|
||||||
|
_cellOccluderBase = new bool[cells];
|
||||||
|
_cellOccluder = new bool[cells];
|
||||||
|
for (var x = 0; x < _config.Width; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < _config.Height; y++)
|
||||||
|
{
|
||||||
|
var terrain = content.Terrains.Classify(heights[x, y]);
|
||||||
|
grid[x, y] = tileByDef[terrain];
|
||||||
|
var cell = y * _config.Width + x;
|
||||||
|
_cellFertility[cell] = terrain.Fertility;
|
||||||
|
_cellLand[cell] = terrain.IsLand;
|
||||||
|
_cellOccluderBase[cell] = terrain.BlocksLight;
|
||||||
|
if (scatterRandom is not null)
|
||||||
|
{
|
||||||
|
ScatterSpawner.Spawn(
|
||||||
|
this,
|
||||||
|
content,
|
||||||
|
_plants,
|
||||||
|
terrain,
|
||||||
|
new Point(x, y),
|
||||||
|
CellSize,
|
||||||
|
scatterRandom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поле «ближайший берег» по клеткам мира: для каждой клетки — мировая позиция ближайшей береговой
|
||||||
|
// клетки (суша, граничащая с водой), найденная многоисточниковым BFS за O(клеток). Звери читают его
|
||||||
|
// за O(1) при поиске водопоя (см. AnimalDecisionSystem.TryFindShore), не сканируя береговую линию.
|
||||||
|
// Пусто, если воды/берега в мире нет — тогда пить негде (зверь не пойдёт).
|
||||||
|
private Vector2[] ComputeShoreField()
|
||||||
|
{
|
||||||
|
var w = _config.Width;
|
||||||
|
var h = _config.Height;
|
||||||
|
var count = w * h;
|
||||||
|
|
||||||
|
// BFS: nearest[c] — индекс ближайшей береговой клетки. Источники — все береговые клетки (шаг 0).
|
||||||
|
var nearest = new int[count];
|
||||||
|
Array.Fill(nearest, -1);
|
||||||
|
var queue = new Queue<int>();
|
||||||
|
for (var cell = 0; cell < count; cell++)
|
||||||
|
{
|
||||||
|
if (!_cellLand[cell])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = cell % w;
|
||||||
|
var y = cell / w;
|
||||||
|
var nearWater =
|
||||||
|
(x > 0 && !_cellLand[cell - 1])
|
||||||
|
|| (x < w - 1 && !_cellLand[cell + 1])
|
||||||
|
|| (y > 0 && !_cellLand[cell - w])
|
||||||
|
|| (y < h - 1 && !_cellLand[cell + w]);
|
||||||
|
if (nearWater)
|
||||||
|
{
|
||||||
|
nearest[cell] = cell;
|
||||||
|
queue.Enqueue(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue.Count == 0)
|
||||||
|
{
|
||||||
|
return []; // нет берега
|
||||||
|
}
|
||||||
|
|
||||||
|
// Волна по 4-связности: каждой клетке достаётся источник ближайшего фронта (берег по шагам сетки).
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var cell = queue.Dequeue();
|
||||||
|
var src = nearest[cell];
|
||||||
|
var x = cell % w;
|
||||||
|
var y = cell / w;
|
||||||
|
if (x > 0 && nearest[cell - 1] < 0)
|
||||||
|
{
|
||||||
|
nearest[cell - 1] = src;
|
||||||
|
queue.Enqueue(cell - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < w - 1 && nearest[cell + 1] < 0)
|
||||||
|
{
|
||||||
|
nearest[cell + 1] = src;
|
||||||
|
queue.Enqueue(cell + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y > 0 && nearest[cell - w] < 0)
|
||||||
|
{
|
||||||
|
nearest[cell - w] = src;
|
||||||
|
queue.Enqueue(cell - w);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < h - 1 && nearest[cell + w] < 0)
|
||||||
|
{
|
||||||
|
nearest[cell + w] = src;
|
||||||
|
queue.Enqueue(cell + w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var field = new Vector2[count];
|
||||||
|
for (var cell = 0; cell < count; cell++)
|
||||||
|
{
|
||||||
|
var src = nearest[cell];
|
||||||
|
if (src >= 0)
|
||||||
|
{
|
||||||
|
field[cell] = new Vector2((src % w + 0.5f) * CellSize, (src / w + 0.5f) * CellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Затеняют ли зрелые деревья землю. Сейчас выключено: одиночное дерево = 1 клетка-окклюдер, а спрайт
|
||||||
|
// кроны крупнее и смещён, плюс направленная тень тянется «хвостом» — на открытой почве это читается как
|
||||||
|
// странные оторванные пятна. Вернуть, когда у кроны будет настоящий многоклеточный след затенения.
|
||||||
|
private static readonly bool TreesCastShade = false;
|
||||||
|
|
||||||
|
// Сетка окклюдеров для лайтмапа: светонепроницаемый террейн (сейчас такого нет) + опц. зрелые деревья.
|
||||||
|
private bool[] BuildOccluders()
|
||||||
|
{
|
||||||
|
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
|
||||||
|
if (!TreesCastShade)
|
||||||
|
{
|
||||||
|
return _cellOccluder; // ровное освещение — без странных теней от отдельных деревьев
|
||||||
|
}
|
||||||
|
|
||||||
|
Store
|
||||||
|
.Query<PlantGrowth, Transform2D>()
|
||||||
|
.ForEachEntity(
|
||||||
|
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
|
||||||
|
{
|
||||||
|
var species = _plants[grow.Species];
|
||||||
|
if (
|
||||||
|
species.Def.TrunkRadiusCells <= 0f
|
||||||
|
|| grow.Stage < species.Stages.Length - 1
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return; // затеняют только зрелые деревья (со стволом)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cx = Math.Clamp(
|
||||||
|
(int)(transform.Position.X / CellSize),
|
||||||
|
0,
|
||||||
|
_config.Width - 1
|
||||||
|
);
|
||||||
|
var cy = Math.Clamp(
|
||||||
|
(int)(transform.Position.Y / CellSize),
|
||||||
|
0,
|
||||||
|
_config.Height - 1
|
||||||
|
);
|
||||||
|
_cellOccluder[cy * _config.Width + cx] = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return _cellOccluder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.App;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using LittleSim.UI;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Assets;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.DevConsole;
|
||||||
|
using MrGameEng.Formulas;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Host;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
using MrGameEng.Inspector;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
using MrGameEng.Tilemaps;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
|
// Часть WorldScene: HUD-хелперы, полоса скорости, горячие клавиши, переходы.
|
||||||
|
public sealed partial class WorldScene
|
||||||
|
{
|
||||||
|
// Переход к «ответственному» за событие (двойной клик в журнале): выбирает сущность и центрирует на
|
||||||
|
// ней камеру, если она ещё существует (актёр мог умереть). Камера зажимается к границам при отрисовке.
|
||||||
|
private void FocusEntity(Entity camera, int id)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
id < 0
|
||||||
|
|| !Store.TryGetEntityById(id, out var target)
|
||||||
|
|| target.IsNull
|
||||||
|
|| !target.HasComponent<Transform2D>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selection.Select(id);
|
||||||
|
ref var cam = ref camera.GetComponent<Camera>();
|
||||||
|
cam.Position = target.GetComponent<Transform2D>().Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка-переключатель оверлея HUD: подсвечивается как активная, пока оверлей открыт.
|
||||||
|
private static TextButton HudToggle(string text, Action onClick, Func<bool> isOpen)
|
||||||
|
{
|
||||||
|
var button = Ui.ChoiceButton(text);
|
||||||
|
button.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
onClick();
|
||||||
|
Ui.SetChoiceActive(button, isOpen());
|
||||||
|
};
|
||||||
|
Ui.SetChoiceActive(button, isOpen());
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
||||||
|
{
|
||||||
|
var bar = Ui.Row(6);
|
||||||
|
bar.HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
bar.VerticalAlignment = VerticalAlignment.Bottom;
|
||||||
|
bar.Margin = new Thickness(0, 0, 0, 12);
|
||||||
|
|
||||||
|
var hover = new SolidBrush(new Color(40, 54, 72)); // подсветка кнопок скорости при наведении
|
||||||
|
var pause = new TextButton
|
||||||
|
{
|
||||||
|
Text = content.Languages.Get("speed.pause"),
|
||||||
|
Padding = new Thickness(8, 4),
|
||||||
|
OverBackground = hover,
|
||||||
|
};
|
||||||
|
pause.Click += (_, _) => _speed.Pause();
|
||||||
|
bar.Widgets.Add(pause);
|
||||||
|
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
||||||
|
|
||||||
|
for (var i = 0; i < _speed.Steps.Count; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
var button = new TextButton
|
||||||
|
{
|
||||||
|
Text = $"x{_speed.Steps[i]:0}",
|
||||||
|
Padding = new Thickness(8, 4),
|
||||||
|
OverBackground = hover,
|
||||||
|
};
|
||||||
|
button.Click += (_, _) => _speed.SetStep(index);
|
||||||
|
bar.Widgets.Add(button);
|
||||||
|
_speedRefreshers.Add(() =>
|
||||||
|
button.TextColor =
|
||||||
|
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Refresh()
|
||||||
|
{
|
||||||
|
foreach (var refresh in _speedRefreshers)
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_speed.Changed += Refresh;
|
||||||
|
RegisterUnload(() => _speed.Changed -= Refresh);
|
||||||
|
Refresh();
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Hotkeys(InputManager input)
|
||||||
|
{
|
||||||
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
|
||||||
|
if (_selection.HasSelection)
|
||||||
|
{
|
||||||
|
_selection.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pause.Toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pause.IsOpen)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.Space))
|
||||||
|
{
|
||||||
|
_speed.TogglePause();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.D1))
|
||||||
|
{
|
||||||
|
_speed.SetStep(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
|
||||||
|
{
|
||||||
|
_speed.SetStep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
|
||||||
|
{
|
||||||
|
_speed.SetStep(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Switch(Scene scene)
|
||||||
|
{
|
||||||
|
if (!Context.Scenes.IsTransitioning)
|
||||||
|
{
|
||||||
|
_speed.Resume();
|
||||||
|
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+300
-470
@@ -20,6 +20,7 @@ using MrGameEng.Lighting;
|
|||||||
using MrGameEng.Tilemaps;
|
using MrGameEng.Tilemaps;
|
||||||
using MrGameEng.UI;
|
using MrGameEng.UI;
|
||||||
using Myra.Graphics2D;
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
using Myra.Graphics2D.UI;
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
namespace LittleSim.Scenes;
|
namespace LittleSim.Scenes;
|
||||||
@@ -32,7 +33,7 @@ namespace LittleSim.Scenes;
|
|||||||
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
|
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
|
||||||
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
|
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorldScene : Scene
|
public sealed partial class WorldScene : Scene
|
||||||
{
|
{
|
||||||
public const int CellSize = 16;
|
public const int CellSize = 16;
|
||||||
|
|
||||||
@@ -62,13 +63,19 @@ public sealed class WorldScene : Scene
|
|||||||
private PauseMenu _pause = null!;
|
private PauseMenu _pause = null!;
|
||||||
private Selection _selection = null!;
|
private Selection _selection = null!;
|
||||||
private InspectPanel _inspect = null!;
|
private InspectPanel _inspect = null!;
|
||||||
|
private PerfPanel? _perf; // оверлей производительности (только в дев-режиме)
|
||||||
|
private WorldEvents _events = null!; // журнал событий мира (фабрика записей истории особей)
|
||||||
|
private EventsPanel _eventsPanel = null!; // оверлей глобального журнала событий
|
||||||
private readonly List<Action> _speedRefreshers = [];
|
private readonly List<Action> _speedRefreshers = [];
|
||||||
|
|
||||||
private PlantSet _plants = null!;
|
private PlantSet _plants = null!;
|
||||||
|
private AnimalSet _animals = null!;
|
||||||
|
private NeedSet _needs = null!;
|
||||||
|
private SkillSet _skills = null!;
|
||||||
private float[] _cellFertility = [];
|
private float[] _cellFertility = [];
|
||||||
private bool[] _cellLand = [];
|
private bool[] _cellLand = [];
|
||||||
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
|
private bool[] _cellOccluderBase = []; // светонепроницаемый террейн (статично; сейчас такого нет)
|
||||||
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
|
private bool[] _cellOccluder = []; // террейн-окклюдеры + зрелые деревья (пересобирается лайтмапом)
|
||||||
|
|
||||||
/// <summary>Новый мир из конфига.</summary>
|
/// <summary>Новый мир из конфига.</summary>
|
||||||
public WorldScene(WorldConfig config)
|
public WorldScene(WorldConfig config)
|
||||||
@@ -98,7 +105,10 @@ public sealed class WorldScene : Scene
|
|||||||
GameLayers.EnsureRegistered(renderer);
|
GameLayers.EnsureRegistered(renderer);
|
||||||
this.UseTilemaps();
|
this.UseTilemaps();
|
||||||
|
|
||||||
var calendar = Context.UseCalendar(SecondsPerDay, StartHour / 24.0);
|
// Часы движка всегда стартуют с нуля, поэтому прошедшее время мира из сейва вносим
|
||||||
|
// смещением календаря (startDay): иначе загрузка сбрасывала время суток/сезон на день 0.
|
||||||
|
var startDay = StartHour / 24.0 + (_save?.ElapsedSeconds ?? 0.0) / SecondsPerDay;
|
||||||
|
var calendar = Context.UseCalendar(SecondsPerDay, startDay);
|
||||||
var climate = Context.UseClimate(
|
var climate = Context.UseClimate(
|
||||||
ClimateSettings.Default with
|
ClimateSettings.Default with
|
||||||
{
|
{
|
||||||
@@ -109,6 +119,11 @@ public sealed class WorldScene : Scene
|
|||||||
|
|
||||||
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
||||||
_plants = new PlantSet(content, atlases, device);
|
_plants = new PlantSet(content, atlases, device);
|
||||||
|
_animals = new AnimalSet(content, atlases, device);
|
||||||
|
_needs = new NeedSet(content);
|
||||||
|
_skills = new SkillSet(content);
|
||||||
|
// Журнал событий мира — нужен и HUD-оверлею (ниже), и системам животных (создаются позже).
|
||||||
|
_events = new WorldEvents(content.Languages, _animals, calendar);
|
||||||
var loadingPlants = _save?.Plants is { Count: > 0 };
|
var loadingPlants = _save?.Plants is { Count: > 0 };
|
||||||
BuildTerrain(
|
BuildTerrain(
|
||||||
content,
|
content,
|
||||||
@@ -122,6 +137,17 @@ public sealed class WorldScene : Scene
|
|||||||
RestorePlants(content);
|
RestorePlants(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Животные: из сейва (сохранённая популяция — эволюция/состояние не теряются) либо
|
||||||
|
// детерминированный спавн из сида (новый мир или старый сейв без животных).
|
||||||
|
if (_save?.Animals is { Count: > 0 })
|
||||||
|
{
|
||||||
|
RestoreAnimals(content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SpawnAnimals();
|
||||||
|
}
|
||||||
|
|
||||||
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
|
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
|
||||||
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
|
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
|
||||||
var lighting = this.UseLighting(
|
var lighting = this.UseLighting(
|
||||||
@@ -138,7 +164,7 @@ public sealed class WorldScene : Scene
|
|||||||
|
|
||||||
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
||||||
var desktop = this.UseScaledUI();
|
var desktop = this.UseScaledUI();
|
||||||
var hudLabel = new Label { Left = 10, Top = 8 };
|
var hudLabel = new Label { TextColor = Ui.Accent }; // дата/время/сезон — заполняет HudSystem
|
||||||
var speedBar = BuildSpeedBar(content);
|
var speedBar = BuildSpeedBar(content);
|
||||||
_pause = new PauseMenu(
|
_pause = new PauseMenu(
|
||||||
Context,
|
Context,
|
||||||
@@ -155,35 +181,149 @@ public sealed class WorldScene : Scene
|
|||||||
atlases.GetRegion(device, "ui/overlays/selectionbracket"),
|
atlases.GetRegion(device, "ui/overlays/selectionbracket"),
|
||||||
GameLayers.Selection
|
GameLayers.Selection
|
||||||
);
|
);
|
||||||
_inspect = new InspectPanel(Store, _plants, content, climate, _selection, brackets);
|
_inspect = new InspectPanel(
|
||||||
desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root);
|
Store,
|
||||||
|
_plants,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
content,
|
||||||
|
climate,
|
||||||
|
_selection,
|
||||||
|
brackets,
|
||||||
|
Context.Clock
|
||||||
|
);
|
||||||
|
// Режим разработчика (настройка, по умолчанию вкл): без него дев-консоль и спавнер не поднимаются.
|
||||||
|
var devMode = GameSettingsStore.Load().DeveloperMode;
|
||||||
|
var spawner = devMode
|
||||||
|
? new DevSpawner(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_plants,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
content,
|
||||||
|
_selection,
|
||||||
|
_cellFertility,
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
CellSize
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
// Кнопки-переключатели оверлеев (верх-право HUD). Perf-окно поднимается только в дев-режиме.
|
||||||
|
var hudButtons = new HorizontalStackPanel
|
||||||
|
{
|
||||||
|
Spacing = 6,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 8, 12, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Карточка статуса (верх-слева): строка даты/времени/сезона + краткие подсказки клавиш. Спавнер
|
||||||
|
// (F9) — только в дев-режиме; инспектор (F1) доступен всегда.
|
||||||
|
var hint = content.Languages.Get("hud.inspector");
|
||||||
|
if (spawner is not null)
|
||||||
|
{
|
||||||
|
hint += " · " + content.Languages.Get("hud.spawner");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hudCard = new VerticalStackPanel
|
||||||
|
{
|
||||||
|
Spacing = 3,
|
||||||
|
Padding = new Thickness(12, 8),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(10, 8, 0, 0),
|
||||||
|
Background = new SolidBrush(new Color(10, 13, 18, 210)),
|
||||||
|
Border = new SolidBrush(new Color(96, 134, 168)),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
};
|
||||||
|
hudCard.Widgets.Add(hudLabel);
|
||||||
|
hudCard.Widgets.Add(new Label { Text = hint, TextColor = Ui.Muted });
|
||||||
|
|
||||||
|
var hudChildren = new List<Widget> { hudCard, _inspect.Panel, speedBar, _pause.Root };
|
||||||
|
if (spawner is not null)
|
||||||
|
{
|
||||||
|
hudChildren.Add(spawner.Panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Журнал событий — доступен всем (кнопка «События»). Двойной клик по записи ведёт камеру к
|
||||||
|
// «ответственному» и выбирает его (если он ещё жив).
|
||||||
|
_eventsPanel = new EventsPanel(
|
||||||
|
content.Languages,
|
||||||
|
_events,
|
||||||
|
Context.Clock,
|
||||||
|
id => FocusEntity(camera, id)
|
||||||
|
);
|
||||||
|
hudChildren.Add(_eventsPanel.Panel);
|
||||||
|
hudButtons.Widgets.Add(
|
||||||
|
HudToggle(
|
||||||
|
content.Languages.Get("hud.events"),
|
||||||
|
() => _eventsPanel.Toggle(),
|
||||||
|
() => _eventsPanel.IsOpen
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (devMode)
|
||||||
|
{
|
||||||
|
_perf = new PerfPanel(content.Languages, Context.Clock, UpdateSystems, DrawSystems);
|
||||||
|
hudChildren.Add(_perf.Panel);
|
||||||
|
hudButtons.Widgets.Add(
|
||||||
|
HudToggle(
|
||||||
|
content.Languages.Get("hud.perf"),
|
||||||
|
() => _perf.Toggle(),
|
||||||
|
() => _perf.IsOpen
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
hudChildren.Add(hudButtons); // кнопки добавляем последними — поверх остального HUD (кликабельны)
|
||||||
|
desktop.Root = Ui.Screen(hudChildren.ToArray());
|
||||||
|
|
||||||
this.UseInspector(renderer);
|
this.UseInspector(renderer);
|
||||||
var console = this.UseDevConsole();
|
|
||||||
RegisterCommands(console, content, atlases);
|
MrGameEng.DevConsole.DevConsole? console = null;
|
||||||
console.Register(
|
if (devMode)
|
||||||
"light",
|
{
|
||||||
"light [radius] — place a point light at the cursor (night shadows demo)",
|
console = this.UseDevConsole();
|
||||||
(c, args) =>
|
RegisterCommands(console, content, atlases);
|
||||||
{
|
// Тайминги систем Friflo в дев-консоль: команда `perf` (см. PerfReport). Включение мониторинга
|
||||||
var radius =
|
// на все системы — в конце OnLoad, когда они уже добавлены в корни.
|
||||||
args.Length > 0
|
PerfReport.Register(console, UpdateSystems, DrawSystems);
|
||||||
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
|
console.Register(
|
||||||
: 120f;
|
"light",
|
||||||
var mouse = Mouse.GetState();
|
"light [radius] — place a point light at the cursor (night shadows demo)",
|
||||||
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
(c, args) =>
|
||||||
Store.CreateEntity(
|
{
|
||||||
Transform2D.At(world),
|
var radius =
|
||||||
new PointLight
|
args.Length > 0
|
||||||
{
|
? float.Parse(
|
||||||
Radius = radius,
|
args[0],
|
||||||
Color = Color.White,
|
System.Globalization.CultureInfo.InvariantCulture
|
||||||
Intensity = 0.9f,
|
)
|
||||||
}
|
: 120f;
|
||||||
);
|
var mouse = Mouse.GetState();
|
||||||
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
|
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||||||
}
|
Store.CreateEntity(
|
||||||
);
|
Transform2D.At(world),
|
||||||
|
new PointLight
|
||||||
|
{
|
||||||
|
Radius = radius,
|
||||||
|
Color = Color.White,
|
||||||
|
Intensity = 0.9f,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spawner is not null)
|
||||||
|
{
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new DevSpawnerSystem(renderer, spawner, () => desktop.IsMouseOverGUI)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
|
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
|
||||||
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
||||||
@@ -203,6 +343,119 @@ public sealed class WorldScene : Scene
|
|||||||
_config.Seed
|
_config.Seed
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
// Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет
|
||||||
|
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
|
||||||
|
var shoreByCell = ComputeShoreField();
|
||||||
|
var thoughts = new ThoughtSet(content);
|
||||||
|
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs, _animals));
|
||||||
|
UpdateSystems.Add(new AnimalSkillSystem(Context.Clock, SecondsPerDay, _skills));
|
||||||
|
UpdateSystems.Add(new AnimalMemorySystem(Context.Clock, SecondsPerDay));
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalHealthSystem(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_events,
|
||||||
|
CellSize,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
climate,
|
||||||
|
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(),
|
||||||
|
content.Defs.TryGet<HediffDef>("Hypothermia", out var hypo) ? hypo : null,
|
||||||
|
content.Defs.TryGet<HediffDef>("Heatstroke", out var heat) ? heat : null,
|
||||||
|
content.Defs.TryGet<HediffDef>("Infection", out var infe) ? infe : null,
|
||||||
|
_config.Seed + 0x3EAD
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// Настроение (A8) до выбора решения: стресс (низкое настроение) подавляет спаривание.
|
||||||
|
UpdateSystems.Add(new AnimalMoodSystem(Context.Clock, SecondsPerDay, _needs));
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalDecisionSystem(
|
||||||
|
Store,
|
||||||
|
Context.Clock,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
CellSize,
|
||||||
|
shoreByCell,
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
_events,
|
||||||
|
_config.Seed + 0x5EED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
var bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var bl) ? bl : null;
|
||||||
|
var poisoned = content.Defs.TryGet<HediffDef>("Poisoned", out var ps) ? ps : null;
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalActionSystem(
|
||||||
|
Store,
|
||||||
|
_plants,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
thoughts,
|
||||||
|
_events,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
CellSize,
|
||||||
|
_bounds,
|
||||||
|
bleeding,
|
||||||
|
poisoned,
|
||||||
|
_config.Seed + 0x1B17
|
||||||
|
)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
|
||||||
|
UpdateSystems.Add(new DamageFlashSystem(Context.Clock)); // после внешнего вида — вспышка поверх
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize, _events)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(new AnimalFacingSystem(_animals, CellSize));
|
||||||
|
UpdateSystems.Add(new AnimalBobSystem(Context.Clock));
|
||||||
|
UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, _events, CellSize));
|
||||||
|
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new AnimalPregnancySystem(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
thoughts,
|
||||||
|
_events,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
CellSize,
|
||||||
|
maxAnimals: 500,
|
||||||
|
_config.Seed + 0x4BED
|
||||||
|
)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new EggSystem(
|
||||||
|
Store,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
_events,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
CellSize,
|
||||||
|
_config.Seed + 0x6E66
|
||||||
|
)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new SeedDispersalSystem(
|
||||||
|
Store,
|
||||||
|
_plants,
|
||||||
|
Context.Clock,
|
||||||
|
SecondsPerDay,
|
||||||
|
_config.Width,
|
||||||
|
_config.Height,
|
||||||
|
CellSize,
|
||||||
|
_cellFertility,
|
||||||
|
_cellLand
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||||
);
|
);
|
||||||
@@ -216,451 +469,28 @@ public sealed class WorldScene : Scene
|
|||||||
() => desktop.IsMouseOverGUI
|
() => desktop.IsMouseOverGUI
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
UpdateSystems.Add(new CallbackSystem(() => _inspect.Refresh()));
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new HudSystem(
|
new CallbackSystem(() =>
|
||||||
Context,
|
{
|
||||||
content.Languages,
|
_inspect.Refresh();
|
||||||
hudLabel,
|
_eventsPanel.Refresh();
|
||||||
"hud.world",
|
_perf?.Refresh();
|
||||||
_config.Seed,
|
})
|
||||||
calendar,
|
|
||||||
climate
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
UpdateSystems.Add(new HudSystem(Context, content.Languages, hudLabel, calendar, climate));
|
||||||
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
|
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
|
||||||
|
|
||||||
|
// Все системы добавлены — включаем per-system мониторинг (дев-режим), чтобы команда `perf`
|
||||||
|
// показывала данные с первого кадра. Вне дев-режима мониторинг не трогаем (нулевой оверхед).
|
||||||
|
if (devMode)
|
||||||
|
{
|
||||||
|
UpdateSystems.SetMonitorPerf(true);
|
||||||
|
DrawSystems.SetMonitorPerf(true);
|
||||||
|
}
|
||||||
|
|
||||||
Log.Info(
|
Log.Info(
|
||||||
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
|
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
|
||||||
+ (_save is null ? " (new)" : " (loaded)")
|
+ (_save is null ? " (new)" : " (loaded)")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
|
|
||||||
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
|
|
||||||
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
|
|
||||||
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
|
|
||||||
/// </summary>
|
|
||||||
private void BuildTerrain(
|
|
||||||
GameContent content,
|
|
||||||
ModAtlases atlases,
|
|
||||||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
|
||||||
AssetManager assets,
|
|
||||||
Random? scatterRandom
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
|
||||||
|
|
||||||
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
|
|
||||||
var tiles = new TileSet();
|
|
||||||
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
|
||||||
foreach (var terrain in content.Terrains.All)
|
|
||||||
{
|
|
||||||
tileByDef[terrain] = terrain.Surface is { } surface
|
|
||||||
? tiles.Add(atlases.GetRegion(device, surface))
|
|
||||||
: tiles.Add(white, terrain.Tint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
|
|
||||||
var heights = WorldGenerator.Generate(
|
|
||||||
_config.Width,
|
|
||||||
_config.Height,
|
|
||||||
_config.Seed,
|
|
||||||
_config.SmoothPasses
|
|
||||||
);
|
|
||||||
var grid = new TileGrid(_config.Width, _config.Height);
|
|
||||||
var cells = _config.Width * _config.Height;
|
|
||||||
_cellFertility = new float[cells];
|
|
||||||
_cellLand = new bool[cells];
|
|
||||||
_cellOccluderBase = new bool[cells];
|
|
||||||
_cellOccluder = new bool[cells];
|
|
||||||
for (var x = 0; x < _config.Width; x++)
|
|
||||||
{
|
|
||||||
for (var y = 0; y < _config.Height; y++)
|
|
||||||
{
|
|
||||||
var terrain = content.Terrains.Classify(heights[x, y]);
|
|
||||||
grid[x, y] = tileByDef[terrain];
|
|
||||||
var cell = y * _config.Width + x;
|
|
||||||
_cellFertility[cell] = terrain.Fertility;
|
|
||||||
_cellLand[cell] = terrain.IsLand;
|
|
||||||
_cellOccluderBase[cell] = terrain.BlocksLight;
|
|
||||||
if (scatterRandom is not null)
|
|
||||||
{
|
|
||||||
ScatterSpawner.Spawn(
|
|
||||||
this,
|
|
||||||
content,
|
|
||||||
_plants,
|
|
||||||
terrain,
|
|
||||||
new Point(x, y),
|
|
||||||
CellSize,
|
|
||||||
scatterRandom
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
|
||||||
{
|
|
||||||
var bar = Ui.Row(6);
|
|
||||||
bar.HorizontalAlignment = HorizontalAlignment.Center;
|
|
||||||
bar.VerticalAlignment = VerticalAlignment.Bottom;
|
|
||||||
bar.Margin = new Thickness(0, 0, 0, 12);
|
|
||||||
|
|
||||||
var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
|
|
||||||
pause.Click += (_, _) => _speed.Pause();
|
|
||||||
bar.Widgets.Add(pause);
|
|
||||||
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
|
||||||
|
|
||||||
for (var i = 0; i < _speed.Steps.Count; i++)
|
|
||||||
{
|
|
||||||
var index = i;
|
|
||||||
var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
|
|
||||||
button.Click += (_, _) => _speed.SetStep(index);
|
|
||||||
bar.Widgets.Add(button);
|
|
||||||
_speedRefreshers.Add(() =>
|
|
||||||
button.TextColor =
|
|
||||||
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Refresh()
|
|
||||||
{
|
|
||||||
foreach (var refresh in _speedRefreshers)
|
|
||||||
{
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_speed.Changed += Refresh;
|
|
||||||
RegisterUnload(() => _speed.Changed -= Refresh);
|
|
||||||
Refresh();
|
|
||||||
return bar;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Hotkeys(InputManager input)
|
|
||||||
{
|
|
||||||
if (input.IsKeyPressed(Keys.Escape))
|
|
||||||
{
|
|
||||||
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
|
|
||||||
if (_selection.HasSelection)
|
|
||||||
{
|
|
||||||
_selection.Clear();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_pause.Toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pause.IsOpen)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.IsKeyPressed(Keys.Space))
|
|
||||||
{
|
|
||||||
_speed.TogglePause();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.IsKeyPressed(Keys.D1))
|
|
||||||
{
|
|
||||||
_speed.SetStep(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
|
|
||||||
{
|
|
||||||
_speed.SetStep(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
|
|
||||||
{
|
|
||||||
_speed.SetStep(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SaveWorld()
|
|
||||||
{
|
|
||||||
var save = new WorldSave
|
|
||||||
{
|
|
||||||
Name = _config.Name,
|
|
||||||
Width = _config.Width,
|
|
||||||
Height = _config.Height,
|
|
||||||
Seed = _config.Seed,
|
|
||||||
SmoothPasses = _config.SmoothPasses,
|
|
||||||
Population = _config.Population,
|
|
||||||
SavedUtc = DateTime.UtcNow,
|
|
||||||
ElapsedSeconds = Context.Clock.TotalTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
|
|
||||||
Store
|
|
||||||
.Query<PlantGrowth, PlantOrganism, Transform2D>()
|
|
||||||
.ForEachEntity(
|
|
||||||
(
|
|
||||||
ref PlantGrowth grow,
|
|
||||||
ref PlantOrganism org,
|
|
||||||
ref Transform2D transform,
|
|
||||||
Entity _
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
save.Plants.Add(
|
|
||||||
new PlantSave
|
|
||||||
{
|
|
||||||
Species = _plants[grow.Species].Def.DefName,
|
|
||||||
X = transform.Position.X,
|
|
||||||
Y = transform.Position.Y,
|
|
||||||
AgeDays = grow.AgeDays,
|
|
||||||
Stage = grow.Stage,
|
|
||||||
CellFertility = grow.CellFertility,
|
|
||||||
Genome = org.Genome.ToDictionary(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
new SaveStore().Write(save);
|
|
||||||
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
|
|
||||||
return _config.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RestorePlants(GameContent content)
|
|
||||||
{
|
|
||||||
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
|
|
||||||
foreach (var plant in _save!.Plants)
|
|
||||||
{
|
|
||||||
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
|
|
||||||
{
|
|
||||||
continue; // вид пропал (мод убрали) — пропускаем растение
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = _plants.IndexOf(def);
|
|
||||||
var genome = plant.Genome is { Count: > 0 }
|
|
||||||
? new Genome(plant.Genome)
|
|
||||||
: _plants[index].Template.Generate(fallback);
|
|
||||||
PlantFactory.Create(
|
|
||||||
Store,
|
|
||||||
_plants,
|
|
||||||
index,
|
|
||||||
new Vector2(plant.X, plant.Y),
|
|
||||||
plant.AgeDays,
|
|
||||||
genome,
|
|
||||||
plant.CellFertility,
|
|
||||||
CellSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
|
|
||||||
private bool[] BuildOccluders()
|
|
||||||
{
|
|
||||||
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
|
|
||||||
Store
|
|
||||||
.Query<PlantGrowth, Transform2D>()
|
|
||||||
.ForEachEntity(
|
|
||||||
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
|
|
||||||
{
|
|
||||||
var species = _plants[grow.Species];
|
|
||||||
if (
|
|
||||||
species.Def.TrunkRadiusCells <= 0f
|
|
||||||
|| grow.Stage < species.Stages.Length - 1
|
|
||||||
)
|
|
||||||
{
|
|
||||||
return; // затеняют только зрелые деревья (со стволом)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cx = Math.Clamp(
|
|
||||||
(int)(transform.Position.X / CellSize),
|
|
||||||
0,
|
|
||||||
_config.Width - 1
|
|
||||||
);
|
|
||||||
var cy = Math.Clamp(
|
|
||||||
(int)(transform.Position.Y / CellSize),
|
|
||||||
0,
|
|
||||||
_config.Height - 1
|
|
||||||
);
|
|
||||||
_cellOccluder[cy * _config.Width + cx] = true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return _cellOccluder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Switch(Scene scene)
|
|
||||||
{
|
|
||||||
if (!Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
_speed.Resume();
|
|
||||||
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
|
|
||||||
{
|
|
||||||
ContentCommands.Register(console, content, atlases);
|
|
||||||
console.Register(
|
|
||||||
"regen",
|
|
||||||
"regen [seed] — regenerate the world with a new seed",
|
|
||||||
(c, args) =>
|
|
||||||
{
|
|
||||||
if (Context.Scenes.IsTransitioning)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
|
||||||
c.WriteLine($"regenerating world, seed {seed}");
|
|
||||||
Context.Scenes.Switch(
|
|
||||||
new WorldScene(_config with { Seed = seed }),
|
|
||||||
Transitions.Fade(0.6f)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.Register(
|
|
||||||
"formula",
|
|
||||||
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
|
|
||||||
(c, args) =>
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
c.WriteLine(
|
|
||||||
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expression = string.Join(' ', args);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
|
|
||||||
}
|
|
||||||
catch (FormulaException e)
|
|
||||||
{
|
|
||||||
c.WriteLine($"error: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.Register(
|
|
||||||
"gene",
|
|
||||||
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
|
|
||||||
(c, args) => RunGeneDemo(c, content, args)
|
|
||||||
);
|
|
||||||
console.Register(
|
|
||||||
"plant",
|
|
||||||
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
|
||||||
(c, args) => RunPlantDemo(c, content, args)
|
|
||||||
);
|
|
||||||
console.Register(
|
|
||||||
"menu",
|
|
||||||
"menu — return to the main menu",
|
|
||||||
(_, _) => Switch(new MainMenuScene())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
|
|
||||||
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
|
|
||||||
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
|
|
||||||
{
|
|
||||||
var genes = content.Defs.All<GeneDef>();
|
|
||||||
if (genes.Count == 0)
|
|
||||||
{
|
|
||||||
console.WriteLine("no Gene defs loaded");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
|
||||||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
|
||||||
var random = new Random(seed);
|
|
||||||
|
|
||||||
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
|
|
||||||
var parentA = Genome.Generate(genes, random);
|
|
||||||
foreach (var gene in genes)
|
|
||||||
{
|
|
||||||
var allele = parentA[gene.DefName];
|
|
||||||
console.WriteLine(
|
|
||||||
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.WriteLine("traits:");
|
|
||||||
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
|
|
||||||
{
|
|
||||||
console.WriteLine($" {trait} = {value:0.###}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentB = Genome.Generate(genes, random);
|
|
||||||
var child = Genome.Breed(parentA, parentB, registry, random);
|
|
||||||
var childTraits = Phenotype.Compute(child, registry);
|
|
||||||
console.WriteLine(
|
|
||||||
$"bred child: {child.Alleles.Count} genes, "
|
|
||||||
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
|
|
||||||
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
|
|
||||||
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
|
|
||||||
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
|
|
||||||
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
|
|
||||||
{
|
|
||||||
var species = content.Defs.NamesOf("Plant");
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
|
|
||||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
|
|
||||||
{
|
|
||||||
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = _plants.IndexOf(def);
|
|
||||||
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
|
||||||
var genome = _plants[index].Template.Generate(new Random(seed));
|
|
||||||
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
|
|
||||||
|
|
||||||
console.WriteLine(
|
|
||||||
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
|
|
||||||
);
|
|
||||||
console.WriteLine(
|
|
||||||
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
|
||||||
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
|
||||||
);
|
|
||||||
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
|
||||||
console.WriteLine(
|
|
||||||
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
|
|
||||||
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (def.HarvestProduct is { } harvest)
|
|
||||||
{
|
|
||||||
console.WriteLine(
|
|
||||||
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
|
||||||
{
|
|
||||||
var season = content.Languages.Get(
|
|
||||||
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
|
|
||||||
);
|
|
||||||
console.WriteLine(
|
|
||||||
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.WriteLine(" barren (no fruit)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ProductLabel(GameContent content, string productDefName) =>
|
|
||||||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
|
||||||
? content.Languages.Get(product.Label)
|
|
||||||
: productDefName;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Идентификаторы способностей (capacities) организма — на них ссылаются вклады частей тела.</summary>
|
||||||
|
public static class AnimalCapacities
|
||||||
|
{
|
||||||
|
public const string Consciousness = "Consciousness";
|
||||||
|
public const string Moving = "Moving";
|
||||||
|
public const string Breathing = "Breathing";
|
||||||
|
public const string BloodPumping = "BloodPumping";
|
||||||
|
public const string BloodFiltration = "BloodFiltration";
|
||||||
|
public const string Sight = "Sight";
|
||||||
|
public const string Hearing = "Hearing";
|
||||||
|
public const string Smell = "Smell";
|
||||||
|
public const string Touch = "Touch";
|
||||||
|
public const string Talking = "Talking";
|
||||||
|
public const string Eating = "Eating";
|
||||||
|
public const string Digestion = "Digestion";
|
||||||
|
|
||||||
|
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
|
||||||
|
public static readonly string[] DisplayOrder =
|
||||||
|
[
|
||||||
|
Consciousness,
|
||||||
|
Moving,
|
||||||
|
Sight,
|
||||||
|
Hearing,
|
||||||
|
Smell,
|
||||||
|
Touch,
|
||||||
|
Talking,
|
||||||
|
Eating,
|
||||||
|
Breathing,
|
||||||
|
BloodPumping,
|
||||||
|
BloodFiltration,
|
||||||
|
Digestion,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Scenes;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Единая точка создания сущности-животного: спрайт вида, признаки из генома формулами генов, тинт по
|
||||||
|
/// оттенку меха и компонент-организм. Переиспользуется начальным спавном, размножением и загрузкой —
|
||||||
|
/// чтобы все животные собирались одинаково. Зеркало <see cref="PlantFactory"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class AnimalFactory
|
||||||
|
{
|
||||||
|
// Якоря оттенка меха: тёплый рыжевато-песочный → холодный серо-голубой (по гену furColor).
|
||||||
|
private static readonly Color FurWarm = new(255, 226, 188);
|
||||||
|
private static readonly Color FurCool = new(206, 222, 235);
|
||||||
|
|
||||||
|
/// <summary>Создаёт животное вида <paramref name="species"/> заданного возраста, генома и поколения.</summary>
|
||||||
|
public static Entity Create(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
NeedSet needs,
|
||||||
|
SkillSet skills,
|
||||||
|
int species,
|
||||||
|
Vector2 position,
|
||||||
|
float ageDays,
|
||||||
|
Genome genome,
|
||||||
|
int generation,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var sp = animals[species];
|
||||||
|
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry));
|
||||||
|
var isMale = IsMaleGenome(genome);
|
||||||
|
var (eatsPlants, eatsMeat) = Diet(traits, sp.Def); // диета фиксируется при рождении (см. EatsPlants/EatsMeat)
|
||||||
|
var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan, sp.Stages);
|
||||||
|
var region = RegionFor(sp, stage, isMale);
|
||||||
|
|
||||||
|
var sprite = new Sprite(region, GameLayers.Beings);
|
||||||
|
sprite.CenterOrigin();
|
||||||
|
sprite.Color = FurTint(traits);
|
||||||
|
|
||||||
|
var entity = store.CreateEntity(
|
||||||
|
new Transform2D(
|
||||||
|
position,
|
||||||
|
scale: new Vector2(Scale(sp, traits, stage, region, cellSize))
|
||||||
|
),
|
||||||
|
sprite,
|
||||||
|
new AnimalOrganism
|
||||||
|
{
|
||||||
|
Genome = genome,
|
||||||
|
Traits = traits,
|
||||||
|
Species = species,
|
||||||
|
IsMale = isMale,
|
||||||
|
EatsPlants = eatsPlants,
|
||||||
|
EatsMeat = eatsMeat,
|
||||||
|
Generation = generation,
|
||||||
|
},
|
||||||
|
new AnimalGrowth { Stage = stage, AgeDays = ageDays },
|
||||||
|
// Нужды — динамический массив по NeedSet (deplete полны, drive пусты); «мозг» решает сразу.
|
||||||
|
new AnimalNeeds { Values = needs.NewValues() },
|
||||||
|
new AnimalBrain
|
||||||
|
{
|
||||||
|
Action = AnimalActions.Wander,
|
||||||
|
TargetPlant = -1,
|
||||||
|
TargetMate = -1,
|
||||||
|
},
|
||||||
|
new Health { State = HealthState.Create(sp.Body, traits.BodySize) },
|
||||||
|
// Настроение (фаза A8): нейтральное на старте, активность по тиру мозга считает MoodSystem.
|
||||||
|
new Mood
|
||||||
|
{
|
||||||
|
Value = 0.5f,
|
||||||
|
Active = false,
|
||||||
|
Thoughts = [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Навыки: стартовые уровни + страсть (детерминированно по геному — воспроизводимо без RNG-параметра).
|
||||||
|
entity.AddComponent(
|
||||||
|
new Skills
|
||||||
|
{
|
||||||
|
Levels = skills.NewLevels(),
|
||||||
|
Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Память: объём ∝ размеру мозга (примитивные ~0 — ничего не помнят).
|
||||||
|
entity.AddComponent(
|
||||||
|
new Memory
|
||||||
|
{
|
||||||
|
Entries = [],
|
||||||
|
Capacity = (int)MathF.Round(MathF.Max(0f, traits.BrainSize) * MemoryPerBrain),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Личная хроника (UI «История»): кольцевой список последних событий особи.
|
||||||
|
entity.AddComponent(new CreatureLog { Entries = [], Capacity = CreatureLogCapacity });
|
||||||
|
// Вспышка урона (презентация): заранее на нуле, чтобы удар лишь выставлял время (без структурных правок).
|
||||||
|
entity.AddComponent(new DamageFlash());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сколько записей памяти даёт единица размера мозга (объём памяти = brainSize × это).</summary>
|
||||||
|
public const float MemoryPerBrain = 16f;
|
||||||
|
|
||||||
|
/// <summary>Глубина личной хроники особи (последние N событий о ней).</summary>
|
||||||
|
public const int CreatureLogCapacity = 24;
|
||||||
|
|
||||||
|
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
|
||||||
|
private static int SkillSeed(Genome genome)
|
||||||
|
{
|
||||||
|
var acc = 0;
|
||||||
|
foreach (var (_, allele) in genome.Alleles) // IReadOnlyDictionary без копии — без аллокации
|
||||||
|
{
|
||||||
|
acc += (int)(allele.A * 7919f) + (int)(allele.B * 104729f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
|
||||||
|
private static readonly Color CorpseFreshTint = new(176, 148, 128);
|
||||||
|
|
||||||
|
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и
|
||||||
|
/// запас мяса <paramref name="meatKg"/> (∝ масса тела, считается вызывающим через <see cref="Mass"/>).</summary>
|
||||||
|
public static Entity CreateCorpse(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
int species,
|
||||||
|
Vector2 position,
|
||||||
|
float bodySize,
|
||||||
|
float meatKg,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var sp = animals[species];
|
||||||
|
var region = sp.Corpse;
|
||||||
|
var sprite = new Sprite(region, GameLayers.Beings);
|
||||||
|
sprite.CenterOrigin();
|
||||||
|
sprite.Color = CorpseFreshTint;
|
||||||
|
var sizeCells = bodySize > 0f ? bodySize : sp.Def.SizeCells;
|
||||||
|
return store.CreateEntity(
|
||||||
|
new Transform2D(position, scale: new Vector2(cellSize * sizeCells / region.Width)),
|
||||||
|
sprite,
|
||||||
|
new Corpse
|
||||||
|
{
|
||||||
|
Species = species,
|
||||||
|
RotDays = 0f,
|
||||||
|
Stage = 0,
|
||||||
|
Meat = meatKg,
|
||||||
|
BodySize = bodySize,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца.
|
||||||
|
private static readonly Color EggTint = new(238, 230, 208);
|
||||||
|
private const float EggSizeFraction = 0.45f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент <see cref="Egg"/> с уже
|
||||||
|
/// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в <see cref="EggSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static Entity CreateEgg(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
int species,
|
||||||
|
Vector2 position,
|
||||||
|
Genome genome,
|
||||||
|
int generation,
|
||||||
|
float incubateDays,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var sp = animals[species];
|
||||||
|
var region = sp.Egg;
|
||||||
|
var sprite = new Sprite(region, GameLayers.Beings);
|
||||||
|
sprite.CenterOrigin();
|
||||||
|
sprite.Color = EggTint;
|
||||||
|
return store.CreateEntity(
|
||||||
|
new Transform2D(
|
||||||
|
position,
|
||||||
|
scale: new Vector2(cellSize * EggSizeFraction / region.Width)
|
||||||
|
),
|
||||||
|
sprite,
|
||||||
|
new Egg
|
||||||
|
{
|
||||||
|
Species = species,
|
||||||
|
Genome = genome,
|
||||||
|
Generation = generation,
|
||||||
|
IncubateDays = incubateDays,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Порог выраженности признака диеты, с которого вид реально ест данный корм.</summary>
|
||||||
|
public const float DietThreshold = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рацион особи из ГЕНОВ: ест растения, если max(herbivory, omnivory) ≥ порога; ест мясо
|
||||||
|
/// (охота/падаль), если max(carnivory, omnivory) ≥ порога — всеядность (omnivory) включает оба
|
||||||
|
/// источника, специалисты задаются высоким herbivory ЛИБО carnivory. Если у вида нет генов диеты
|
||||||
|
/// (все три ≈ 0) — откат на список <see cref="AnimalDef.Diet"/> (легаси/модерский fallback).
|
||||||
|
/// </summary>
|
||||||
|
public static (bool EatsPlants, bool EatsMeat) Diet(in AnimalPhenotype traits, AnimalDef def)
|
||||||
|
{
|
||||||
|
if (traits.Herbivory <= 0f && traits.Carnivory <= 0f && traits.Omnivory <= 0f)
|
||||||
|
{
|
||||||
|
return (DietContains(def, "plant"), DietContains(def, "meat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var eatsPlants = MathF.Max(traits.Herbivory, traits.Omnivory) >= DietThreshold;
|
||||||
|
var eatsMeat = MathF.Max(traits.Carnivory, traits.Omnivory) >= DietThreshold;
|
||||||
|
return (eatsPlants, eatsMeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DietContains(AnimalDef def, string food)
|
||||||
|
{
|
||||||
|
foreach (var f in def.Diet)
|
||||||
|
{
|
||||||
|
if (string.Equals(f, food, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти.</summary>
|
||||||
|
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
|
||||||
|
{
|
||||||
|
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
|
||||||
|
var position = entity.GetComponent<Transform2D>().Position;
|
||||||
|
var sp = animals[org.Species];
|
||||||
|
var stageScale = entity.TryGetComponent<AnimalGrowth>(out var grow)
|
||||||
|
? sp.Stages[Math.Clamp(grow.Stage, 0, sp.Stages.Length - 1)].Scale
|
||||||
|
: 1f;
|
||||||
|
var meat = Mass.OfAnimal(sp.Def, org.Traits, stageScale) * Mass.MeatFraction;
|
||||||
|
CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, meat, cellSize);
|
||||||
|
entity.DeleteEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Эффективный интеллект особи (фаза A7): размер мозга (ген) × сознание (capacity из здоровья).
|
||||||
|
/// Повреждение мозга/болезнь/боль/кровопотеря снижают Consciousness → динамически роняют интеллект,
|
||||||
|
/// что гейтит набор доступных нужд/действий (см. <see cref="AnimalContext"/>). Вид без частей тела
|
||||||
|
/// (сознание не вычисляется) считается полностью сознательным — чтобы не отключить ему всё.
|
||||||
|
/// </summary>
|
||||||
|
public static float Intelligence(in AnimalPhenotype traits, HealthState? health)
|
||||||
|
{
|
||||||
|
var consciousness = health is { Parts.Length: > 0 }
|
||||||
|
? health.Capacity(AnimalCapacities.Consciousness)
|
||||||
|
: 1f;
|
||||||
|
return traits.BrainSize * consciousness;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).</summary>
|
||||||
|
public static bool IsMaleGenome(Genome genome) =>
|
||||||
|
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
|
||||||
|
|
||||||
|
/// <summary>Индекс стадии по возрасту: последняя стадия, чей возраст входа (EnterAt × якорь) ≤ возраст.</summary>
|
||||||
|
public static int StageAt(
|
||||||
|
float ageDays,
|
||||||
|
float maturity,
|
||||||
|
float lifespan,
|
||||||
|
AnimalStageDef[] stages
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var stage = 0;
|
||||||
|
for (var i = 1; i < stages.Length; i++)
|
||||||
|
{
|
||||||
|
var anchor = string.Equals(stages[i].RelativeTo, "lifespan", StringComparison.Ordinal)
|
||||||
|
? lifespan
|
||||||
|
: maturity;
|
||||||
|
if (ageDays >= stages[i].EnterAt * anchor)
|
||||||
|
{
|
||||||
|
stage = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка).</summary>
|
||||||
|
public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) =>
|
||||||
|
sp.LookDirs(stage, isMale).East; // восток — базовое направление; разворот делает AnimalFacingSystem
|
||||||
|
|
||||||
|
/// <summary>Взрослая ли стадия (способна размножаться) — флаг из данных стадии.</summary>
|
||||||
|
public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult;
|
||||||
|
|
||||||
|
/// <summary>Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона).</summary>
|
||||||
|
public static float Scale(
|
||||||
|
AnimalSet.Species sp,
|
||||||
|
in AnimalPhenotype traits,
|
||||||
|
int stage,
|
||||||
|
Texture2DRegion region,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var sizeCells =
|
||||||
|
(traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells) * sp.Stages[stage].Scale;
|
||||||
|
return cellSize * sizeCells / region.Width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры —
|
||||||
|
/// субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений).
|
||||||
|
/// Публичен, чтобы <see cref="AnimalAppearanceSystem"/> домножал его на яркость по нуждам.
|
||||||
|
/// </summary>
|
||||||
|
public static Color FurTint(in AnimalPhenotype traits)
|
||||||
|
{
|
||||||
|
var fur = Color.Lerp(FurWarm, FurCool, Math.Clamp(traits.FurHue, 0f, 1f));
|
||||||
|
return Color.Lerp(Color.White, fur, 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,720 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Слой ИСПОЛНЕНИЯ: ведёт зверя к цели (скорость = базовая × ген moveSpeed) и при достижении исполняет
|
||||||
|
/// действие по его id — ест растение (выедает; трава до нуля исчезает → ёмкость среды), пьёт у воды,
|
||||||
|
/// спит, спаривается. Восполняемую нужду находит по действию через <see cref="NeedSet"/>. Структурные
|
||||||
|
/// изменения (гибель растений, беременность) применяются после прохода.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalActionSystem : BaseSystem
|
||||||
|
{
|
||||||
|
private const float GrazeKillAge = 1f; // трава с возрастом ниже этого после выедания исчезает
|
||||||
|
private const float ThirstReliefBelow = 0.15f; // ниже этого жажда — питьё даёт мысль «напилась»
|
||||||
|
private const float FleeSpeedMult = 1.2f; // страх подгоняет жертву чуть быстрее обычного шага
|
||||||
|
private const float PostReproductiveFrac = 0.9f; // доля жизни, после которой самка уже не зачинает
|
||||||
|
private const float SeedIngestChance = 0.15f; // шанс проглотить семя при выедании зрелого растения
|
||||||
|
private const float GutSeedDropDays = 2f; // через сколько дней проглоченное семя высаживается
|
||||||
|
private const float IdleRestFactor = 0.3f; // доля скорости восстановления сил при холостом отдыхе
|
||||||
|
private const float DigForageFactor = 0.25f; // доля сытости с копания относительно нормальной кормёжки
|
||||||
|
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly AnimalSet _animalSet;
|
||||||
|
private readonly NeedSet _needs;
|
||||||
|
private readonly SkillSet _skills;
|
||||||
|
private readonly ThoughtSet _thoughts;
|
||||||
|
private readonly WorldEvents _events;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly RectF _bounds;
|
||||||
|
private readonly HediffDef? _bleeding;
|
||||||
|
private readonly HediffDef? _poisoned;
|
||||||
|
private readonly Random _rng;
|
||||||
|
|
||||||
|
// Кэш индексов навыков-эффектов (−1, если навык не определён модом — тогда эффекта/XP нет).
|
||||||
|
private readonly int _forageSkill;
|
||||||
|
private readonly int _huntSkill;
|
||||||
|
private readonly int _evadeSkill;
|
||||||
|
|
||||||
|
private readonly ArchetypeQuery<
|
||||||
|
AnimalBrain,
|
||||||
|
AnimalNeeds,
|
||||||
|
AnimalOrganism,
|
||||||
|
Transform2D,
|
||||||
|
Health
|
||||||
|
> _query;
|
||||||
|
private readonly List<Entity> _eaten = [];
|
||||||
|
private readonly HashSet<Entity> _eatenSet = []; // O(1)-проверка членства (порядок даёт _eaten)
|
||||||
|
private readonly List<Entity> _consumed = []; // трупы, доеденные до нуля
|
||||||
|
private readonly HashSet<Entity> _consumedSet = []; // O(1)-проверка членства (порядок даёт _consumed)
|
||||||
|
private readonly List<Entity> _kills = []; // добыча, забитая в этом проходе (смерть после итерации)
|
||||||
|
private readonly HashSet<Entity> _killsSet = []; // O(1)-проверка членства (порядок даёт _kills)
|
||||||
|
private readonly List<(Entity Self, Entity Partner)> _matings = [];
|
||||||
|
private readonly HashSet<(int, int)> _matedPairs = []; // обработанные пары за проход (дедуп (a,b)/(b,a))
|
||||||
|
private readonly List<(Entity Eater, GutSeed Seed)> _ingested = []; // проглоченные семена (эндозоохория)
|
||||||
|
|
||||||
|
public AnimalActionSystem(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
AnimalSet animals,
|
||||||
|
NeedSet needs,
|
||||||
|
SkillSet skills,
|
||||||
|
ThoughtSet thoughts,
|
||||||
|
WorldEvents events,
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
int cellSize,
|
||||||
|
RectF bounds,
|
||||||
|
HediffDef? bleeding,
|
||||||
|
HediffDef? poisoned,
|
||||||
|
int seed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_plants = plants;
|
||||||
|
_animalSet = animals;
|
||||||
|
_needs = needs;
|
||||||
|
_skills = skills;
|
||||||
|
_forageSkill = skills.IndexOf(AnimalSkills.Foraging);
|
||||||
|
_huntSkill = skills.IndexOf(AnimalSkills.Hunting);
|
||||||
|
_evadeSkill = skills.IndexOf(AnimalSkills.Evasion);
|
||||||
|
_thoughts = thoughts;
|
||||||
|
_events = events;
|
||||||
|
_clock = clock;
|
||||||
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_bounds = bounds;
|
||||||
|
_bleeding = bleeding;
|
||||||
|
_poisoned = poisoned;
|
||||||
|
_rng = new Random(seed);
|
||||||
|
_query = store.Query<AnimalBrain, AnimalNeeds, AnimalOrganism, Transform2D, Health>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
var seconds = _clock.DeltaTime;
|
||||||
|
if (seconds <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var days = seconds / _secondsPerDay;
|
||||||
|
_eaten.Clear();
|
||||||
|
_eatenSet.Clear();
|
||||||
|
_consumed.Clear();
|
||||||
|
_consumedSet.Clear();
|
||||||
|
_kills.Clear();
|
||||||
|
_killsSet.Clear();
|
||||||
|
_matings.Clear();
|
||||||
|
_ingested.Clear();
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
var (brains, needsChunk, organisms, transforms, healths, entities) in _query.Chunks
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var b = brains.Span;
|
||||||
|
var n = needsChunk.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
var hh = healths.Span;
|
||||||
|
for (var i = 0; i < b.Length; i++)
|
||||||
|
{
|
||||||
|
ref var brain = ref b[i];
|
||||||
|
var values = n[i].Values;
|
||||||
|
ref var pos = ref t[i].Position;
|
||||||
|
var def = _animalSet[o[i].Species].Def;
|
||||||
|
// Способность Moving (падает от ран/болезней) — множитель к скорости: больные медленнее.
|
||||||
|
var moving = hh[i].State.Capacity(AnimalCapacities.Moving);
|
||||||
|
var speed = def.BaseSpeed * MathF.Max(0.2f, o[i].Traits.MoveSpeed) * moving;
|
||||||
|
// Питание (челюсть) масштабирует укус, пищеварение (желудок) — усвоение: их произведение
|
||||||
|
// даёт сытость с корма; раненая челюсть/больной желудок → зверь голоднее (опция 1).
|
||||||
|
var eating = hh[i].State.Capacity(AnimalCapacities.Eating);
|
||||||
|
var foodEff = eating * hh[i].State.Capacity(AnimalCapacities.Digestion);
|
||||||
|
var self = entities.EntityAt(i);
|
||||||
|
self.TryGetComponent<Skills>(out var skills); // один лукап на особь (эффекты + XP)
|
||||||
|
// Ноша замедляет (задел под перенос): перегруз сверх грузоподъёмности роняет скорость.
|
||||||
|
if (self.TryGetComponent<Carrier>(out var carrier) && carrier.CarriedKg > 0f)
|
||||||
|
{
|
||||||
|
var sp = _animalSet[o[i].Species];
|
||||||
|
var stageScale = entities.EntityAt(i).TryGetComponent<AnimalGrowth>(out var gr)
|
||||||
|
? sp.Stages[Math.Clamp(gr.Stage, 0, sp.Stages.Length - 1)].Scale
|
||||||
|
: 1f;
|
||||||
|
var capacity = Mass.CarryCapacity(
|
||||||
|
Mass.OfAnimal(sp.Def, o[i].Traits, stageScale),
|
||||||
|
moving
|
||||||
|
);
|
||||||
|
speed *= Mass.LoadSpeedFactor(carrier.CarriedKg, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (brain.Action)
|
||||||
|
{
|
||||||
|
case AnimalActions.Sleep:
|
||||||
|
Refill(values, AnimalActions.Sleep, days);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Eat:
|
||||||
|
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
||||||
|
{
|
||||||
|
// Навык добычи корма: опытный форажир извлекает больше сытости; растёт от практики.
|
||||||
|
var forageF = SkillFactor(_forageSkill, skills);
|
||||||
|
Refill(values, AnimalActions.Eat, days, foodEff * forageF);
|
||||||
|
Graze(
|
||||||
|
brain.TargetPlant,
|
||||||
|
days,
|
||||||
|
def.ForageBiteDays * MathF.Max(0.2f, eating)
|
||||||
|
);
|
||||||
|
// Коэволюция: защита растения бьёт по поедателю (яд/шипы).
|
||||||
|
ApplyPlantDefense(
|
||||||
|
brain.TargetPlant,
|
||||||
|
hh[i].State,
|
||||||
|
o[i].Traits.ToxinTolerance,
|
||||||
|
days
|
||||||
|
);
|
||||||
|
// Эндозоохория: шанс проглотить семя зрелого растения (высадит позже).
|
||||||
|
TryIngestSeed(brain.TargetPlant, self);
|
||||||
|
GrantXp(_forageSkill, skills, days);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Drink:
|
||||||
|
if (MoveTo(ref pos, brain.Target, speed * seconds))
|
||||||
|
{
|
||||||
|
// Утоление сильной жажды — событийная мысль настроения (A8); уровень берём
|
||||||
|
// ДО восполнения, чтобы радость была от облегчения, а не от каждого глотка.
|
||||||
|
var thirst = _needs.NeedForAction(AnimalActions.Drink);
|
||||||
|
var parched =
|
||||||
|
thirst >= 0
|
||||||
|
&& thirst < values.Length
|
||||||
|
&& values[thirst] < ThirstReliefBelow;
|
||||||
|
Refill(values, AnimalActions.Drink, days);
|
||||||
|
if (parched)
|
||||||
|
{
|
||||||
|
entities
|
||||||
|
.EntityAt(i)
|
||||||
|
.GetComponent<Mood>()
|
||||||
|
.Add(_thoughts.Get(AnimalThoughts.QuenchedThirst));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Mate:
|
||||||
|
if (
|
||||||
|
brain.TargetMate >= 0
|
||||||
|
&& _store.TryGetEntityById(brain.TargetMate, out var partner)
|
||||||
|
&& !partner.IsNull
|
||||||
|
&& partner.HasComponent<Transform2D>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var partnerPos = partner.GetComponent<Transform2D>().Position;
|
||||||
|
if (MoveTo(ref pos, partnerPos, speed * seconds))
|
||||||
|
{
|
||||||
|
_matings.Add((entities.EntityAt(i), partner));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Hunt:
|
||||||
|
{
|
||||||
|
// Сила удара: размер тела особи (со стадией) относительно эталона вида — крупная/
|
||||||
|
// взрослая особь бьёт сильнее, детёныш слабее. Дальше домножается на навык охоты.
|
||||||
|
var spH = _animalSet[o[i].Species];
|
||||||
|
var stageScaleH = self.TryGetComponent<AnimalGrowth>(out var grH)
|
||||||
|
? spH.Stages[Math.Clamp(grH.Stage, 0, spH.Stages.Length - 1)].Scale
|
||||||
|
: 1f;
|
||||||
|
var strength = MathF.Max(
|
||||||
|
0.2f,
|
||||||
|
o[i].Traits.BodySize * stageScaleH / spH.RefBodySize
|
||||||
|
);
|
||||||
|
// Навык охоты: опытный хищник бьёт сильнее; растёт от практики.
|
||||||
|
Hunt(
|
||||||
|
ref brain,
|
||||||
|
ref pos,
|
||||||
|
values,
|
||||||
|
def,
|
||||||
|
speed * seconds,
|
||||||
|
seconds,
|
||||||
|
days,
|
||||||
|
foodEff,
|
||||||
|
SkillFactor(_huntSkill, skills),
|
||||||
|
strength,
|
||||||
|
self,
|
||||||
|
o[i].Species
|
||||||
|
);
|
||||||
|
GrantXp(_huntSkill, skills, days);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case AnimalActions.Flee:
|
||||||
|
// Навык уклонения: опытная жертва убегает быстрее; растёт от практики бегства.
|
||||||
|
MoveTo(
|
||||||
|
ref pos,
|
||||||
|
brain.Target,
|
||||||
|
speed * FleeSpeedMult * SkillFactor(_evadeSkill, skills) * seconds
|
||||||
|
);
|
||||||
|
GrantXp(_evadeSkill, skills, days);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Rest:
|
||||||
|
// Отдых на месте: понемногу восстанавливает силы (меньше, чем полноценный сон).
|
||||||
|
Refill(values, AnimalActions.Sleep, days, IdleRestFactor);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Dig:
|
||||||
|
// Копание в земле: травоядные/всеядные выкапывают корешки/личинок — немного сытости.
|
||||||
|
if (o[i].EatsPlants)
|
||||||
|
{
|
||||||
|
Refill(values, AnimalActions.Eat, days, DigForageFactor * foodEff);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AnimalActions.Wallow:
|
||||||
|
// Валяние в грязи: стоит на месте у воды (комфорт/терморегуляция) — без эффекта на нужды.
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // Wander — блуждание к цели
|
||||||
|
MoveTo(ref pos, brain.Target, speed * seconds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.X = Math.Clamp(pos.X, _bounds.Left, _bounds.Right);
|
||||||
|
pos.Y = Math.Clamp(pos.Y, _bounds.Top, _bounds.Bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var plant in _eaten)
|
||||||
|
{
|
||||||
|
plant.DeleteEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var corpse in _consumed)
|
||||||
|
{
|
||||||
|
if (!corpse.IsNull)
|
||||||
|
{
|
||||||
|
corpse.DeleteEntity(); // труп доеден до нуля
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var prey in _kills)
|
||||||
|
{
|
||||||
|
AnimalFactory.Die(_store, _animalSet, prey, _cellSize); // забитая добыча → труп
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (eater, seed) in _ingested)
|
||||||
|
{
|
||||||
|
if (!eater.IsNull && !eater.HasComponent<GutSeed>())
|
||||||
|
{
|
||||||
|
eater.AddComponent(seed); // структурное добавление — после прохода
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyMatings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эндозоохория: при выедании ЗРЕЛОГО растения шанс проглотить его семя — зверь понесёт его и
|
||||||
|
// высадит позже в другом месте (SeedDispersalSystem). Геном клонируется (отдельная особь-росток).
|
||||||
|
private void TryIngestSeed(int plantId, Entity eater)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
eater.IsNull
|
||||||
|
|| eater.HasComponent<GutSeed>()
|
||||||
|
|| plantId < 0
|
||||||
|
|| _rng.NextSingle() >= SeedIngestChance
|
||||||
|
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||||
|
|| plant.IsNull
|
||||||
|
|| !plant.HasComponent<PlantGrowth>()
|
||||||
|
|| !plant.HasComponent<PlantOrganism>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref readonly var grow = ref plant.GetComponent<PlantGrowth>();
|
||||||
|
if (grow.Stage < _plants[grow.Species].Stages.Length - 1)
|
||||||
|
{
|
||||||
|
return; // только зрелое растение даёт семя
|
||||||
|
}
|
||||||
|
|
||||||
|
var genome = new Genome(plant.GetComponent<PlantOrganism>().Genome.ToDictionary());
|
||||||
|
_ingested.Add(
|
||||||
|
(
|
||||||
|
eater,
|
||||||
|
new GutSeed
|
||||||
|
{
|
||||||
|
PlantSpecies = grow.Species,
|
||||||
|
Genome = genome,
|
||||||
|
DropInDays = GutSeedDropDays,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Охота/падальщество: ведёт к цели и при контакте либо ест труп (утоляет голод, расходует мясо),
|
||||||
|
// либо кусает живую добычу — урон части тела, острая кровопотеря, рана-кровотечение (первый
|
||||||
|
// травматический урон). Добитая добыча помечается на смерть (труп оставит общий путь Die).
|
||||||
|
// Минимальная видимая длительность красной вспышки урона у жертвы (реального времени).
|
||||||
|
private const float DamageFlashSeconds = 0.35f;
|
||||||
|
|
||||||
|
private void Hunt(
|
||||||
|
ref AnimalBrain brain,
|
||||||
|
ref Vector2 pos,
|
||||||
|
float[] values,
|
||||||
|
AnimalDef def,
|
||||||
|
float step,
|
||||||
|
float seconds,
|
||||||
|
float days,
|
||||||
|
float foodEff,
|
||||||
|
float huntFactor,
|
||||||
|
float strength,
|
||||||
|
Entity hunter,
|
||||||
|
int hunterSpecies
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
brain.TargetPrey < 0
|
||||||
|
|| !_store.TryGetEntityById(brain.TargetPrey, out var target)
|
||||||
|
|| target.IsNull
|
||||||
|
|| !target.HasComponent<Transform2D>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return; // цель исчезла — действие пересмотрят на следующем решении
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetPos = target.GetComponent<Transform2D>().Position;
|
||||||
|
var range = def.AttackRangeCells * _cellSize;
|
||||||
|
var inRange = Vector2.DistanceSquared(pos, targetPos) <= range * range;
|
||||||
|
|
||||||
|
if (brain.TargetIsCorpse)
|
||||||
|
{
|
||||||
|
if (!target.HasComponent<Corpse>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inRange)
|
||||||
|
{
|
||||||
|
MoveTo(ref pos, targetPos, step);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var corpse = ref target.GetComponent<Corpse>();
|
||||||
|
Refill(values, AnimalActions.Eat, days, foodEff);
|
||||||
|
corpse.Meat -= def.ForageBiteDays * days; // расход мяса той же «силой укуса», что и выедание
|
||||||
|
if (corpse.Meat <= 0f && _consumedSet.Add(target))
|
||||||
|
{
|
||||||
|
_consumed.Add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Живая добыча.
|
||||||
|
if (!target.HasComponent<Health>() || !target.HasComponent<AnimalOrganism>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дискретные удары (как в RimWorld): кулдаун идёт и при сближении — первый контакт бьёт сразу.
|
||||||
|
brain.AttackCooldown -= seconds;
|
||||||
|
if (!inRange)
|
||||||
|
{
|
||||||
|
MoveTo(ref pos, targetPos, step);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brain.AttackCooldown > 0f)
|
||||||
|
{
|
||||||
|
return; // в контакте, но удар ещё на перезарядке
|
||||||
|
}
|
||||||
|
|
||||||
|
brain.AttackCooldown = MathF.Max(0.05f, def.AttackIntervalSec);
|
||||||
|
|
||||||
|
var preyHealth = target.GetComponent<Health>().State;
|
||||||
|
if (preyHealth is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сила удара = база дефа × сила особи (размер×стадия) × навык охоты; кровопотеря/кровотечение — ×силу.
|
||||||
|
var damage = def.AttackDamage * strength * huntFactor;
|
||||||
|
preyHealth.ApplyInjury(
|
||||||
|
damage,
|
||||||
|
def.AttackBloodLoss * strength,
|
||||||
|
_bleeding,
|
||||||
|
def.AttackBleed * strength,
|
||||||
|
_rng
|
||||||
|
);
|
||||||
|
|
||||||
|
// Красная вспышка жертвы (презентация) + удар в боевую хронику обоих участников.
|
||||||
|
if (target.HasComponent<DamageFlash>())
|
||||||
|
{
|
||||||
|
target.GetComponent<DamageFlash>().Seconds = DamageFlashSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
_events.Struck(
|
||||||
|
hunter,
|
||||||
|
hunterSpecies,
|
||||||
|
target,
|
||||||
|
target.GetComponent<AnimalOrganism>().Species,
|
||||||
|
damage
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
preyHealth.BloodLevel <= 0f
|
||||||
|
|| preyHealth.Capacity(AnimalCapacities.Consciousness) <= 0.01f
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_killsSet.Add(target))
|
||||||
|
{
|
||||||
|
_kills.Add(target); // добита — смерть после прохода (структурное изменение)
|
||||||
|
_events.Killed(
|
||||||
|
hunter,
|
||||||
|
hunterSpecies,
|
||||||
|
target.GetComponent<AnimalOrganism>().Species
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Восполняет нужду, которую утоляет действие (по NeedSet), на её FeedPerDay × эффективность
|
||||||
|
// (питание/пищеварение для еды — слабая челюсть/больной желудок дают меньше сытости с того же корма).
|
||||||
|
private void Refill(float[] values, string action, float days, float efficiency = 1f)
|
||||||
|
{
|
||||||
|
var index = _needs.NeedForAction(action);
|
||||||
|
if (index < 0 || values is null || index >= values.Length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[index] = Math.Clamp(
|
||||||
|
values[index] + _needs[index].FeedPerDay * days * efficiency,
|
||||||
|
0f,
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Бонус-множитель от уровня навыка: 0.7 (новичок) … 1.3 (мастер). Навык не определён модом → 1 (без эффекта).
|
||||||
|
private static float SkillFactor(int idx, in Skills s)
|
||||||
|
{
|
||||||
|
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
|
||||||
|
{
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.7f + 0.6f * s.Levels[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Начисляет опыт навыку от практики: рост ∝ скорость обучения × страсть × (1−уровень) (убывающая отдача).
|
||||||
|
private void GrantXp(int idx, in Skills s, float days)
|
||||||
|
{
|
||||||
|
if (idx < 0 || s.Levels is null || idx >= s.Levels.Length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var passion = s.Passions is not null && idx < s.Passions.Length ? s.Passions[idx] : (byte)0;
|
||||||
|
var gain =
|
||||||
|
_skills[idx].LearnRate
|
||||||
|
* SkillSet.PassionMultiplier(passion)
|
||||||
|
* days
|
||||||
|
* (1f - s.Levels[idx]);
|
||||||
|
s.Levels[idx] = Math.Clamp(s.Levels[idx] + gain, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Двигает позицию к цели на step; возвращает true, если уже у цели (можно исполнять действие).
|
||||||
|
private static bool MoveTo(ref Vector2 pos, Vector2 target, float step)
|
||||||
|
{
|
||||||
|
var delta = target - pos;
|
||||||
|
var dist = delta.Length();
|
||||||
|
if (dist <= step || dist < 0.001f)
|
||||||
|
{
|
||||||
|
pos = dist < 0.001f ? pos : target;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos += delta / dist * step;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выедание: убавляет рост растения; траву (без ствола), выеденную до нуля, помечает на гибель.
|
||||||
|
private void Graze(int plantId, float days, float bitePerDay)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
plantId < 0
|
||||||
|
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||||
|
|| plant.IsNull
|
||||||
|
|| !plant.HasComponent<PlantGrowth>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var grow = ref plant.GetComponent<PlantGrowth>();
|
||||||
|
grow.AgeDays = MathF.Max(0f, grow.AgeDays - bitePerDay * days);
|
||||||
|
var isGrass = _plants[grow.Species].Def.TrunkRadiusCells <= 0f;
|
||||||
|
if (isGrass && grow.AgeDays <= GrazeKillAge && _eatenSet.Add(plant))
|
||||||
|
{
|
||||||
|
_eaten.Add(plant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Доза яда за день укуса при токсичности 1 и нулевой устойчивости (масштабируется тяжести Poisoned).
|
||||||
|
private const float PoisonDosePerDay = 1.6f;
|
||||||
|
|
||||||
|
// Порог шипов, выше которого укус о растение травмирует, и масштабы урона/кровопотери от шипов.
|
||||||
|
private const float ThornThreshold = 0.2f;
|
||||||
|
private const float ThornPartDamage = 4f;
|
||||||
|
private const float ThornBloodLoss = 0.02f;
|
||||||
|
|
||||||
|
// Коэволюция: при поедании растение «отвечает» — токсичность отравляет (тем сильнее, чем ниже
|
||||||
|
// устойчивость зверя к яду), шипы наносят лёгкую травму. Доза яда копится в хедифе Poisoned; если
|
||||||
|
// зверь ест яд быстрее, чем выводит, тяжесть дойдёт до летальной. Детерминированно (общий _rng).
|
||||||
|
private void ApplyPlantDefense(
|
||||||
|
int plantId,
|
||||||
|
HealthState? health,
|
||||||
|
float toxinTolerance,
|
||||||
|
float days
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
health is null
|
||||||
|
|| plantId < 0
|
||||||
|
|| !_store.TryGetEntityById(plantId, out var plant)
|
||||||
|
|| plant.IsNull
|
||||||
|
|| !plant.HasComponent<PlantOrganism>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref readonly var traits = ref plant.GetComponent<PlantOrganism>().Traits;
|
||||||
|
|
||||||
|
var poisoned = false;
|
||||||
|
if (_poisoned is not null && traits.Toxicity > 0f)
|
||||||
|
{
|
||||||
|
var dose = traits.Toxicity * (1f - Math.Clamp(toxinTolerance, 0f, 1f));
|
||||||
|
if (dose > 0f)
|
||||||
|
{
|
||||||
|
health.Intensify(_poisoned, dose * PoisonDosePerDay * days);
|
||||||
|
poisoned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (traits.Thorns > ThornThreshold)
|
||||||
|
{
|
||||||
|
// ApplyInjury сам пересчитывает способности (учтёт и свежий яд).
|
||||||
|
health.ApplyInjury(
|
||||||
|
traits.Thorns * ThornPartDamage * days,
|
||||||
|
traits.Thorns * ThornBloodLoss * days,
|
||||||
|
_bleeding,
|
||||||
|
traits.Thorns * 0.15f * days,
|
||||||
|
_rng
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (poisoned)
|
||||||
|
{
|
||||||
|
health.RecomputeCapacities(); // отравление меняет capacity-моды — применить сразу
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяет спаривания после прохода: самка пары беременеет геномом самца (структурное добавление —
|
||||||
|
// после итерации), влечение обоих обнуляется. Двойные события на одну самку отсеиваются.
|
||||||
|
private void ApplyMatings()
|
||||||
|
{
|
||||||
|
var mateNeed = _needs.NeedForAction(AnimalActions.Mate);
|
||||||
|
_matedPairs.Clear();
|
||||||
|
foreach (var (a, b) in _matings)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
a.IsNull
|
||||||
|
|| b.IsNull
|
||||||
|
|| !a.HasComponent<AnimalOrganism>()
|
||||||
|
|| !b.HasComponent<AnimalOrganism>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Оба партнёра независимо ставят себя в очередь, так что пара приходит дважды — (a,b) и (b,a).
|
||||||
|
// Дедупим по неупорядоченному ключу: иначе ResetMating/мысль «спаривание» применяются повторно.
|
||||||
|
var pairKey = a.Id < b.Id ? (a.Id, b.Id) : (b.Id, a.Id);
|
||||||
|
if (!_matedPairs.Add(pairKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aMale = a.GetComponent<AnimalOrganism>().IsMale;
|
||||||
|
var bMale = b.GetComponent<AnimalOrganism>().IsMale;
|
||||||
|
if (aMale == bMale)
|
||||||
|
{
|
||||||
|
continue; // один пол — не пара (страховка)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mother = aMale ? b : a;
|
||||||
|
var father = aMale ? a : b;
|
||||||
|
if (!mother.HasComponent<Pregnant>())
|
||||||
|
{
|
||||||
|
ref readonly var mom = ref mother.GetComponent<AnimalOrganism>();
|
||||||
|
// Плодовитость падает со старостью (тот же фактор сенесценса): дряхлая самка приносит
|
||||||
|
// меньший помёт, а после порога пострепродуктивного возраста уже не зачинает.
|
||||||
|
var momAge = mother.HasComponent<AnimalGrowth>()
|
||||||
|
? mother.GetComponent<AnimalGrowth>().AgeDays
|
||||||
|
: 0f;
|
||||||
|
var ageFrac = mom.Traits.Lifespan > 0f ? momAge / mom.Traits.Lifespan : 0f;
|
||||||
|
if (ageFrac <= PostReproductiveFrac)
|
||||||
|
{
|
||||||
|
var fert = AnimalHealthSystem.SenescenceFactor(momAge, mom.Traits.Lifespan);
|
||||||
|
mother.AddComponent(
|
||||||
|
new Pregnant
|
||||||
|
{
|
||||||
|
FatherGenome = father.GetComponent<AnimalOrganism>().Genome,
|
||||||
|
DueInDays = MathF.Max(1f, mom.Traits.GestationDays),
|
||||||
|
Litter = Math.Max(1, (int)MathF.Round(mom.Traits.LitterSize * fert)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
_events.Pregnant(mother, mom.Species);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetMating(a, mateNeed);
|
||||||
|
ResetMating(b, mateNeed);
|
||||||
|
// Спаривание — положительная событийная мысль обоим партнёрам (A8).
|
||||||
|
var mated = _thoughts.Get(AnimalThoughts.Mated);
|
||||||
|
a.GetComponent<Mood>().Add(mated);
|
||||||
|
b.GetComponent<Mood>().Add(mated);
|
||||||
|
_events.Mated(
|
||||||
|
a,
|
||||||
|
a.GetComponent<AnimalOrganism>().Species,
|
||||||
|
b,
|
||||||
|
b.GetComponent<AnimalOrganism>().Species
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResetMating(Entity entity, int mateNeed)
|
||||||
|
{
|
||||||
|
if (mateNeed < 0 || !entity.HasComponent<AnimalNeeds>())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = entity.GetComponent<AnimalNeeds>().Values;
|
||||||
|
if (values is not null && mateNeed < values.Length)
|
||||||
|
{
|
||||||
|
values[mateNeed] = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Id поведений-исполнителей (реестр действий ИИ). Дефы нужд (<see cref="NeedDef.Action"/>) ссылаются на
|
||||||
|
/// них; <see cref="AnimalDecisionSystem"/> выбирает действие, <see cref="AnimalActionSystem"/> исполняет.
|
||||||
|
/// Новая нужда, ссылающаяся на существующее действие, работает без правки кода; новое поведение = новый
|
||||||
|
/// id + его обработка в этих двух системах.
|
||||||
|
/// </summary>
|
||||||
|
public static class AnimalActions
|
||||||
|
{
|
||||||
|
public const string Wander = "Wander";
|
||||||
|
public const string Eat = "Eat";
|
||||||
|
public const string Drink = "Drink";
|
||||||
|
public const string Sleep = "Sleep";
|
||||||
|
public const string Mate = "Mate";
|
||||||
|
|
||||||
|
/// <summary>Охота: преследовать живую добычу и кусать (урон) либо есть труп — утоляет голод хищника.</summary>
|
||||||
|
public const string Hunt = "Hunt";
|
||||||
|
|
||||||
|
/// <summary>Бегство: уходить от ближайшего хищника (реакция жертвы, важнее прочих нужд).</summary>
|
||||||
|
public const string Flee = "Flee";
|
||||||
|
|
||||||
|
// — Холостые (idle) активности: чем зверь занят, когда нет срочных нужд. Выбираются внутри ветки
|
||||||
|
// Wander с фиксированной длительностью (см. AnimalDecisionSystem) — придают поведению разнообразие.
|
||||||
|
|
||||||
|
/// <summary>Отдых: стоит/лежит на месте, понемногу восстанавливая силы.</summary>
|
||||||
|
public const string Rest = "Rest";
|
||||||
|
|
||||||
|
/// <summary>Копание в земле в поисках корня/личинок (травоядные/всеядные) — даёт чуть сытости.</summary>
|
||||||
|
public const string Dig = "Dig";
|
||||||
|
|
||||||
|
/// <summary>Валяние в грязи у воды (комфорт/терморегуляция) — стоит на месте у кромки.</summary>
|
||||||
|
public const string Wallow = "Wallow";
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Презентация состояния нужд: голодный/уставший зверь тускнеет (яркость падает с самой острой
|
||||||
|
/// убывающей нуждой), сохраняя оттенок меха из генов. Читает симуляцию, пишет только
|
||||||
|
/// <see cref="Sprite"/> — граница sim/presentation цела.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalAppearanceSystem(NeedSet needs)
|
||||||
|
: QuerySystem<AnimalNeeds, AnimalOrganism, Sprite>
|
||||||
|
{
|
||||||
|
private const float MinBrightness = 0.5f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
foreach (var (needsChunk, organisms, sprites, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var n = needsChunk.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var s = sprites.Span;
|
||||||
|
for (var i = 0; i < n.Length; i++)
|
||||||
|
{
|
||||||
|
var values = n[i].Values;
|
||||||
|
var worst = 1f;
|
||||||
|
if (values is not null)
|
||||||
|
{
|
||||||
|
for (var k = 0; k < needs.Count && k < values.Length; k++)
|
||||||
|
{
|
||||||
|
if (needs.IsDeplete(k))
|
||||||
|
{
|
||||||
|
worst = MathF.Min(worst, values[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var brightness = MinBrightness + (1f - MinBrightness) * Math.Clamp(worst, 0f, 1f);
|
||||||
|
s[i].Color = AnimalFactory.FurTint(o[i].Traits) * brightness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// «Раскачка» при ходьбе (презентация): идущий зверь покачивается из стороны в сторону (наклон спрайта
|
||||||
|
/// через <see cref="Transform2D.Rotation"/>), стоящий — плавно выпрямляется. Делает движение живее, не
|
||||||
|
/// затрагивая симуляцию (она вращение не использует). Фаза покачивания идёт по игровому времени (стоп на
|
||||||
|
/// паузе, быстрее на ускорении) и сдвинута по id особи — звери качаются вразнобой. Граница sim/presentation
|
||||||
|
/// цела: система пишет только поворот трансформа.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalBobSystem(GameClock clock) : QuerySystem<AnimalBrain, Transform2D>
|
||||||
|
{
|
||||||
|
private const float SwaySpeed = 9f; // скорость набега фазы (рад/сек игрового времени)
|
||||||
|
private const float SwayAmplitude = 0.08f; // амплитуда наклона (рад ≈ 4.6°)
|
||||||
|
private const float MinMoveSq = 0.5f * 0.5f; // ближе к цели — считаем стоящим
|
||||||
|
private const float UprightDecay = 0.85f; // как быстро выпрямляется стоя (за кадр)
|
||||||
|
|
||||||
|
private float _phase;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var dt = clock.DeltaTime;
|
||||||
|
if (dt <= 0f)
|
||||||
|
{
|
||||||
|
return; // пауза — не качаем и не выпрямляем
|
||||||
|
}
|
||||||
|
|
||||||
|
_phase += dt * SwaySpeed;
|
||||||
|
foreach (var (brains, transforms, entities) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var b = brains.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < b.Length; i++)
|
||||||
|
{
|
||||||
|
ref var rot = ref t[i].Rotation;
|
||||||
|
if ((b[i].Target - t[i].Position).LengthSquared() > MinMoveSq)
|
||||||
|
{
|
||||||
|
var offset = entities.EntityAt(i).Id * 0.7f; // десинхрон между особями
|
||||||
|
rot = MathF.Sin(_phase + offset) * SwayAmplitude;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rot *= UprightDecay; // у цели/стоя — плавно к вертикали
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// «Мозг» зверя: выбранное действие (id поведения), его цель и таймер до пересмотра. Решение принимает
|
||||||
|
/// <see cref="AnimalDecisionSystem"/> (движковый <see cref="UtilityAi{TContext}"/>), исполняет
|
||||||
|
/// <see cref="AnimalActionSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct AnimalBrain : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Id текущего действия (см. <see cref="AnimalActions"/>).</summary>
|
||||||
|
public string Action;
|
||||||
|
|
||||||
|
/// <summary>Куда идти (позиция растения/воды/партнёра/точки блуждания).</summary>
|
||||||
|
public Vector2 Target;
|
||||||
|
|
||||||
|
/// <summary>Id растения-цели для <see cref="AnimalActions.Eat"/>; -1 — нет.</summary>
|
||||||
|
public int TargetPlant;
|
||||||
|
|
||||||
|
/// <summary>Id партнёра для <see cref="AnimalActions.Mate"/>; -1 — нет.</summary>
|
||||||
|
public int TargetMate;
|
||||||
|
|
||||||
|
/// <summary>Id цели для <see cref="AnimalActions.Hunt"/> — живой добычи или трупа; -1 — нет.</summary>
|
||||||
|
public int TargetPrey;
|
||||||
|
|
||||||
|
/// <summary>Цель — труп (падаль), а не живая добыча: при достижении едим, а не атакуем.</summary>
|
||||||
|
public bool TargetIsCorpse;
|
||||||
|
|
||||||
|
/// <summary>Секунды до следующего пересмотра решения.</summary>
|
||||||
|
public float DecideIn;
|
||||||
|
|
||||||
|
/// <summary>Игровые секунды до следующего удара по добыче (кулдаун дискретных атак, как в RimWorld).</summary>
|
||||||
|
public float AttackCooldown;
|
||||||
|
|
||||||
|
/// <summary>Текущее направление блуждания (коррелированное — плавно дрейфует, не дёргается каждый раз).</summary>
|
||||||
|
public Vector2 Heading;
|
||||||
|
|
||||||
|
/// <summary>Игровое время (TotalTime), до которого длится текущая холостая активность (отдых/копание/…).</summary>
|
||||||
|
public float ActivityUntil;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Снимок нужд зверя для соображений utility-выбора (доступ по индексу нужды) с гейтом интеллектом
|
||||||
|
/// (фаза A7): нужда, чей <see cref="NeedDef.MinBrain"/> выше эффективного интеллекта особи
|
||||||
|
/// (brainSize × Consciousness), в выбор не входит.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct AnimalContext(
|
||||||
|
float[] values,
|
||||||
|
NeedSet needs,
|
||||||
|
float intelligence,
|
||||||
|
int overrideIndex = -1,
|
||||||
|
float overrideValue = 0f
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private readonly float[] _values = values;
|
||||||
|
private readonly NeedSet _needs = needs;
|
||||||
|
private readonly float _intelligence = intelligence;
|
||||||
|
|
||||||
|
// Переопределение значения одной нужды (стиль питания: «эффективный голод» с гистерезисом —
|
||||||
|
// см. AnimalDecisionSystem). Override имеет приоритет над сырым значением и над гейтом мозга.
|
||||||
|
private readonly int _overrideIndex = overrideIndex;
|
||||||
|
private readonly float _overrideValue = overrideValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Значение нужды по индексу. Если для этой нужды задан override (стиль питания) — он и возвращается.
|
||||||
|
/// Если её порог мозга выше интеллекта особи — нужда недоступна и возвращается её «не-срочный» край
|
||||||
|
/// (deplete → 1 «сыта», drive → 0 «без влечения»), чтобы соображение дало ~0 и действие не выбралось.
|
||||||
|
/// Индекс вне диапазона → 0.
|
||||||
|
/// </summary>
|
||||||
|
public float Get(int index)
|
||||||
|
{
|
||||||
|
if (index == _overrideIndex)
|
||||||
|
{
|
||||||
|
return _overrideValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= _values.Length)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_needs[index].MinBrain > _intelligence)
|
||||||
|
{
|
||||||
|
return _needs.IsDrive(index) ? 0f : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _values[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Разворот спрайта по направлению движения (как в RimWorld): зверь смотрит туда, куда идёт. Восток —
|
||||||
|
/// базовая текстура, запад — её зеркало (флип по X), север/юг — отдельные текстуры (если есть у вида).
|
||||||
|
/// Направление берётся из цели «мозга» (<see cref="AnimalBrain.Target"/>) относительно позиции; стоящий
|
||||||
|
/// зверь сохраняет прежний разворот. При смене региона пересчитывает центр-origin и масштаб (текстуры
|
||||||
|
/// направлений могут отличаться шириной). Презентация (читает sim, пишет только Sprite/Transform-scale).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize)
|
||||||
|
: QuerySystem<AnimalBrain, AnimalOrganism, AnimalGrowth, Transform2D, Sprite>
|
||||||
|
{
|
||||||
|
private const float MinMove = 0.5f; // короче — считаем стоящим, разворот не трогаем
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
foreach (var (brains, organisms, growths, transforms, sprites, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var b = brains.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var g = growths.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
var s = sprites.Span;
|
||||||
|
for (var i = 0; i < b.Length; i++)
|
||||||
|
{
|
||||||
|
var dir = b[i].Target - t[i].Position;
|
||||||
|
if (dir.LengthSquared() < MinMove * MinMove)
|
||||||
|
{
|
||||||
|
continue; // стоит/у цели — сохраняем текущий разворот
|
||||||
|
}
|
||||||
|
|
||||||
|
var sp = animals[o[i].Species];
|
||||||
|
var dirs = sp.LookDirs(g[i].Stage, o[i].IsMale);
|
||||||
|
Texture2DRegion region;
|
||||||
|
SpriteFlip flip;
|
||||||
|
if (MathF.Abs(dir.X) >= MathF.Abs(dir.Y))
|
||||||
|
{
|
||||||
|
region = dirs.East;
|
||||||
|
flip = dir.X < 0f ? SpriteFlip.X : SpriteFlip.None; // запад = зеркало востока
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
region = dir.Y < 0f ? dirs.North : dirs.South; // экранный Y вниз: вверх = север
|
||||||
|
flip = SpriteFlip.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var sprite = ref s[i];
|
||||||
|
if (sprite.Region == region && sprite.Flip == flip)
|
||||||
|
{
|
||||||
|
continue; // разворот не изменился
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite.Region = region;
|
||||||
|
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||||
|
sprite.Flip = flip;
|
||||||
|
t[i].Scale = new Vector2(
|
||||||
|
AnimalFactory.Scale(sp, o[i].Traits, g[i].Stage, region, cellSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние роста животного (фаза A3): текущая стадия и накопленный возраст в игровых днях. Плоские
|
||||||
|
/// данные — переход между стадиями (Baby→Juvenile→Adult→Senior), смену спрайта и размера выполняет
|
||||||
|
/// <see cref="AnimalGrowthSystem"/>, читая пороги из генов (возраст созревания, продолжительность жизни).
|
||||||
|
/// Зеркало <see cref="PlantGrowth"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct AnimalGrowth : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Текущая стадия: 0 Baby, 1 Juvenile, 2 Adult, 3 Senior.</summary>
|
||||||
|
public int Stage;
|
||||||
|
|
||||||
|
/// <summary>Накопленный возраст в игровых днях.</summary>
|
||||||
|
public float AgeDays;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
|
||||||
|
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalGrowthSystem(
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
AnimalSet animals,
|
||||||
|
int cellSize,
|
||||||
|
WorldEvents events
|
||||||
|
) : QuerySystem<AnimalGrowth, AnimalOrganism, Transform2D, Sprite>
|
||||||
|
{
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var days = clock.DeltaTime / secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (growths, organisms, transforms, sprites, entities) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
var s = sprites.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
ref var grow = ref g[i];
|
||||||
|
grow.AgeDays += days;
|
||||||
|
ref readonly var org = ref o[i];
|
||||||
|
var sp = animals[org.Species];
|
||||||
|
var stage = AnimalFactory.StageAt(
|
||||||
|
grow.AgeDays,
|
||||||
|
org.Traits.MaturityAge,
|
||||||
|
org.Traits.Lifespan,
|
||||||
|
sp.Stages
|
||||||
|
);
|
||||||
|
if (stage == grow.Stage)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
grow.Stage = stage;
|
||||||
|
var region = AnimalFactory.RegionFor(sp, stage, org.IsMale);
|
||||||
|
ref var sprite = ref s[i];
|
||||||
|
sprite.Region = region;
|
||||||
|
sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||||
|
t[i].Scale = new Vector2(
|
||||||
|
AnimalFactory.Scale(sp, org.Traits, stage, region, cellSize)
|
||||||
|
);
|
||||||
|
events.Grew(entities.EntityAt(i), org.Species, sp.Stages[stage].Name); // в хронику особи
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Здоровье и болезни (фаза A6): медленный тик (~игровой час). Фоновое заражение, прогресс болезней
|
||||||
|
/// (гонка тяжесть↔иммунитет — выздоровление или смерть), пересчёт способностей с учётом хедифов. Смерть
|
||||||
|
/// по здоровью (летальная болезнь / кровь на нуле / потеря сознания) оставляет труп. Первый реальный
|
||||||
|
/// источник урона — оживляет каркас A5 (больные звери теряют Moving → медленнее).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalHealthSystem : BaseSystem
|
||||||
|
{
|
||||||
|
private const float TickDays = 1f / 24f; // ~игровой час
|
||||||
|
private const float BloodRecoveryPerDay = 0.25f; // восстановление крови в покое (нет кровотечений)
|
||||||
|
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly AnimalSet _animals;
|
||||||
|
private readonly WorldEvents _events;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly Climate _climate;
|
||||||
|
private readonly HediffDef[] _ambient;
|
||||||
|
private readonly HediffDef? _hypothermia;
|
||||||
|
private readonly HediffDef? _heatstroke;
|
||||||
|
private readonly HediffDef? _infection;
|
||||||
|
private readonly Random _rng;
|
||||||
|
private readonly ArchetypeQuery<Health, AnimalOrganism, AnimalGrowth> _query;
|
||||||
|
private readonly List<Entity> _deaths = [];
|
||||||
|
private float _accum;
|
||||||
|
|
||||||
|
// Сенесценс (старение): спад способностей начинается с этой доли продолжительности жизни и достигает
|
||||||
|
// максимума к её концу. Дряхлеющий зверь слабее (медленнее, хуже видит/фильтрует кровь) и уязвимее.
|
||||||
|
private const float SenescenceStart = 0.6f;
|
||||||
|
private const float SenescenceMax = 0.45f;
|
||||||
|
|
||||||
|
// Термокомфорт (°C) при теплоизоляции 0.5 и размере тела 1; границы сдвигаются мехом и размером тела.
|
||||||
|
private const float ColdComfort = 2f; // ниже — мёрзнет
|
||||||
|
private const float HeatComfort = 30f; // выше — перегревается
|
||||||
|
private const float ColdInsulationBonus = 30f; // мех (insulation 0..1) расширяет холодоустойчивость
|
||||||
|
private const float HeatInsulationPenalty = 14f; // тот же мех ухудшает жароустойчивость
|
||||||
|
private const float ColdBodyBonus = 8f; // крупное тело (тепловая инерция) держит холод лучше
|
||||||
|
private const float ThermalRatePerDegree = 0.06f; // прирост тяжести за °C за пределом комфорта в день
|
||||||
|
|
||||||
|
// Заражение раны: шанс в день при кровотечении тяжести 1 подхватить инфекцию.
|
||||||
|
private const float InfectionChancePerDay = 0.25f;
|
||||||
|
|
||||||
|
public AnimalHealthSystem(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
WorldEvents events,
|
||||||
|
int cellSize,
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
Climate climate,
|
||||||
|
HediffDef[] ambient,
|
||||||
|
HediffDef? hypothermia,
|
||||||
|
HediffDef? heatstroke,
|
||||||
|
HediffDef? infection,
|
||||||
|
int seed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_animals = animals;
|
||||||
|
_events = events;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_clock = clock;
|
||||||
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_climate = climate;
|
||||||
|
_ambient = ambient;
|
||||||
|
_hypothermia = hypothermia;
|
||||||
|
_heatstroke = heatstroke;
|
||||||
|
_infection = infection;
|
||||||
|
_rng = new Random(seed);
|
||||||
|
_query = store.Query<Health, AnimalOrganism, AnimalGrowth>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Множитель способностей от старения (1 — молод/расцвет, <1 — дряхлеет к концу жизни).</summary>
|
||||||
|
public static float SenescenceFactor(float ageDays, float lifespan)
|
||||||
|
{
|
||||||
|
if (lifespan <= 0f)
|
||||||
|
{
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frac = (ageDays / lifespan - SenescenceStart) / MathF.Max(0.01f, 1f - SenescenceStart);
|
||||||
|
return 1f - SenescenceMax * Math.Clamp(frac, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
_accum += _clock.DeltaTime / _secondsPerDay;
|
||||||
|
if (_accum < TickDays)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var days = _accum;
|
||||||
|
_accum = 0f;
|
||||||
|
_deaths.Clear();
|
||||||
|
|
||||||
|
foreach (var (healths, organisms, growths, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var h = healths.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var g = growths.Span;
|
||||||
|
for (var i = 0; i < h.Length; i++)
|
||||||
|
{
|
||||||
|
var state = h[i].State;
|
||||||
|
if (state is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var disease in _ambient)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!state.Has(disease.DefName)
|
||||||
|
&& _rng.NextSingle() < disease.AmbientPerDay * days
|
||||||
|
)
|
||||||
|
{
|
||||||
|
state.Add(disease);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Терморегуляция: вне комфорта (зависит от GeneInsulation + размера тела) копится тяжесть
|
||||||
|
// гипотермии/теплового удара; в комфорте immunityPerDay их рассасывает. Мех расширяет
|
||||||
|
// холодоустойчивость, но ухудшает жароустойчивость; крупное тело держит холод.
|
||||||
|
var insul = Math.Clamp(o[i].Traits.Insulation, 0f, 1f);
|
||||||
|
var coldLimit =
|
||||||
|
ColdComfort
|
||||||
|
- ColdInsulationBonus * insul
|
||||||
|
- ColdBodyBonus * MathF.Max(0f, o[i].Traits.BodySize - 1f);
|
||||||
|
var heatLimit = HeatComfort - HeatInsulationPenalty * insul;
|
||||||
|
var temp = _climate.Temperature;
|
||||||
|
if (temp < coldLimit && _hypothermia is not null)
|
||||||
|
{
|
||||||
|
state.Intensify(_hypothermia, (coldLimit - temp) * ThermalRatePerDegree * days);
|
||||||
|
}
|
||||||
|
else if (temp > heatLimit && _heatstroke is not null)
|
||||||
|
{
|
||||||
|
state.Intensify(_heatstroke, (temp - heatLimit) * ThermalRatePerDegree * days);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заражение раны: пока есть кровотечение, шанс (∝ его тяжести) подхватить инфекцию.
|
||||||
|
if (_infection is not null && !state.Has(_infection.DefName))
|
||||||
|
{
|
||||||
|
var bleedSeverity = 0f;
|
||||||
|
foreach (var hd in state.Hediffs)
|
||||||
|
{
|
||||||
|
if (hd.Def.BloodLossPerDay > 0f && hd.Severity > bleedSeverity)
|
||||||
|
{
|
||||||
|
bleedSeverity = hd.Severity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
bleedSeverity > 0f
|
||||||
|
&& _rng.NextSingle() < InfectionChancePerDay * bleedSeverity * days
|
||||||
|
)
|
||||||
|
{
|
||||||
|
state.Add(_infection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтрация крови (почки/печень) ускоряет вывод токсинов и набор иммунитета: повреждённый
|
||||||
|
// орган → медленнее выздоровление от яда/болезни/инфекции (для тела без неё Capacity вернёт 1).
|
||||||
|
var lethal = state.AdvanceHediffs(
|
||||||
|
days,
|
||||||
|
state.Capacity(AnimalCapacities.BloodFiltration)
|
||||||
|
);
|
||||||
|
// Кровопотеря от ран (кровотечения), иначе — постепенное восстановление крови в покое.
|
||||||
|
var bleed = 0f;
|
||||||
|
foreach (var hd in state.Hediffs)
|
||||||
|
{
|
||||||
|
bleed += hd.Def.BloodLossPerDay * hd.Severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bleed > 0f)
|
||||||
|
{
|
||||||
|
state.BloodLevel = Math.Clamp(state.BloodLevel - bleed * days, 0f, 1f);
|
||||||
|
}
|
||||||
|
else if (state.BloodLevel < 1f)
|
||||||
|
{
|
||||||
|
state.BloodLevel = Math.Clamp(
|
||||||
|
state.BloodLevel + BloodRecoveryPerDay * days,
|
||||||
|
0f,
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Старение: к концу жизни способности дряхлеют (медленнее/слабее, уязвимее к ранам/болезни).
|
||||||
|
var senescence = SenescenceFactor(g[i].AgeDays, o[i].Traits.Lifespan);
|
||||||
|
state.RecomputeCapacities(senescence);
|
||||||
|
if (
|
||||||
|
lethal
|
||||||
|
|| state.BloodLevel <= 0f
|
||||||
|
|| state.Capacity(AnimalCapacities.Consciousness) <= 0.01f
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_deaths.Add(entities.EntityAt(i));
|
||||||
|
_events.Died(o[i].Species, "event.death.disease");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dead in _deaths)
|
||||||
|
{
|
||||||
|
AnimalFactory.Die(_store, _animals, dead, _cellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Забывание (память): уверенность каждой записи падает со временем, состарившиеся стираются. Свежие
|
||||||
|
/// наблюдения записывает/освежает <see cref="AnimalDecisionSystem"/>; эта система только «выветривает»
|
||||||
|
/// память, давая ей конечный срок.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalMemorySystem(GameClock clock, float secondsPerDay) : QuerySystem<Memory>
|
||||||
|
{
|
||||||
|
private const float ForgetPerDay = 0.5f; // уверенность падает (память живёт ~2 дня без подтверждения)
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var days = clock.DeltaTime / secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (memories, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var m = memories.Span;
|
||||||
|
for (var i = 0; i < m.Length; i++)
|
||||||
|
{
|
||||||
|
var entries = m[i].Entries;
|
||||||
|
if (entries is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var k = entries.Count - 1; k >= 0; k--)
|
||||||
|
{
|
||||||
|
var e = entries[k];
|
||||||
|
e.Confidence -= ForgetPerDay * days;
|
||||||
|
e.AgeDays += days;
|
||||||
|
if (e.Confidence <= 0f)
|
||||||
|
{
|
||||||
|
entries.RemoveAt(k);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entries[k] = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настроение (фаза A8): для особей тира 4+ (мозг ≥ порога) считает настроение из непрерывного фона
|
||||||
|
/// (удовлетворённость убывающих нужд минус боль из здоровья) и суммы затухающих событийных мыслей.
|
||||||
|
/// Мысли угасают со временем. У примитивных (тир < 4) настроения нет (Active=false, мысли сбрасываются).
|
||||||
|
/// Гейт интеллектом — тем же порогом тира 4, что и высшие нужды (сон/секс). Низкое настроение влияет на
|
||||||
|
/// поведение (стресс не даёт спариваться — см. <see cref="AnimalDecisionSystem"/>) и видно в консоли/инспекторе.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalMoodSystem(GameClock clock, float secondsPerDay, NeedSet needs)
|
||||||
|
: QuerySystem<Mood, AnimalNeeds, AnimalOrganism, Health>
|
||||||
|
{
|
||||||
|
/// <summary>Порог размера мозга (тир 4), с которого у особи появляется настроение.</summary>
|
||||||
|
public const float MoodMinBrain = 0.40f;
|
||||||
|
private const float NeutralBase = 0.35f; // настроение при полном истощении нужд (без боли/мыслей)
|
||||||
|
private const float NeedsWeight = 0.55f; // вклад средней удовлетворённости убывающих нужд
|
||||||
|
private const float PainWeight = 0.4f; // боль из здоровья тянет настроение вниз
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var days = clock.DeltaTime / secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (moods, needsChunk, organisms, healths, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var m = moods.Span;
|
||||||
|
var n = needsChunk.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var h = healths.Span;
|
||||||
|
for (var i = 0; i < m.Length; i++)
|
||||||
|
{
|
||||||
|
ref var mood = ref m[i];
|
||||||
|
if (o[i].Traits.BrainSize < MoodMinBrain)
|
||||||
|
{
|
||||||
|
mood.Active = false; // тир ниже зверя — настроения нет
|
||||||
|
mood.Value = 0.5f;
|
||||||
|
mood.Thoughts?.Clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mood.Active = true;
|
||||||
|
// 1) Затухание событийных мыслей + сумма их сдвигов.
|
||||||
|
var thoughtSum = 0f;
|
||||||
|
var list = mood.Thoughts;
|
||||||
|
if (list is not null)
|
||||||
|
{
|
||||||
|
for (var k = list.Count - 1; k >= 0; k--)
|
||||||
|
{
|
||||||
|
var t = list[k];
|
||||||
|
t.RemainingDays -= days;
|
||||||
|
if (t.RemainingDays <= 0f)
|
||||||
|
{
|
||||||
|
list.RemoveAt(k);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list[k] = t;
|
||||||
|
thoughtSum += t.Def.MoodOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Непрерывный фон: средняя удовлетворённость убывающих нужд минус боль из здоровья.
|
||||||
|
var wellbeing = AvgDeplete(n[i].Values);
|
||||||
|
var pain = h[i].State?.Pain ?? 0f;
|
||||||
|
mood.Value = Math.Clamp(
|
||||||
|
NeutralBase + NeedsWeight * wellbeing - PainWeight * pain + thoughtSum,
|
||||||
|
0f,
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Средняя удовлетворённость убывающих нужд (голод/жажда/усталость) [0,1]; нет нужд → 1 (нейтрально).
|
||||||
|
private float AvgDeplete(float[] values)
|
||||||
|
{
|
||||||
|
if (values is null)
|
||||||
|
{
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = 0f;
|
||||||
|
var count = 0;
|
||||||
|
for (var k = 0; k < needs.Count && k < values.Length; k++)
|
||||||
|
{
|
||||||
|
if (needs.IsDeplete(k))
|
||||||
|
{
|
||||||
|
sum += values[k];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0 ? sum / count : 1f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Смертность (фаза A3/A5): гибель от старости (возраст превысил продолжительность жизни) или истощения
|
||||||
|
/// (счётчик голода/жажды превысил порог). Замыкает контур ёмкости среды. Фаза A5: вместо деспавна
|
||||||
|
/// оставляет <see cref="Corpse"/> (через <see cref="AnimalFactory.CreateCorpse"/>), затем удаляет особь.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalMortalitySystem : BaseSystem
|
||||||
|
{
|
||||||
|
private const float LethalStarveDays = 3f;
|
||||||
|
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly AnimalSet _animals;
|
||||||
|
private readonly WorldEvents _events;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly ArchetypeQuery<AnimalGrowth, AnimalOrganism, AnimalNeeds> _query;
|
||||||
|
private readonly List<Entity> _deaths = [];
|
||||||
|
|
||||||
|
public AnimalMortalitySystem(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
WorldEvents events,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_animals = animals;
|
||||||
|
_events = events;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_query = store.Query<AnimalGrowth, AnimalOrganism, AnimalNeeds>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
_deaths.Clear();
|
||||||
|
foreach (var (growths, organisms, needsChunk, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var n = needsChunk.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
var ageDeath = g[i].AgeDays > o[i].Traits.Lifespan;
|
||||||
|
var starveDeath = n[i].Starve >= LethalStarveDays;
|
||||||
|
if (ageDeath || starveDeath)
|
||||||
|
{
|
||||||
|
_deaths.Add(entities.EntityAt(i));
|
||||||
|
_events.Died(o[i].Species, ageDeath ? "event.death.age" : "event.death.starve");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dead in _deaths)
|
||||||
|
{
|
||||||
|
AnimalFactory.Die(_store, _animals, dead, _cellSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Нужды зверя как ДАННЫЕ (рефактор расширяемости): значения по индексам из <see cref="NeedSet"/>
|
||||||
|
/// (managed-массив — добавление нужды не меняет структуру) + счётчик истощения. Падают/растут в
|
||||||
|
/// <see cref="AnimalNeedsSystem"/> по <see cref="NeedDef"/>, восполняются исполнением действия.
|
||||||
|
/// </summary>
|
||||||
|
public struct AnimalNeeds : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Значения нужд [0,1] по индексам <see cref="NeedSet"/>.</summary>
|
||||||
|
public float[] Values;
|
||||||
|
|
||||||
|
/// <summary>Накоплено игровых дней истощения (летальная нужда на нуле); порог — смерть (A3).</summary>
|
||||||
|
public float Starve;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Падение/рост нужд со временем (игровые дни через <see cref="GameClock"/>): deplete-нужды убывают
|
||||||
|
/// (× ген обмена веществ), drive-нужды растут под своим хедифом (гон) и спадают вне его. Летальные
|
||||||
|
/// нужды на нуле копят счётчик истощения. Полностью управляется данными <see cref="NeedSet"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalNeedsSystem(
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
NeedSet needs,
|
||||||
|
AnimalSet animals
|
||||||
|
) : QuerySystem<AnimalNeeds, AnimalOrganism, Health, AnimalGrowth>
|
||||||
|
{
|
||||||
|
// Прибавка к обмену веществ при устойчивости к яду 1.0 (цена детоксикации, коэволюция C2).
|
||||||
|
private const float ToleranceMetabolicCost = 0.5f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var days = clock.DeltaTime / secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (needsChunk, organisms, healths, growths, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var n = needsChunk.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var h = healths.Span;
|
||||||
|
var g = growths.Span;
|
||||||
|
for (var i = 0; i < n.Length; i++)
|
||||||
|
{
|
||||||
|
var values = n[i].Values;
|
||||||
|
if (values is null || values.Length < needs.Count)
|
||||||
|
{
|
||||||
|
continue; // защита от старого/пустого состояния
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обмен веществ = ген × цена устойчивости к яду (C2) × множитель Клайбера по массе тела:
|
||||||
|
// расход ∝ масса^0.75 = (линейный размер/эталон)^2.25 (масса ∝ размер³), нормирован на
|
||||||
|
// эталон вида → молодняк экономнее, крупные прожорливее. Эталон предпосчитан (без словаря).
|
||||||
|
var sp = animals[o[i].Species];
|
||||||
|
var stageScale = sp.Stages[Math.Clamp(g[i].Stage, 0, sp.Stages.Length - 1)].Scale;
|
||||||
|
var sizeRatio = MathF.Max(
|
||||||
|
0.01f,
|
||||||
|
o[i].Traits.BodySize * stageScale / sp.RefBodySize
|
||||||
|
);
|
||||||
|
var metabolism =
|
||||||
|
MathF.Max(0.1f, o[i].Traits.Metabolism)
|
||||||
|
* (1f + ToleranceMetabolicCost * Math.Clamp(o[i].Traits.ToxinTolerance, 0f, 1f))
|
||||||
|
* Mass.KleiberFactorFromSizeRatio(sizeRatio);
|
||||||
|
var lethalEmpty = false;
|
||||||
|
for (var k = 0; k < needs.Count; k++)
|
||||||
|
{
|
||||||
|
var def = needs[k];
|
||||||
|
if (needs.IsDrive(k))
|
||||||
|
{
|
||||||
|
var rising = def.RisesUnder is null || h[i].State.Has(def.RisesUnder);
|
||||||
|
var delta = (rising ? def.RisePerDay : -def.DecayPerDay) * days;
|
||||||
|
values[k] = Math.Clamp(values[k] + delta, 0f, 1f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
values[k] = Math.Clamp(
|
||||||
|
values[k] - def.DecayPerDay * metabolism * days,
|
||||||
|
0f,
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
if (def.Lethal && values[k] <= 0f)
|
||||||
|
{
|
||||||
|
lethalEmpty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref var starve = ref n[i].Starve;
|
||||||
|
starve = lethalEmpty ? starve + days : MathF.Max(0f, starve - days * 2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Компонент-организм животного: управляемый <see cref="Genome"/> (для размножения, сейва и
|
||||||
|
/// пересчёта), выраженный фенотип <see cref="Traits"/>, индекс вида в <see cref="Content.AnimalSet"/>
|
||||||
|
/// и номер поколения (для <c>popstats</c> и видимого дрейфа). Геном — ссылочный объект, поэтому
|
||||||
|
/// компонент несёт ссылку, а не копию (как у растений). Зеркало <see cref="PlantOrganism"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct AnimalOrganism : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Геном особи (аллели по генам); источник истины для признаков и размножения.</summary>
|
||||||
|
public Genome Genome;
|
||||||
|
|
||||||
|
/// <summary>Выраженные признаки — то, что читают системы.</summary>
|
||||||
|
public AnimalPhenotype Traits;
|
||||||
|
|
||||||
|
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/>.</summary>
|
||||||
|
public int Species;
|
||||||
|
|
||||||
|
/// <summary>Пол: самец (XY) или самка (XX) — выведен из локуса GeneSex при создании.</summary>
|
||||||
|
public bool IsMale;
|
||||||
|
|
||||||
|
/// <summary>Ест ли растения (диета по генам/дефу, неизменна после рождения — кэш вместо пересчёта на тик).</summary>
|
||||||
|
public bool EatsPlants;
|
||||||
|
|
||||||
|
/// <summary>Ест ли мясо (диета по генам/дефу, неизменна после рождения — кэш вместо пересчёта на тик).</summary>
|
||||||
|
public bool EatsMeat;
|
||||||
|
|
||||||
|
/// <summary>Номер поколения: max(родителей) + 1; у основателей 0.</summary>
|
||||||
|
public int Generation;
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Признаки (фенотип) животного, вычисленные из генома формулами генов (см. <c>Defs/Genes/</c>):
|
||||||
|
/// готовые значения, которые читают системы, не касаясь генов напрямую. Зеркало
|
||||||
|
/// <see cref="PlantPhenotype"/>. Имена полей соответствуют именам признаков в эффектах генов.
|
||||||
|
/// </summary>
|
||||||
|
public struct AnimalPhenotype
|
||||||
|
{
|
||||||
|
/// <summary>Размер тела в клетках (масштаб спрайта; позже — HP/расход энергии).</summary>
|
||||||
|
public float BodySize;
|
||||||
|
|
||||||
|
/// <summary>Скорость обмена веществ — множитель расхода нужд.</summary>
|
||||||
|
public float Metabolism;
|
||||||
|
|
||||||
|
/// <summary>Скорость передвижения (множитель).</summary>
|
||||||
|
public float MoveSpeed;
|
||||||
|
|
||||||
|
/// <summary>Объём крови (множитель) — масштаб скорости кровопотери.</summary>
|
||||||
|
public float BloodVolume;
|
||||||
|
|
||||||
|
/// <summary>Дальность зрения (множитель радиуса поиска).</summary>
|
||||||
|
public float Vision;
|
||||||
|
|
||||||
|
/// <summary>Острота слуха (множитель радиуса).</summary>
|
||||||
|
public float Hearing;
|
||||||
|
|
||||||
|
/// <summary>Острота обоняния (множитель радиуса).</summary>
|
||||||
|
public float Smell;
|
||||||
|
|
||||||
|
/// <summary>Острота осязания (множитель радиуса; чувство ближнего контакта).</summary>
|
||||||
|
public float Touch;
|
||||||
|
|
||||||
|
/// <summary>Камуфляж [0..1] — снижает обнаружение этой особи зрением (заметность = 1−camouflage).</summary>
|
||||||
|
public float Camouflage;
|
||||||
|
|
||||||
|
/// <summary>Запах [0..1] — повышает обнаружение этой особи обонянием.</summary>
|
||||||
|
public float Scent;
|
||||||
|
|
||||||
|
/// <summary>Шумность [0..1] — повышает обнаружение этой особи слухом при движении.</summary>
|
||||||
|
public float Noise;
|
||||||
|
|
||||||
|
/// <summary>Размер мозга (0..1) — основа интеллекта.</summary>
|
||||||
|
public float BrainSize;
|
||||||
|
|
||||||
|
/// <summary>Теплоизоляция мехом (0..1).</summary>
|
||||||
|
public float Insulation;
|
||||||
|
|
||||||
|
/// <summary>Оттенок меха (0..1) — сдвиг тинта спрайта.</summary>
|
||||||
|
public float FurHue;
|
||||||
|
|
||||||
|
/// <summary>Возраст созревания (игровых дней) — порог стадии Adult.</summary>
|
||||||
|
public float MaturityAge;
|
||||||
|
|
||||||
|
/// <summary>Травоядность [0..1] — способность/тяга питаться растениями.</summary>
|
||||||
|
public float Herbivory;
|
||||||
|
|
||||||
|
/// <summary>Хищничество [0..1] — тяга охотиться и питаться мясом (живая добыча/падаль).</summary>
|
||||||
|
public float Carnivory;
|
||||||
|
|
||||||
|
/// <summary>Всеядность [0..1] — генералист: включает оба источника пищи (растения и мясо).</summary>
|
||||||
|
public float Omnivory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Стиль питания [0..1]: 0 — «пастьба» (graze), часто подтапливает сытость мелкими приёмами;
|
||||||
|
/// 1 — «обжорство» (gorge), ищет еду лишь при сильном голоде и ест до полной сытости (редкие
|
||||||
|
/// крупные приёмы, как у хищников). Задаёт пороги начала/конца кормёжки в <see cref="Sim.AnimalDecisionSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public float FeedingStyle;
|
||||||
|
|
||||||
|
/// <summary>Устойчивость к растительным ядам [0..1] — снижает дозу отравления токсичным кормом.</summary>
|
||||||
|
public float ToxinTolerance;
|
||||||
|
|
||||||
|
/// <summary>Социальность [0..1] — тяга держаться стаи/стада (когезия + спокойствие в группе).</summary>
|
||||||
|
public float Sociability;
|
||||||
|
|
||||||
|
/// <summary>Продолжительность жизни (игровых дней).</summary>
|
||||||
|
public float Lifespan;
|
||||||
|
|
||||||
|
/// <summary>Сезон гона (0 весна … 3 зима).</summary>
|
||||||
|
public int BreedingSeason;
|
||||||
|
|
||||||
|
/// <summary>Срок вынашивания (игровых дней).</summary>
|
||||||
|
public float GestationDays;
|
||||||
|
|
||||||
|
/// <summary>Размер помёта.</summary>
|
||||||
|
public float LitterSize;
|
||||||
|
|
||||||
|
/// <summary>Тип рождения [0..1]: ≥0.5 — яйцекладущий (откладывает яйца), иначе живородящий.</summary>
|
||||||
|
public float EggLaying;
|
||||||
|
|
||||||
|
/// <summary>Яйцекладущий ли вид (тип рождения) — определяет цикл зачатия.</summary>
|
||||||
|
public readonly bool Oviparous => EggLaying >= 0.5f;
|
||||||
|
|
||||||
|
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||||
|
public static AnimalPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
||||||
|
{
|
||||||
|
float T(string name) => traits.GetValueOrDefault(name);
|
||||||
|
// Острота чувств по умолчанию 1 (вид без гена не «слепнет»); заметность — мягкие дефолты.
|
||||||
|
float Acuity(string name) => traits.TryGetValue(name, out var v) ? v : 1f;
|
||||||
|
return new AnimalPhenotype
|
||||||
|
{
|
||||||
|
BodySize = T("bodySize"),
|
||||||
|
Metabolism = T("metabolism"),
|
||||||
|
MoveSpeed = T("moveSpeed"),
|
||||||
|
BloodVolume = T("bloodVolume"),
|
||||||
|
Vision = T("vision"),
|
||||||
|
Hearing = Acuity("hearing"),
|
||||||
|
Smell = Acuity("smell"),
|
||||||
|
Touch = Acuity("touch"),
|
||||||
|
Camouflage = T("camouflage"),
|
||||||
|
Scent = traits.GetValueOrDefault("scent", 0.5f),
|
||||||
|
Noise = traits.GetValueOrDefault("noise", 0.5f),
|
||||||
|
BrainSize = T("brainSize"),
|
||||||
|
Insulation = T("insulation"),
|
||||||
|
FurHue = T("furHue"),
|
||||||
|
MaturityAge = T("maturityAge"),
|
||||||
|
Herbivory = T("herbivory"),
|
||||||
|
Carnivory = T("carnivory"),
|
||||||
|
Omnivory = T("omnivory"),
|
||||||
|
FeedingStyle = traits.GetValueOrDefault("feedingStyle", 0.5f),
|
||||||
|
ToxinTolerance = T("toxinTolerance"),
|
||||||
|
Sociability = T("sociability"),
|
||||||
|
Lifespan = T("lifespan"),
|
||||||
|
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||||
|
GestationDays = T("gestationDays"),
|
||||||
|
LitterSize = T("litterSize"),
|
||||||
|
EggLaying = T("eggLaying"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Беременность и роды (фаза A4): тикает срок вынашивания; по истечении рожает помёт — каждый детёныш =
|
||||||
|
/// <see cref="Genome.Breed"/> генома матери и отца (пол наследуется без YY), стадия Baby, поколение
|
||||||
|
/// матери + 1, рядом с матерью. Компонент <see cref="Pregnant"/> снимается. Потолок численности
|
||||||
|
/// страхует от взрыва популяции.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalPregnancySystem : BaseSystem
|
||||||
|
{
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly AnimalSet _animals;
|
||||||
|
private readonly NeedSet _needs;
|
||||||
|
private readonly SkillSet _skills;
|
||||||
|
private readonly ThoughtSet _thoughts;
|
||||||
|
private readonly WorldEvents _events;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly float _secondsPerDay;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly int _maxAnimals;
|
||||||
|
private readonly Random _rng;
|
||||||
|
private readonly ArchetypeQuery<Pregnant, AnimalOrganism, Transform2D> _query;
|
||||||
|
private readonly ArchetypeQuery<AnimalOrganism> _census;
|
||||||
|
private readonly List<Birth> _births = [];
|
||||||
|
|
||||||
|
public AnimalPregnancySystem(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
NeedSet needs,
|
||||||
|
SkillSet skills,
|
||||||
|
ThoughtSet thoughts,
|
||||||
|
WorldEvents events,
|
||||||
|
GameClock clock,
|
||||||
|
float secondsPerDay,
|
||||||
|
int cellSize,
|
||||||
|
int maxAnimals,
|
||||||
|
int seed
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_animals = animals;
|
||||||
|
_needs = needs;
|
||||||
|
_skills = skills;
|
||||||
|
_thoughts = thoughts;
|
||||||
|
_events = events;
|
||||||
|
_clock = clock;
|
||||||
|
_secondsPerDay = secondsPerDay;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_maxAnimals = maxAnimals;
|
||||||
|
_rng = new Random(seed);
|
||||||
|
_query = store.Query<Pregnant, AnimalOrganism, Transform2D>();
|
||||||
|
_census = store.Query<AnimalOrganism>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
var days = _clock.DeltaTime / _secondsPerDay;
|
||||||
|
if (days <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_births.Clear();
|
||||||
|
foreach (var (pregnancies, organisms, transforms, entities) in _query.Chunks)
|
||||||
|
{
|
||||||
|
var p = pregnancies.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var t = transforms.Span;
|
||||||
|
for (var i = 0; i < p.Length; i++)
|
||||||
|
{
|
||||||
|
ref var preg = ref p[i];
|
||||||
|
preg.DueInDays -= days;
|
||||||
|
if (preg.DueInDays > 0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_births.Add(
|
||||||
|
new Birth
|
||||||
|
{
|
||||||
|
Mother = entities.EntityAt(i),
|
||||||
|
Species = o[i].Species,
|
||||||
|
MotherGenome = o[i].Genome,
|
||||||
|
FatherGenome = preg.FatherGenome,
|
||||||
|
Litter = preg.Litter,
|
||||||
|
Generation = o[i].Generation + 1,
|
||||||
|
Position = t[i].Position,
|
||||||
|
Oviparous = o[i].Traits.Oviparous,
|
||||||
|
IncubateDays = MathF.Max(1f, o[i].Traits.GestationDays),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = _census.Count;
|
||||||
|
foreach (var birth in _births)
|
||||||
|
{
|
||||||
|
if (birth.Mother.HasComponent<Pregnant>())
|
||||||
|
{
|
||||||
|
birth.Mother.RemoveComponent<Pregnant>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Роды — сильная положительная мысль матери (A8).
|
||||||
|
if (birth.Mother.HasComponent<Mood>())
|
||||||
|
{
|
||||||
|
birth.Mother.GetComponent<Mood>().Add(_thoughts.Get(AnimalThoughts.GaveBirth));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bornLive = 0;
|
||||||
|
for (var k = 0; k < birth.Litter && count < _maxAnimals; k++)
|
||||||
|
{
|
||||||
|
var child = Genome.Breed(
|
||||||
|
birth.MotherGenome,
|
||||||
|
birth.FatherGenome,
|
||||||
|
_animals.GeneRegistry,
|
||||||
|
_rng
|
||||||
|
);
|
||||||
|
var offset =
|
||||||
|
new Vector2(_rng.NextSingle() - 0.5f, _rng.NextSingle() - 0.5f) * _cellSize;
|
||||||
|
if (birth.Oviparous)
|
||||||
|
{
|
||||||
|
// Яйцекладка: вместо живого детёныша откладываем яйцо с его геномом — вылупится позже.
|
||||||
|
AnimalFactory.CreateEgg(
|
||||||
|
_store,
|
||||||
|
_animals,
|
||||||
|
birth.Species,
|
||||||
|
birth.Position + offset,
|
||||||
|
child,
|
||||||
|
birth.Generation,
|
||||||
|
birth.IncubateDays,
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnimalFactory.Create(
|
||||||
|
_store,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
birth.Species,
|
||||||
|
birth.Position + offset,
|
||||||
|
ageDays: 0f,
|
||||||
|
child,
|
||||||
|
birth.Generation,
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
bornLive++;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Живые роды — заметное событие (яйцекладка отметится позже при вылуплении в EggSystem).
|
||||||
|
if (bornLive > 0)
|
||||||
|
{
|
||||||
|
_events.Born(birth.Mother, birth.Species, bornLive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Birth
|
||||||
|
{
|
||||||
|
public Entity Mother;
|
||||||
|
public int Species;
|
||||||
|
public Genome MotherGenome;
|
||||||
|
public Genome FatherGenome;
|
||||||
|
public int Litter;
|
||||||
|
public int Generation;
|
||||||
|
public Vector2 Position;
|
||||||
|
public bool Oviparous;
|
||||||
|
public float IncubateDays;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Genetics;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Гон (фаза A4): взрослым в их брачный сезон навешивает хедиф Rut, вне сезона снимает. Рост влечения
|
||||||
|
/// под этим хедифом считает <see cref="AnimalNeedsSystem"/> (нужда Mating с <c>risesUnder=Rut</c>) —
|
||||||
|
/// сезонность и влечение разнесены: эта система отвечает только за состояние.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalRutSystem(AnimalSet animals, Climate climate, HediffDef rut)
|
||||||
|
: QuerySystem<AnimalGrowth, AnimalOrganism, Health>
|
||||||
|
{
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var season = (int)climate.Season;
|
||||||
|
foreach (var (growths, organisms, healths, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var g = growths.Span;
|
||||||
|
var o = organisms.Span;
|
||||||
|
var h = healths.Span;
|
||||||
|
for (var i = 0; i < g.Length; i++)
|
||||||
|
{
|
||||||
|
var inRut =
|
||||||
|
AnimalFactory.IsAdult(animals[o[i].Species], g[i].Stage)
|
||||||
|
&& season == o[i].Traits.BreedingSeason;
|
||||||
|
if (inRut)
|
||||||
|
{
|
||||||
|
h[i].State.Add(rut);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
h[i].State.Remove(rut.DefName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user