Compare commits
54
Commits
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"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,
|
||||||
|
"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,
|
||||||
|
"attackDamage": 80,
|
||||||
|
"attackBleed": 3.0,
|
||||||
|
"attackBloodLoss": 5.0,
|
||||||
|
"baseMassKg": 45,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 1.6,
|
||||||
|
"GeneMetabolism": 1.1,
|
||||||
|
"GeneMoveSpeed": 1.4,
|
||||||
|
"GeneBloodVolume": 1.3,
|
||||||
|
"GeneVision": 1.5,
|
||||||
|
"GeneBrainSize": 0.5,
|
||||||
|
"GeneInsulation": 0.6,
|
||||||
|
"GeneFurColor": 0.7,
|
||||||
|
"GeneMaturityAge": 70,
|
||||||
|
"GeneLifespan": 240,
|
||||||
|
"GeneBreedingSeason": 3,
|
||||||
|
"GeneGestationDays": 35,
|
||||||
|
"GeneLitterSize": 4,
|
||||||
|
"GeneCarnivory": 1.0,
|
||||||
|
"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,
|
||||||
|
"attackDamage": 45,
|
||||||
|
"attackBleed": 1.8,
|
||||||
|
"attackBloodLoss": 3.0,
|
||||||
|
"baseMassKg": 80,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 1.3,
|
||||||
|
"GeneMetabolism": 1.2,
|
||||||
|
"GeneMoveSpeed": 1.0,
|
||||||
|
"GeneBloodVolume": 1.2,
|
||||||
|
"GeneVision": 1.0,
|
||||||
|
"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,
|
||||||
|
"GeneSociability": 0.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"defName": "Chicken",
|
||||||
|
"label": "pawn.chicken",
|
||||||
|
"kind": "animal",
|
||||||
|
"texture": "things/pawn/animal/chicken/Chicken_east",
|
||||||
|
"body": "Quadruped",
|
||||||
|
"diet": ["plant"],
|
||||||
|
"spawnPer1000Cells": 1.5,
|
||||||
|
"baseSpeed": 22,
|
||||||
|
"visionCells": 10,
|
||||||
|
"baseMassKg": 2.5,
|
||||||
|
"genome": {
|
||||||
|
"GeneMaxBodySize": 0.5,
|
||||||
|
"GeneMetabolism": 1.3,
|
||||||
|
"GeneMoveSpeed": 0.9,
|
||||||
|
"GeneBloodVolume": 0.7,
|
||||||
|
"GeneVision": 1.1,
|
||||||
|
"GeneBrainSize": 0.3,
|
||||||
|
"GeneInsulation": 0.45,
|
||||||
|
"GeneFurColor": 0.6,
|
||||||
|
"GeneMaturityAge": 50,
|
||||||
|
"GeneLifespan": 180,
|
||||||
|
"GeneBreedingSeason": 1,
|
||||||
|
"GeneGestationDays": 12,
|
||||||
|
"GeneLitterSize": 6,
|
||||||
|
"GeneHerbivory": 0.8,
|
||||||
|
"GeneToxinTolerance": 0.15,
|
||||||
|
"GeneEggLaying": 1.0,
|
||||||
|
"GeneSociability": 0.55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"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": "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,86 @@
|
|||||||
|
{
|
||||||
|
"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" } },
|
||||||
|
{ "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" } },
|
||||||
|
|
||||||
|
// Пол как локус (фаза 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" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Контент-гены (фаза G4+): плодоношение, добыча, цвет, производные и дискретные морфы.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
|
||||||
|
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
|
||||||
|
"effects": { "fruitYield": "value" } },
|
||||||
|
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
|
||||||
|
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
|
||||||
|
"effects": { "fruitSeason": "value" } },
|
||||||
|
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
|
||||||
|
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
|
||||||
|
"effects": { "harvestAmount": "value" } },
|
||||||
|
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
|
||||||
|
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
|
||||||
|
"effects": { "leafHue": "value" } },
|
||||||
|
|
||||||
|
// --- Защита растения (коэволюция с травоядными) ---
|
||||||
|
// Токсичность отравляет поедателя (hediff Poisoned, тем сильнее, чем ниже его устойчивость к яду);
|
||||||
|
// шипы наносят поедателю лёгкую травму; вкусность (palatability) — обратная привлекательность для
|
||||||
|
// умных травоядных (избегание появится в C2). У защиты ЕСТЬ ЦЕНА: токсичность/шипы тормозят рост
|
||||||
|
// (см. PlantGrowthSystem), иначе все растения дошли бы до максимума и коэволюция бы встала.
|
||||||
|
{ "defName": "GeneToxicity", "parent": "BaseNumericGene", "label": "gene.toxicity",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
|
||||||
|
"effects": { "toxicity": "value" } },
|
||||||
|
{ "defName": "GeneThorns", "parent": "BaseNumericGene", "label": "gene.thorns",
|
||||||
|
"default": 0.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
|
||||||
|
"effects": { "thorns": "value" } },
|
||||||
|
{ "defName": "GenePalatability", "parent": "BaseNumericGene", "label": "gene.palatability",
|
||||||
|
"default": 1.0, "min": 0.0, "max": 1.0, "tags": ["defense"],
|
||||||
|
"effects": { "palatability": "value" } },
|
||||||
|
|
||||||
|
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
|
||||||
|
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
|
||||||
|
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
|
||||||
|
"default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"],
|
||||||
|
"effects": { "hardiness": "gsum('Gene.*Tolerance')" } },
|
||||||
|
|
||||||
|
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
||||||
|
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
||||||
|
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
||||||
|
"effects": { "variant": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Гены среды: свет, температура, почва. Покрывают экологические измерения генома растения,
|
||||||
|
// которые читают системы роста/жизненного цикла.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight",
|
||||||
|
"default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"],
|
||||||
|
"effects": { "optimalLight": "value" } },
|
||||||
|
{ "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance",
|
||||||
|
"default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"],
|
||||||
|
"effects": { "lightTolerance": "value" } },
|
||||||
|
{ "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature",
|
||||||
|
"default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "optimalTemperature": "value" } },
|
||||||
|
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
|
||||||
|
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "temperatureTolerance": "value" } },
|
||||||
|
// Жёсткие края диапазона роста (модель RimWorld): на сколько °C ниже/выше плато оптимума рост
|
||||||
|
// спадает до нуля. Ниже (optimal-tolerance-coldHardiness) или выше (optimal+tolerance+heatHardiness)
|
||||||
|
// растение дормантно и копит температурный стресс. Асимметрично: вид может терпеть жару лучше холода.
|
||||||
|
{ "defName": "GeneColdHardiness", "parent": "BaseNumericGene", "label": "gene.coldHardiness",
|
||||||
|
"default": 8, "min": 0, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "coldHardiness": "value" } },
|
||||||
|
{ "defName": "GeneHeatHardiness", "parent": "BaseNumericGene", "label": "gene.heatHardiness",
|
||||||
|
"default": 10, "min": 0, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "heatHardiness": "value" } },
|
||||||
|
{ "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
|
||||||
|
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
||||||
|
"effects": { "optimalFertility": "value" } },
|
||||||
|
{ "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance",
|
||||||
|
"default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
||||||
|
"effects": { "fertilityTolerance": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Гены роста и размножения: бодрость, продолжительность жизни, расселение, репродукция.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor",
|
||||||
|
"default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"],
|
||||||
|
"effects": { "vigor": "value" } },
|
||||||
|
{ "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan",
|
||||||
|
"default": 160, "min": 5, "max": 500, "tags": ["lifecycle"],
|
||||||
|
"effects": { "lifespan": "value" } },
|
||||||
|
{ "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange",
|
||||||
|
"default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"],
|
||||||
|
"effects": { "dispersalRange": "value" } },
|
||||||
|
{ "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval",
|
||||||
|
"default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"],
|
||||||
|
"effects": { "reproduceInterval": "value" } },
|
||||||
|
{ "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination",
|
||||||
|
"default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
||||||
|
"effects": { "selfPollination": "value" } },
|
||||||
|
{ "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate",
|
||||||
|
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
||||||
|
"effects": { "mutationRate": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Общие (абстрактные) гены: база наследования для конкретных генов. Каждый ген задаёт, как
|
||||||
|
// генерируются/мутируют аллели и как ген вкладывается в признаки (effects: имя признака →
|
||||||
|
// формула; value — выраженное значение гена). Конкретные гены лежат рядом по подтемам.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
|
||||||
|
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "Pawn",
|
"type": "Pawn",
|
||||||
|
// Животные.
|
||||||
"defs": [
|
"defs": [
|
||||||
{ "defName": "BaseBeing", "abstract": true, "kind": "being", "sizeCells": 1.1 },
|
|
||||||
{ "defName": "BeingMale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Male_south" },
|
|
||||||
{ "defName": "BeingFemale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Female_south" },
|
|
||||||
{ "defName": "BeingThin", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Thin_south" },
|
|
||||||
{ "defName": "BeingFat", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Fat_south" },
|
|
||||||
{ "defName": "BeingHulk", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Hulk_south" },
|
|
||||||
|
|
||||||
{ "defName": "BaseAnimal", "abstract": true, "kind": "animal" },
|
|
||||||
{ "defName": "Bear", "parent": "BaseAnimal", "label": "pawn.bear", "texture": "things/pawn/animal/bear/Bear_east", "sizeCells": 1.7 },
|
{ "defName": "Bear", "parent": "BaseAnimal", "label": "pawn.bear", "texture": "things/pawn/animal/bear/Bear_east", "sizeCells": 1.7 },
|
||||||
{ "defName": "DeerDoe", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerFemale_east", "sizeCells": 1.4 },
|
{ "defName": "DeerDoe", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerFemale_east", "sizeCells": 1.4 },
|
||||||
{ "defName": "DeerBuck", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerMale_east", "sizeCells": 1.6 },
|
{ "defName": "DeerBuck", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerMale_east", "sizeCells": 1.6 },
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "Pawn",
|
||||||
|
// Жители: варианты тел.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BeingMale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Male_south" },
|
||||||
|
{ "defName": "BeingFemale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Female_south" },
|
||||||
|
{ "defName": "BeingThin", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Thin_south" },
|
||||||
|
{ "defName": "BeingFat", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Fat_south" },
|
||||||
|
{ "defName": "BeingHulk", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Hulk_south" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "Pawn",
|
||||||
|
// Общие (абстрактные) существа: база для жителей и животных.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseBeing", "abstract": true, "kind": "being", "sizeCells": 1.1 },
|
||||||
|
{ "defName": "BaseAnimal", "abstract": true, "kind": "animal" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Ягодный почвопокров луга (еда).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "Strawberry", "parent": "BaseBerryBush", "label": "plant.strawberry", "sizeCells": 1.0, "texture": "things/plant/strawberryplant/StrawberryPlant" },
|
||||||
|
{ "defName": "Raspberry", "parent": "BaseBerryBush", "label": "plant.raspberry", "texture": "things/plant/raspberryplant/raspberrybusha" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Кусты: ягодный подлесок. Патч даёт им древесину при сборе (веточки) — см. Patches/Patches.json.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
|
||||||
|
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
|
||||||
|
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Пустыня: кактусы и агава на песке. Большинство без плодов; сагуаро/агава плодоносят.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "SaguaroCactusA", "parent": "BaseCactus", "label": "plant.saguaro", "sizeCells": 2.2, "trunkRadiusCells": 0.18,
|
||||||
|
"fruitProduct": "ProductBerry", "texture": "things/plant/saguarocactus/SaguaroCactusA" },
|
||||||
|
{ "defName": "SaguaroCactusB", "parent": "BaseCactus", "label": "plant.saguaro", "sizeCells": 2.2, "trunkRadiusCells": 0.18,
|
||||||
|
"fruitProduct": "ProductBerry", "texture": "things/plant/saguarocactus/SaguaroCactusB" },
|
||||||
|
{ "defName": "PebbleCactusA", "parent": "BaseCactus", "sizeCells": 1.0, "texture": "things/plant/pebblecactus/PebbleCactusA" },
|
||||||
|
{ "defName": "PebbleCactusB", "parent": "BaseCactus", "sizeCells": 1.0, "texture": "things/plant/pebblecactus/PebbleCactusB" },
|
||||||
|
{ "defName": "PincushionCactusA", "parent": "BaseCactus", "sizeCells": 0.9, "texture": "things/plant/pincushioncactus/PincushionCactusA" },
|
||||||
|
{ "defName": "AgaveA", "parent": "BaseCactus", "label": "plant.agave", "sizeCells": 1.3, "fruitProduct": "ProductBerry", "texture": "things/plant/agave/AgaveA" },
|
||||||
|
{ "defName": "AgaveB", "parent": "BaseCactus", "label": "plant.agave", "sizeCells": 1.3, "fruitProduct": "ProductBerry", "texture": "things/plant/agave/AgaveB" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Луг: декоративные цветы с быстрым расселением.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "DandelionA", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/Dandelion" },
|
||||||
|
{ "defName": "DandelionB", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/DandelionB" },
|
||||||
|
{ "defName": "DandelionC", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/DandelionC" },
|
||||||
|
{ "defName": "DaylilyA", "parent": "BaseFlower", "label": "plant.daylily", "texture": "things/plant/daylily/DaylilyA" },
|
||||||
|
{ "defName": "DaylilyB", "parent": "BaseFlower", "label": "plant.daylily", "texture": "things/plant/daylily/DaylilyB" },
|
||||||
|
{ "defName": "RoseA", "parent": "BaseFlower", "label": "plant.rose", "sizeCells": 1.1, "texture": "things/plant/rose/RoseA" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Трава: быстрый почвопокров, основной скаттер лугов и леса.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5,
|
||||||
|
"baseMassKg": 0.4,
|
||||||
|
"harvestProduct": "ProductGrass",
|
||||||
|
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8,
|
||||||
|
"coldHardiness": 9, "heatHardiness": 6,
|
||||||
|
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
|
||||||
|
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
|
||||||
|
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
||||||
|
"harvestAmount": 2, "leafHue": 0.36 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "sizeCells": 1.5, "label": "plant.stage.mature" }
|
||||||
|
] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Грибы: подлесок леса, растут в тени крон.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GlowstoolA", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/glowstool/GlowstoolA" },
|
||||||
|
{ "defName": "GlowstoolB", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/glowstool/GlowstoolB" },
|
||||||
|
{ "defName": "TimbershroomA", "parent": "BaseMushroom", "sizeCells": 1.1, "texture": "things/plant/timbershroom/TimbershroomA" },
|
||||||
|
{ "defName": "TimbershroomB", "parent": "BaseMushroom", "sizeCells": 1.1, "texture": "things/plant/timbershroom/TimbershroomB" },
|
||||||
|
{ "defName": "NutrifungusA", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/nutrifungus/NutrifungusA" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Деревья: умеренные (BaseTree), тёплые (BaseWarmTree) и холодные (BaseColdTree) ниши.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
|
||||||
|
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
|
||||||
|
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
|
||||||
|
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
|
||||||
|
{ "defName": "TreeMapleA", "parent": "BaseTree", "label": "plant.maple", "texture": "things/plant/treemaple/TreeMapleA" },
|
||||||
|
{ "defName": "TreeMapleB", "parent": "BaseTree", "label": "plant.maple", "texture": "things/plant/treemaple/TreeMapleB" },
|
||||||
|
{ "defName": "TreePoplarA", "parent": "BaseTree", "label": "plant.poplar", "texture": "things/plant/treepoplar/TreePoplarA" },
|
||||||
|
{ "defName": "TreeTeakA", "parent": "BaseWarmTree", "label": "plant.teak", "texture": "things/plant/treeteak/TreeTeakA" },
|
||||||
|
{ "defName": "TreeTeakB", "parent": "BaseWarmTree", "label": "plant.teak", "texture": "things/plant/treeteak/TreeTeakB" },
|
||||||
|
{ "defName": "TreeCypressA", "parent": "BaseColdTree", "label": "plant.cypress", "texture": "things/plant/treecypress/TreeCypressA" },
|
||||||
|
{ "defName": "TreeCypressB", "parent": "BaseColdTree", "label": "plant.cypress", "texture": "things/plant/treecypress/TreeCypressB" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Общие (абстрактные) растения: базовые геномы по типам ниш. Конкретные виды наследуют их
|
||||||
|
// через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
||||||
|
"baseMassKg": 450,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10,
|
||||||
|
"coldHardiness": 14, "heatHardiness": 12,
|
||||||
|
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
|
||||||
|
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Деревья тёплой и холодной температурных ниш (showcase температурных генов).
|
||||||
|
{ "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
||||||
|
"baseMassKg": 420,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 8, "heatHardiness": 16,
|
||||||
|
"optimalFertility": 1.5, "fertilityTolerance": 0.9, "vigor": 1.1,
|
||||||
|
"lifespan": 150, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.22 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
{ "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26,
|
||||||
|
"baseMassKg": 500,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12,
|
||||||
|
"coldHardiness": 18, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.2, "fertilityTolerance": 1.0, "vigor": 0.9,
|
||||||
|
"lifespan": 220, "dispersalRange": 2, "reproduceInterval": 16, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.12, "spread": 0.08,
|
||||||
|
"fruitYield": 4, "fruitSeason": 2, "harvestAmount": 12, "leafHue": 0.45 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
|
||||||
|
"baseMassKg": 6,
|
||||||
|
"fruitProduct": "ProductBerry",
|
||||||
|
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 10, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
|
||||||
|
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
||||||
|
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
|
||||||
|
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы.
|
||||||
|
{ "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0,
|
||||||
|
"baseMassKg": 35,
|
||||||
|
"harvestProduct": "ProductFiber",
|
||||||
|
"genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8,
|
||||||
|
"coldHardiness": 20, "heatHardiness": 18,
|
||||||
|
"optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5,
|
||||||
|
"lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
|
||||||
|
"thorns": 0.6, "palatability": 0.5 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно
|
||||||
|
// (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение.
|
||||||
|
{ "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0,
|
||||||
|
"baseMassKg": 0.3,
|
||||||
|
"harvestProduct": "ProductMushroom",
|
||||||
|
"genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11,
|
||||||
|
"coldHardiness": 12, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2,
|
||||||
|
"lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9,
|
||||||
|
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
||||||
|
"fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5,
|
||||||
|
"toxicity": 0.5, "palatability": 0.4 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда).
|
||||||
|
{ "defName": "BaseFlower", "abstract": true, "label": "plant.flower", "sizeCells": 1.0, "trunkRadiusCells": 0,
|
||||||
|
"baseMassKg": 0.15,
|
||||||
|
"genome": { "optimalLight": 0.9, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 8, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4,
|
||||||
|
"lifespan": 18, "dispersalRange": 4, "reproduceInterval": 3, "selfPollination": 0.85,
|
||||||
|
"mutationRate": 0.08, "variantChance": 0.3, "spread": 0.1,
|
||||||
|
"fruitYield": 0, "fruitSeason": 0, "harvestAmount": 0, "leafHue": 0.2 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0,
|
||||||
|
"baseMassKg": 5,
|
||||||
|
"fruitProduct": "ProductBerry",
|
||||||
|
"genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 10, "heatHardiness": 7,
|
||||||
|
"optimalFertility": 1.2, "fertilityTolerance": 1.0, "vigor": 1.1,
|
||||||
|
"lifespan": 40, "dispersalRange": 2, "reproduceInterval": 6, "selfPollination": 0.6,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
||||||
|
"fruitYield": 4, "fruitSeason": 2, "harvestAmount": 0, "leafHue": 0.33 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 3, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "Product",
|
||||||
|
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
|
||||||
|
// количество задаётся генами (harvestAmount / fruitYield).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "ProductWood", "label": "product.wood", "kind": "material", "massKg": 1.0 },
|
||||||
|
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material", "massKg": 0.1 },
|
||||||
|
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material", "massKg": 0.2 },
|
||||||
|
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food", "massKg": 0.05 },
|
||||||
|
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food", "massKg": 0.05 },
|
||||||
|
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food", "massKg": 0.1 },
|
||||||
|
|
||||||
|
// Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже.
|
||||||
|
{ "defName": "ProductMeat", "label": "product.meat", "kind": "food", "massKg": 0.5 },
|
||||||
|
{ "defName": "ProductBone", "label": "product.bone", "kind": "material", "massKg": 0.3 },
|
||||||
|
{ "defName": "ProductLeather", "label": "product.leather", "kind": "material", "massKg": 0.4 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"type": "Terrain",
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
|
||||||
|
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
|
||||||
|
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.46, "color": [237, 201, 120],
|
||||||
|
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4,
|
||||||
|
"scatter": [
|
||||||
|
{ "chance": 0.05, "options": ["SaguaroCactusA", "SaguaroCactusB"] },
|
||||||
|
{ "chance": 0.10, "options": ["PebbleCactusA", "PebbleCactusB", "PincushionCactusA"] },
|
||||||
|
{ "chance": 0.04, "options": ["AgaveA", "AgaveB"] }
|
||||||
|
] },
|
||||||
|
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.72, "color": [90, 160, 70],
|
||||||
|
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
|
||||||
|
"scatter": [
|
||||||
|
{ "chance": 1.0, "options": ["GrassA"] },
|
||||||
|
{ "chance": 0.5, "options": ["GrassA"] },
|
||||||
|
{ "chance": 0.22, "options": ["BushA", "BushB", "BushC"] },
|
||||||
|
{ "chance": 0.20, "options": ["Strawberry", "Raspberry"] },
|
||||||
|
{ "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] }
|
||||||
|
] },
|
||||||
|
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
||||||
|
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
||||||
|
"scatter": [
|
||||||
|
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA",
|
||||||
|
"TreeMapleA", "TreeMapleB", "TreePoplarA", "TreeCypressA", "TreeCypressB", "TreeTeakA", "TreeTeakB"] },
|
||||||
|
{ "chance": 0.6, "options": ["GrassA"] },
|
||||||
|
{ "chance": 0.16, "options": ["BushA", "BushB", "BushC"] },
|
||||||
|
{ "chance": 0.14, "options": ["Strawberry", "Raspberry"] },
|
||||||
|
{ "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] }
|
||||||
|
] },
|
||||||
|
{ "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 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Gene",
|
|
||||||
// Организм-агностичные гены: каждый ген задаёт, как генерируются/мутируют аллели и как ген
|
|
||||||
// вкладывается в признаки (effects: имя признака → формула; value — выраженное значение гена).
|
|
||||||
// Этот набор покрывает измерения генома растения (его используют системы роста/жизненного цикла).
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
|
|
||||||
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 },
|
|
||||||
|
|
||||||
{ "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight",
|
|
||||||
"default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"],
|
|
||||||
"effects": { "optimalLight": "value" } },
|
|
||||||
{ "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance",
|
|
||||||
"default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"],
|
|
||||||
"effects": { "lightTolerance": "value" } },
|
|
||||||
{ "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature",
|
|
||||||
"default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"],
|
|
||||||
"effects": { "optimalTemperature": "value" } },
|
|
||||||
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
|
|
||||||
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
|
|
||||||
"effects": { "temperatureTolerance": "value" } },
|
|
||||||
{ "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
|
|
||||||
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
|
||||||
"effects": { "optimalFertility": "value" } },
|
|
||||||
{ "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance",
|
|
||||||
"default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
|
||||||
"effects": { "fertilityTolerance": "value" } },
|
|
||||||
|
|
||||||
{ "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor",
|
|
||||||
"default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"],
|
|
||||||
"effects": { "vigor": "value" } },
|
|
||||||
{ "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan",
|
|
||||||
"default": 160, "min": 5, "max": 500, "tags": ["lifecycle"],
|
|
||||||
"effects": { "lifespan": "value" } },
|
|
||||||
{ "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange",
|
|
||||||
"default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"],
|
|
||||||
"effects": { "dispersalRange": "value" } },
|
|
||||||
{ "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval",
|
|
||||||
"default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"],
|
|
||||||
"effects": { "reproduceInterval": "value" } },
|
|
||||||
{ "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination",
|
|
||||||
"default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
|
||||||
"effects": { "selfPollination": "value" } },
|
|
||||||
{ "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate",
|
|
||||||
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
|
||||||
"effects": { "mutationRate": "value" } },
|
|
||||||
|
|
||||||
// --- Контент (фаза G4): плодоношение, добыча, цвет ---
|
|
||||||
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
|
|
||||||
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
|
|
||||||
"effects": { "fruitYield": "value" } },
|
|
||||||
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
|
|
||||||
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
|
|
||||||
"effects": { "fruitSeason": "value" } },
|
|
||||||
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
|
|
||||||
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
|
|
||||||
"effects": { "harvestAmount": "value" } },
|
|
||||||
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
|
|
||||||
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
|
|
||||||
"effects": { "leafHue": "value" } },
|
|
||||||
|
|
||||||
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
|
|
||||||
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
|
|
||||||
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
|
|
||||||
"default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"],
|
|
||||||
"effects": { "hardiness": "gsum('Gene.*Tolerance')" } },
|
|
||||||
|
|
||||||
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
|
||||||
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
|
||||||
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
|
||||||
"effects": { "variant": "value" } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Plant",
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
|
||||||
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
|
||||||
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
|
|
||||||
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
|
|
||||||
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
|
||||||
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
|
||||||
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
|
|
||||||
"stages": [
|
|
||||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
|
||||||
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
|
||||||
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
|
||||||
] },
|
|
||||||
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
|
|
||||||
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
|
|
||||||
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
|
|
||||||
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
|
|
||||||
|
|
||||||
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
|
|
||||||
"harvestProduct": "ProductGrass",
|
|
||||||
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
|
|
||||||
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
|
|
||||||
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
|
|
||||||
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
|
||||||
"harvestAmount": 2, "leafHue": 0.36 },
|
|
||||||
"stages": [
|
|
||||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
|
|
||||||
{ "sizeCells": 1.2, "label": "plant.stage.mature" }
|
|
||||||
] },
|
|
||||||
|
|
||||||
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
|
|
||||||
"fruitProduct": "ProductBerry",
|
|
||||||
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13,
|
|
||||||
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
|
|
||||||
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
|
|
||||||
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
|
||||||
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
|
|
||||||
"stages": [
|
|
||||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
|
|
||||||
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
|
|
||||||
] },
|
|
||||||
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
|
|
||||||
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
|
|
||||||
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Product",
|
|
||||||
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
|
|
||||||
// количество задаётся генами (harvestAmount / fruitYield).
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
|
|
||||||
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
|
|
||||||
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
|
||||||
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Terrain",
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
|
|
||||||
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
|
|
||||||
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120],
|
|
||||||
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4 },
|
|
||||||
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70],
|
|
||||||
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
|
|
||||||
"scatter": [
|
|
||||||
{ "chance": 0.35, "options": ["GrassA"] },
|
|
||||||
{ "chance": 0.07, "options": ["BushA", "BushB", "BushC"] }
|
|
||||||
] },
|
|
||||||
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
|
||||||
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
|
||||||
"scatter": [
|
|
||||||
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
|
|
||||||
] },
|
|
||||||
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
|
|
||||||
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,84 @@
|
|||||||
"season.summer": "summer",
|
"season.summer": "summer",
|
||||||
"season.autumn": "autumn",
|
"season.autumn": "autumn",
|
||||||
"season.winter": "winter",
|
"season.winter": "winter",
|
||||||
"hud.controls": "WASD — camera, wheel — zoom, ` — console, F1 — inspector",
|
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
|
||||||
|
"inspect.tab.overview": "Overview",
|
||||||
|
"inspect.tab.genes": "Genes",
|
||||||
|
"inspect.tab.products": "Products",
|
||||||
|
"inspect.tab.needs": "Needs",
|
||||||
|
"inspect.tab.skills": "Skills",
|
||||||
|
"inspect.tab.health": "Health",
|
||||||
|
"inspect.tab.mood": "Mood",
|
||||||
|
"inspect.stage": "Stage: {0} ({1}/{2})",
|
||||||
|
"inspect.growing": "Growth: {0:0}% to next stage",
|
||||||
|
"inspect.mature": "Fully grown",
|
||||||
|
"inspect.age": "Age: {0:0.0} d · lives to {1:0}",
|
||||||
|
"inspect.mass": "Weight: {0:0.#} kg · carries up to {1:0.#} kg",
|
||||||
|
"inspect.plantmass": "Weight: {0:0.##} kg",
|
||||||
|
"inspect.state": "State: {0}",
|
||||||
|
"inspect.state.growing": "growing",
|
||||||
|
"inspect.state.mature": "mature",
|
||||||
|
"inspect.state.dormantcold": "dormant — too cold",
|
||||||
|
"inspect.state.dormanthot": "dormant — too hot",
|
||||||
|
"inspect.stress": "Stress: {0:0.0} d to death",
|
||||||
|
"inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
|
||||||
|
"inspect.genes": "— Genes —",
|
||||||
|
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d",
|
||||||
|
"inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}",
|
||||||
|
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
|
||||||
|
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
|
||||||
|
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
|
||||||
|
"inspect.gene.defense": "Defense: toxin {0:0.00} · thorns {1:0.00}",
|
||||||
|
"inspect.gene.variant": "Morph: recessive variant",
|
||||||
|
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
|
||||||
|
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
|
||||||
|
"inspect.barren": "Bears no fruit",
|
||||||
|
"inspect.sex.male": "male",
|
||||||
|
"inspect.sex.female": "female",
|
||||||
|
"inspect.animal.sex": "Sex: {0} · generation {1}",
|
||||||
|
"inspect.animal.pregnant": "Pregnant: {0:0.0} d to birth",
|
||||||
|
"inspect.needline": "{0}: {1:0}%",
|
||||||
|
"inspect.skillline": "{0}: {1:0}% {2}",
|
||||||
|
"skill.foraging": "foraging",
|
||||||
|
"skill.hunting": "hunting",
|
||||||
|
"skill.evasion": "evasion",
|
||||||
|
"inspect.health.blood": "Blood: {0:0}% · pain: {1:0}%",
|
||||||
|
"inspect.health.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.talking": "talking",
|
||||||
|
"cap.eating": "eating",
|
||||||
|
"cap.breathing": "breathing",
|
||||||
|
"cap.bloodpumping": "blood pumping",
|
||||||
|
"cap.bloodfiltration": "blood filtration",
|
||||||
|
"cap.digestion": "digestion",
|
||||||
|
"hediff.rut": "rut",
|
||||||
|
"hediff.fever": "fever",
|
||||||
|
"hediff.bleeding": "bleeding",
|
||||||
|
"hediff.poisoned": "poisoned",
|
||||||
|
"hediff.hypothermia": "hypothermia",
|
||||||
|
"hediff.heatstroke": "heatstroke",
|
||||||
|
"hediff.infection": "infection",
|
||||||
|
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
||||||
"hud.paused": "PAUSED",
|
"hud.paused": "PAUSED",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
"menu.subtitle": "a god-game: minimal graphics, deep simulation",
|
"menu.subtitle": "a god-game: minimal graphics, deep simulation",
|
||||||
@@ -15,6 +92,7 @@
|
|||||||
"menu.settings": "Settings",
|
"menu.settings": "Settings",
|
||||||
"menu.credits": "Credits",
|
"menu.credits": "Credits",
|
||||||
"menu.quit": "Quit",
|
"menu.quit": "Quit",
|
||||||
|
"loading.world": "Generating world",
|
||||||
"newworld.title": "New World",
|
"newworld.title": "New World",
|
||||||
"newworld.name": "Name",
|
"newworld.name": "Name",
|
||||||
"newworld.defaultname": "New World",
|
"newworld.defaultname": "New World",
|
||||||
@@ -35,6 +113,17 @@
|
|||||||
"settings.vsync": "Vertical sync",
|
"settings.vsync": "Vertical sync",
|
||||||
"settings.resolution": "Resolution",
|
"settings.resolution": "Resolution",
|
||||||
"settings.volume": "Volume",
|
"settings.volume": "Volume",
|
||||||
|
"settings.uiscale": "UI scale",
|
||||||
|
"settings.devmode": "Developer mode",
|
||||||
|
"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",
|
||||||
@@ -63,14 +152,31 @@
|
|||||||
"plant.pine": "pine",
|
"plant.pine": "pine",
|
||||||
"plant.grass": "grass tuft",
|
"plant.grass": "grass tuft",
|
||||||
"plant.bush": "bush",
|
"plant.bush": "bush",
|
||||||
|
"plant.maple": "maple",
|
||||||
|
"plant.poplar": "poplar",
|
||||||
|
"plant.teak": "teak",
|
||||||
|
"plant.cypress": "cypress",
|
||||||
|
"plant.cactus": "cactus",
|
||||||
|
"plant.saguaro": "saguaro",
|
||||||
|
"plant.agave": "agave",
|
||||||
|
"plant.mushroom": "mushroom",
|
||||||
|
"plant.flower": "flower",
|
||||||
|
"plant.dandelion": "dandelion",
|
||||||
|
"plant.daylily": "daylily",
|
||||||
|
"plant.rose": "rose",
|
||||||
|
"plant.berrybush": "berry bush",
|
||||||
|
"plant.strawberry": "strawberry",
|
||||||
|
"plant.raspberry": "raspberry",
|
||||||
"plant.stage.seedling": "seedling",
|
"plant.stage.seedling": "seedling",
|
||||||
"plant.stage.sprout": "sprout",
|
"plant.stage.sprout": "sprout",
|
||||||
"plant.stage.sapling": "sapling",
|
"plant.stage.sapling": "sapling",
|
||||||
"plant.stage.mature": "mature",
|
"plant.stage.mature": "mature",
|
||||||
"product.wood": "wood",
|
"product.wood": "wood",
|
||||||
"product.grass": "grass",
|
"product.grass": "grass",
|
||||||
|
"product.fiber": "fiber",
|
||||||
"product.berry": "berries",
|
"product.berry": "berries",
|
||||||
"product.acorn": "acorn",
|
"product.acorn": "acorn",
|
||||||
|
"product.mushroom": "mushrooms",
|
||||||
"pawn.being": "being",
|
"pawn.being": "being",
|
||||||
"pawn.bear": "bear",
|
"pawn.bear": "bear",
|
||||||
"pawn.deer": "deer",
|
"pawn.deer": "deer",
|
||||||
@@ -78,11 +184,16 @@
|
|||||||
"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",
|
||||||
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
||||||
"net.connecting": "connecting…",
|
"net.connecting": "connecting…",
|
||||||
"net.connected": "connected",
|
"net.connected": "connected",
|
||||||
"net.failed": "connection failed",
|
"net.failed": "connection failed",
|
||||||
"net.lost": "connection lost"
|
"net.lost": "connection lost",
|
||||||
|
"net.reconnecting": "reconnecting…"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,84 @@
|
|||||||
"season.summer": "лето",
|
"season.summer": "лето",
|
||||||
"season.autumn": "осень",
|
"season.autumn": "осень",
|
||||||
"season.winter": "зима",
|
"season.winter": "зима",
|
||||||
"hud.controls": "WASD — камера, колесо — зум, ` — консоль, F1 — инспектор",
|
"hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
|
||||||
|
"inspect.tab.overview": "Обзор",
|
||||||
|
"inspect.tab.genes": "Гены",
|
||||||
|
"inspect.tab.products": "Продукты",
|
||||||
|
"inspect.tab.needs": "Нужды",
|
||||||
|
"inspect.tab.skills": "Навыки",
|
||||||
|
"inspect.tab.health": "Здоровье",
|
||||||
|
"inspect.tab.mood": "Настроение",
|
||||||
|
"inspect.stage": "Стадия: {0} ({1}/{2})",
|
||||||
|
"inspect.growing": "Рост: {0:0}% до следующей стадии",
|
||||||
|
"inspect.mature": "Полностью выросло",
|
||||||
|
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
|
||||||
|
"inspect.mass": "Вес: {0:0.#} кг · несёт до {1:0.#} кг",
|
||||||
|
"inspect.plantmass": "Вес: {0:0.##} кг",
|
||||||
|
"inspect.state": "Состояние: {0}",
|
||||||
|
"inspect.state.growing": "растёт",
|
||||||
|
"inspect.state.mature": "созрело",
|
||||||
|
"inspect.state.dormantcold": "покой — слишком холодно",
|
||||||
|
"inspect.state.dormanthot": "покой — слишком жарко",
|
||||||
|
"inspect.stress": "Стресс: {0:0.0} дн до гибели",
|
||||||
|
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
|
||||||
|
"inspect.genes": "— Гены —",
|
||||||
|
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
|
||||||
|
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
|
||||||
|
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
|
||||||
|
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
|
||||||
|
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
|
||||||
|
"inspect.gene.defense": "Защита: яд {0:0.00} · шипы {1:0.00}",
|
||||||
|
"inspect.gene.variant": "Морфа: рецессивный вариант",
|
||||||
|
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
|
||||||
|
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
|
||||||
|
"inspect.barren": "Не плодоносит",
|
||||||
|
"inspect.sex.male": "самец",
|
||||||
|
"inspect.sex.female": "самка",
|
||||||
|
"inspect.animal.sex": "Пол: {0} · поколение {1}",
|
||||||
|
"inspect.animal.pregnant": "Беременна: {0:0.0} дн до родов",
|
||||||
|
"inspect.needline": "{0}: {1:0}%",
|
||||||
|
"inspect.skillline": "{0}: {1:0}% {2}",
|
||||||
|
"skill.foraging": "добыча корма",
|
||||||
|
"skill.hunting": "охота",
|
||||||
|
"skill.evasion": "уклонение",
|
||||||
|
"inspect.health.blood": "Кровь: {0:0}% · боль: {1:0}%",
|
||||||
|
"inspect.health.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.talking": "речь",
|
||||||
|
"cap.eating": "питание",
|
||||||
|
"cap.breathing": "дыхание",
|
||||||
|
"cap.bloodpumping": "кровоснабжение",
|
||||||
|
"cap.bloodfiltration": "фильтрация крови",
|
||||||
|
"cap.digestion": "пищеварение",
|
||||||
|
"hediff.rut": "гон",
|
||||||
|
"hediff.fever": "лихорадка",
|
||||||
|
"hediff.bleeding": "кровотечение",
|
||||||
|
"hediff.poisoned": "отравление",
|
||||||
|
"hediff.hypothermia": "переохлаждение",
|
||||||
|
"hediff.heatstroke": "тепловой удар",
|
||||||
|
"hediff.infection": "заражение",
|
||||||
|
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
||||||
"hud.paused": "ПАУЗА",
|
"hud.paused": "ПАУЗА",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
|
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
|
||||||
@@ -15,6 +92,7 @@
|
|||||||
"menu.settings": "Настройки",
|
"menu.settings": "Настройки",
|
||||||
"menu.credits": "Авторы",
|
"menu.credits": "Авторы",
|
||||||
"menu.quit": "Выход",
|
"menu.quit": "Выход",
|
||||||
|
"loading.world": "Создание мира",
|
||||||
"newworld.title": "Новый мир",
|
"newworld.title": "Новый мир",
|
||||||
"newworld.name": "Название",
|
"newworld.name": "Название",
|
||||||
"newworld.defaultname": "Новый мир",
|
"newworld.defaultname": "Новый мир",
|
||||||
@@ -35,6 +113,17 @@
|
|||||||
"settings.vsync": "Вертикальная синхронизация",
|
"settings.vsync": "Вертикальная синхронизация",
|
||||||
"settings.resolution": "Разрешение",
|
"settings.resolution": "Разрешение",
|
||||||
"settings.volume": "Громкость",
|
"settings.volume": "Громкость",
|
||||||
|
"settings.uiscale": "Масштаб UI",
|
||||||
|
"settings.devmode": "Режим разработчика",
|
||||||
|
"dev.title": "Дев-спавнер",
|
||||||
|
"dev.hint": "Выбери пункт · F9 — скрыть",
|
||||||
|
"dev.armed": "В руке: {0} · ЛКМ — спавн, ПКМ — снять",
|
||||||
|
"dev.tools": "Инструменты",
|
||||||
|
"dev.damage": "Ранить выбранного",
|
||||||
|
"dev.kill": "Убить выбранного",
|
||||||
|
"dev.infect": "Заразить выбранного",
|
||||||
|
"dev.animals": "Животные",
|
||||||
|
"dev.plants": "Растения",
|
||||||
"settings.apply": "Применить",
|
"settings.apply": "Применить",
|
||||||
"settings.back": "Назад",
|
"settings.back": "Назад",
|
||||||
"pause.title": "Пауза",
|
"pause.title": "Пауза",
|
||||||
@@ -63,14 +152,31 @@
|
|||||||
"plant.pine": "сосна",
|
"plant.pine": "сосна",
|
||||||
"plant.grass": "пучок травы",
|
"plant.grass": "пучок травы",
|
||||||
"plant.bush": "куст",
|
"plant.bush": "куст",
|
||||||
|
"plant.maple": "клён",
|
||||||
|
"plant.poplar": "тополь",
|
||||||
|
"plant.teak": "тик",
|
||||||
|
"plant.cypress": "кипарис",
|
||||||
|
"plant.cactus": "кактус",
|
||||||
|
"plant.saguaro": "сагуаро",
|
||||||
|
"plant.agave": "агава",
|
||||||
|
"plant.mushroom": "гриб",
|
||||||
|
"plant.flower": "цветок",
|
||||||
|
"plant.dandelion": "одуванчик",
|
||||||
|
"plant.daylily": "лилейник",
|
||||||
|
"plant.rose": "роза",
|
||||||
|
"plant.berrybush": "ягодный куст",
|
||||||
|
"plant.strawberry": "земляника",
|
||||||
|
"plant.raspberry": "малина",
|
||||||
"plant.stage.seedling": "росток",
|
"plant.stage.seedling": "росток",
|
||||||
"plant.stage.sprout": "всходы",
|
"plant.stage.sprout": "всходы",
|
||||||
"plant.stage.sapling": "саженец",
|
"plant.stage.sapling": "саженец",
|
||||||
"plant.stage.mature": "взрослое",
|
"plant.stage.mature": "взрослое",
|
||||||
"product.wood": "древесина",
|
"product.wood": "древесина",
|
||||||
"product.grass": "трава",
|
"product.grass": "трава",
|
||||||
|
"product.fiber": "волокно",
|
||||||
"product.berry": "ягоды",
|
"product.berry": "ягоды",
|
||||||
"product.acorn": "жёлудь",
|
"product.acorn": "жёлудь",
|
||||||
|
"product.mushroom": "грибы",
|
||||||
"pawn.being": "житель",
|
"pawn.being": "житель",
|
||||||
"pawn.bear": "медведь",
|
"pawn.bear": "медведь",
|
||||||
"pawn.deer": "олень",
|
"pawn.deer": "олень",
|
||||||
@@ -78,11 +184,16 @@
|
|||||||
"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": "напилась вволю",
|
||||||
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||||
"net.connecting": "подключение…",
|
"net.connecting": "подключение…",
|
||||||
"net.connected": "подключено",
|
"net.connected": "подключено",
|
||||||
"net.failed": "не удалось подключиться",
|
"net.failed": "не удалось подключиться",
|
||||||
"net.lost": "соединение потеряно"
|
"net.lost": "соединение потеряно",
|
||||||
|
"net.reconnecting": "переподключение…"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ Mods/<Имя>/
|
|||||||
вид — `being`/`animal`). CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип
|
вид — `being`/`animal`). CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип
|
||||||
дефа = новый класс + `RegisterType` в `GameContent.Load`.
|
дефа = новый класс + `RegisterType` в `GameContent.Load`.
|
||||||
|
|
||||||
|
`Defs/` сканируется рекурсивно (`Defs/**/*.json`), и имя файла ни на что не влияет —
|
||||||
|
важны только `type` и `defName`. Поэтому контент Core разложен по подпапкам для удобства
|
||||||
|
поиска: одна папка на тип дефа, внутри — общие (абстрактные) дефы в `_Bases.json` и
|
||||||
|
конкретные, разбитые по семействам:
|
||||||
|
|
||||||
|
```
|
||||||
|
Defs/
|
||||||
|
Genes/ _Bases.json, Environment.json, Growth.json, Content.json
|
||||||
|
Plants/ _Bases.json, Trees.json, Bushes.json, Grass.json, Cacti.json,
|
||||||
|
Mushrooms.json, Flowers.json, Berries.json
|
||||||
|
Pawns/ _Bases.json, Beings.json, Animals.json
|
||||||
|
Terrain/ Terrain.json
|
||||||
|
Products/ Products.json
|
||||||
|
Patches/ Patches.json
|
||||||
|
WorldPresets/ WorldPresets.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`parent` разрешается после слияния всех файлов, так что ссылка на родителя из другого
|
||||||
|
файла/папки (`TreeOakA` → `BaseTree`) работает независимо от расположения.
|
||||||
|
|
||||||
## Локализация
|
## Локализация
|
||||||
|
|
||||||
`Languages/<код>/*.json` — плоские словари «ключ → строка»:
|
`Languages/<код>/*.json` — плоские словари «ключ → строка»:
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
# Roadmap LittleSim
|
|
||||||
|
|
||||||
Этап закрыт, когда механика работает, наблюдаема (HUD/консоль/визуал)
|
|
||||||
и управляется командой консоли.
|
|
||||||
|
|
||||||
## Этап 0 — Каркас ✅
|
|
||||||
- [x] Репозиторий с движком-сабмодулем, общий solution
|
|
||||||
- [x] Генерация рельефа по сиду (вода/песок/трава/лес/горы)
|
|
||||||
- [x] Жители-заглушки (блуждание), камера бога, HUD, консоль (`regen`)
|
|
||||||
|
|
||||||
## Этап 1 — Жители как агенты
|
|
||||||
- [~] Потребности: голод, усталость (тикают со временем) — усталость/энергия готова (`PawnNeeds`), голод впереди
|
|
||||||
- [~] Utility-выбор действия: искать еду / отдыхать / бродить — каркас на движке (`MrGameEng.AI`), выбор «отдыхать/бродить» работает; «искать еду» впереди
|
|
||||||
- [ ] Еда на траве/в лесу: растёт, собирается, истощается
|
|
||||||
- [ ] Смерть от голода; визуальное состояние жителя (цвет/прозрачность)
|
|
||||||
- [ ] Консоль: `spawn <n>`, `feed`, `starve` для тестов
|
|
||||||
|
|
||||||
## Этап 2 — Общество
|
|
||||||
- [ ] Дома и поселения (житель несёт еду домой, спит дома)
|
|
||||||
- [ ] Размножение при сытости; рост населения
|
|
||||||
- [ ] Простые профессии: собиратель, строитель
|
|
||||||
|
|
||||||
## Этап 3 — Бог
|
|
||||||
- [ ] Вера как ресурс (генерируется жителями)
|
|
||||||
- [ ] Первые силы: дождь (ускоряет рост еды), молния (поджигает лес)
|
|
||||||
- [ ] Терраформинг клеток
|
|
||||||
|
|
||||||
## Этап 4 — Экология и время
|
|
||||||
- [ ] День/ночь, сезоны
|
|
||||||
- [ ] Восстановление/деградация ресурсов, пожары
|
|
||||||
- [ ] Миграции при нехватке ресурсов
|
|
||||||
|
|
||||||
## Бэклог
|
|
||||||
- Сохранение/загрузка мира (сериализация Friflo)
|
|
||||||
- Фиксированный тик симуляции, независимый от рендера
|
|
||||||
- Исторические графики (население, еда) в дев-оверлее
|
|
||||||
@@ -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,84 +0,0 @@
|
|||||||
# Растения как экосистема
|
|
||||||
|
|
||||||
Дизайн системы растений: рост зависит от среды, среда описана факторами, реакция растения на
|
|
||||||
факторы задаётся **генами**, а размножение передаёт гены потомкам с мутацией — появляется
|
|
||||||
естественный отбор. Документ — единый ориентир для фаз A–D; детали каждой фазы уточняются её
|
|
||||||
отдельным планом.
|
|
||||||
|
|
||||||
## Модель роста
|
|
||||||
|
|
||||||
Скорость роста растения в данный момент:
|
|
||||||
|
|
||||||
```
|
|
||||||
скорость = база × пригодность(свет) × пригодность(температура) × пригодность(плодородность)
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждая «пригодность» — число в [0..1] из кривой отклика (движковый `ResponseCurve`, модуль
|
|
||||||
`MrGameEng.AI`) с **оптимумом** и **шириной толерантности**, которые берутся из генов растения.
|
|
||||||
Растение вне своих оптимумов растёт медленно или стоит; в своих — быстро. Так все факторы среды
|
|
||||||
сводятся к одному механизму, а различие видов/особей — целиком в генах.
|
|
||||||
|
|
||||||
Фактор «возраст/стадия» остаётся как сейчас (`PlantGrowthStage` в дефах): пригодности умножают
|
|
||||||
прирост `AgeDays`, стадии переключают спрайт/размер по накопленному возрасту.
|
|
||||||
|
|
||||||
## Факторы среды
|
|
||||||
|
|
||||||
- **Свет** — амбиент день/ночь (`MrGameEng.Lighting.DayNight`), сэмплируемый в точке
|
|
||||||
(`SampleAt(world)`); глобальный в фазе A, локальный (с тенями) в фазе D.
|
|
||||||
- **Температура** — непрерывная, из климата (`MrGameEng.Core.Climate`): сезонный синус по дню года
|
|
||||||
+ суточные колебания (холоднее ночью).
|
|
||||||
- **Плодородность** — пока константа типа террейна (`TerrainDef.Fertility`); позже возможна
|
|
||||||
по-клеточная почва с истощением/восстановлением.
|
|
||||||
|
|
||||||
## Гены (диплоидный геном, гибридная экспрессия)
|
|
||||||
|
|
||||||
Каждое растение несёт **геном**: по паре аллелей на ген. Экспрессия гибридная:
|
|
||||||
|
|
||||||
- **Числовые гены** (оптимум света, толерантность к свету, оптимум температуры, толерантность,
|
|
||||||
потребность в плодородности, макс. размер, базовая скорость, продолжительность жизни, дальность
|
|
||||||
расселения, плодовитость, темп мутаций, способность к самоопылению) — фенотип = **смешение**
|
|
||||||
(среднее двух аллелей). Реализм отбора: промежуточные значения, плавный дрейф.
|
|
||||||
- **Дискретные гены** (например, цвет/морфа, наличие признака) — **доминант/рецессив**: рецессивный
|
|
||||||
фенотип виден только в гомозиготе (aa).
|
|
||||||
|
|
||||||
Наследование (мейоз): каждый родитель отдаёт по одному случайному аллелю на ген; потомок получает
|
|
||||||
по одному от каждого. Затем — мутация (с вероятностью из гена темпа мутаций сдвигаем числовой
|
|
||||||
аллель/переключаем дискретный). Всё детерминировано сидом мира.
|
|
||||||
|
|
||||||
Стартовая популяция: геномы генерируются из сида (вид задаёт базовый геном и разброс аллелей в
|
|
||||||
дефе; особи отклоняются).
|
|
||||||
|
|
||||||
## Размножение (полуреалистичное)
|
|
||||||
|
|
||||||
- **Перекрёстное опыление (приоритет):** зрелое растение ищет зрелого соседа того же вида в радиусе
|
|
||||||
(ген дальности; запрос через spatial-hash движка). Есть партнёр → семя с геномом от двух
|
|
||||||
родителей (Мендель + мутация).
|
|
||||||
- **Самоопыление (запас):** нет партнёра и ген «способность к самоопылению» позволяет → семя из
|
|
||||||
одного родителя (само-скрещивание аллелей + мутация).
|
|
||||||
- **Расселение:** семя падает в клетку в радиусе расселения; прорастает, если клетка подходит
|
|
||||||
(суша, не занято сверх лимита, пригодность не нулевая).
|
|
||||||
- **Лимит плотности:** не больше N растений на клетку/площадь — иначе взрыв численности.
|
|
||||||
- **Старение/смерть:** возраст превышает ген жизни → смерть; или длительный стресс (пригодность
|
|
||||||
около нуля) → гибель. Смерть освобождает место — баланс с размножением и основа отбора.
|
|
||||||
|
|
||||||
## Производительность и сейв
|
|
||||||
|
|
||||||
- Представление — **по сущности на растение** (видно в рендере и ECS-инспекторе, индивидуальные
|
|
||||||
гены). Защита от взрыва: лимит плотности, смерть, разумные радиусы, проверки размножения по
|
|
||||||
таймеру/сезону (не каждый кадр на каждое растение), spatial-hash для соседей.
|
|
||||||
- **Персист:** геномы, позиции, возраст и стадия растений сохраняются в `WorldSave` (мир больше не
|
|
||||||
воспроизводится только из сида — эволюция переживает загрузку).
|
|
||||||
- **Детерминизм:** мутации/расселение/опыление — на сидированном `Random`, как в остальной симуляции.
|
|
||||||
|
|
||||||
## Фазы реализации
|
|
||||||
|
|
||||||
- **A — климат и день/ночь (движок).** `Climate` (температура/сезон поверх `Calendar`),
|
|
||||||
`DayNight` (амбиент, `Renderer2D.AmbientLight` на World-слоях, сэмплируемый `SampleAt`),
|
|
||||||
затемнение мира ночью, HUD: сезон/температура/время. Фундамент для влияния среды на рост.
|
|
||||||
- **B — гены и рост по пригодностям (игра).** Компонент `PlantGenome`, расчёт пригодностей
|
|
||||||
(свет из `DayNight.SampleAt`, температура из `Climate`, плодородность из террейна), замена
|
|
||||||
плоского `GrowthRate`. Геном виден/правится в инспекторе.
|
|
||||||
- **C — размножение и смерть (игра).** Перекрёстное/самоопыление, расселение, лимит плотности,
|
|
||||||
старение/стресс-смерть, персист популяции в сейв.
|
|
||||||
- **D — полноценное 2D-освещение (движок).** Точечные источники + тени (окклюзия от препятствий);
|
|
||||||
`DayNight.SampleAt` начинает возвращать локальную освещённость, и затенённые растения растут хуже.
|
|
||||||
+1
-1
Submodule engine updated: d044cafad9...96e19c7c61
+3
-1
@@ -9,7 +9,9 @@ $Project = Join-Path $Root "src\LittleSim\LittleSim.csproj"
|
|||||||
|
|
||||||
Push-Location $Root
|
Push-Location $Root
|
||||||
try {
|
try {
|
||||||
dotnet build LittleSim.sln -c $Configuration
|
# Только игровой проект (тянет за собой нужные движковые библиотеки) — не вся солюшн с
|
||||||
|
# тестами, сервером и WASM-клиентом (его emscripten-сборка занимает минуты).
|
||||||
|
dotnet build $Project -c $Configuration
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -18,5 +18,7 @@ done
|
|||||||
|
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
dotnet build LittleSim.sln -c "$CONFIGURATION"
|
# Только игровой проект (тянет за собой нужные движковые библиотеки) — не вся солюшн с
|
||||||
|
# тестами, сервером и WASM-клиентом (его emscripten-сборка занимает минуты).
|
||||||
|
dotnet build src/LittleSim/LittleSim.csproj -c "$CONFIGURATION"
|
||||||
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
|
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
|
||||||
|
|||||||
@@ -16,24 +16,33 @@ public class LittleSimWebGame : Game
|
|||||||
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
|
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
|
||||||
public EngineContext Context { get; } = new EngineContext();
|
public EngineContext Context { get; } = new EngineContext();
|
||||||
|
|
||||||
private readonly Uri _server;
|
|
||||||
private GraphicsDeviceManager _graphics;
|
private GraphicsDeviceManager _graphics;
|
||||||
private SpriteBatch _spriteBatch = null!;
|
private SpriteBatch _spriteBatch = null!;
|
||||||
private Texture2D _pixel = null!;
|
private Texture2D _pixel = null!;
|
||||||
|
private WorldViewScene _scene = null!;
|
||||||
|
|
||||||
/// <summary>Игра, подключающаяся к серверу <paramref name="server"/>.</summary>
|
/// <summary>Игра-наблюдатель: мира не создаёт, ждёт <see cref="Connect"/> из UI.</summary>
|
||||||
public LittleSimWebGame(Uri server)
|
public LittleSimWebGame()
|
||||||
{
|
{
|
||||||
_server = server;
|
|
||||||
_graphics = new GraphicsDeviceManager(this);
|
_graphics = new GraphicsDeviceManager(this);
|
||||||
Content.RootDirectory = "Content";
|
Content.RootDirectory = "Content";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Состояние соединения со сценой-наблюдателем — читает оверлей подключения.</summary>
|
||||||
|
public ConnectionStatus Status => _scene?.Status ?? ConnectionStatus.Idle;
|
||||||
|
|
||||||
|
/// <summary>Число реплицированных жителей — для статуса в оверлее.</summary>
|
||||||
|
public int EntityCount => _scene?.EntityCount ?? 0;
|
||||||
|
|
||||||
|
/// <summary>Подключиться к серверу <paramref name="server"/> (вызывается из Blazor-оверлея).</summary>
|
||||||
|
public void Connect(Uri server) => _scene?.Connect(server);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Initialize()
|
protected override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
Context.Scenes.Switch(new WorldViewScene(_server, () => _spriteBatch, () => _pixel));
|
_scene = new WorldViewScene(() => _spriteBatch, () => _pixel);
|
||||||
|
Context.Scenes.Switch(_scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@page "/index.html"
|
@page "/index.html"
|
||||||
@inject IJSRuntime JsRuntime
|
@inject IJSRuntime JsRuntime
|
||||||
@using nkast.Wasm.Canvas
|
@using nkast.Wasm.Canvas
|
||||||
@@ -18,3 +18,12 @@
|
|||||||
">
|
">
|
||||||
<canvas id="theCanvas" style="touch-action:none;"></canvas>
|
<canvas id="theCanvas" style="touch-action:none;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="connect-bar">
|
||||||
|
<span class="title">LittleSim</span>
|
||||||
|
<input class="server" placeholder="ws://host:9050"
|
||||||
|
@bind="_serverAddress" @bind:event="oninput"
|
||||||
|
@onkeydown="OnAddressKey" />
|
||||||
|
<button class="connect" @onclick="Connect">Подключиться</button>
|
||||||
|
<span class="status @StatusClass">@StatusText</span>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
|
|
||||||
namespace LittleSim.Web.Pages
|
namespace LittleSim.Web.Pages
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,12 @@ namespace LittleSim.Web.Pages
|
|||||||
[Inject]
|
[Inject]
|
||||||
private NavigationManager Navigation { get; set; } = null!;
|
private NavigationManager Navigation { get; set; } = null!;
|
||||||
|
|
||||||
private Game? _game;
|
private LittleSimWebGame? _game;
|
||||||
|
private string _serverAddress = "";
|
||||||
|
private Uri? _pendingServer; // запрошен Connect до создания игры — подключим в первом тике
|
||||||
|
private ConnectionStatus _lastStatus = ConnectionStatus.Idle;
|
||||||
|
private int _lastCount = -1;
|
||||||
|
private int _frame;
|
||||||
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
protected override void OnAfterRender(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -18,25 +23,92 @@ namespace LittleSim.Web.Pages
|
|||||||
|
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
|
_serverAddress = DefaultServerAddress();
|
||||||
|
StateHasChanged();
|
||||||
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Гонится из requestAnimationFrame (index.html). Игру создаём лениво в первом тике —
|
||||||
|
// мира она не создаёт, лишь ждёт адрес сервера из формы.
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public void TickDotNet()
|
public void TickDotNet()
|
||||||
{
|
{
|
||||||
if (_game == null)
|
if (_game == null)
|
||||||
{
|
{
|
||||||
_game = new LittleSimWebGame(ResolveServerUri());
|
_game = new LittleSimWebGame();
|
||||||
_game.Run();
|
_game.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
_game.Tick();
|
_game.Tick();
|
||||||
|
|
||||||
|
if (_pendingServer is not null)
|
||||||
|
{
|
||||||
|
_game.Connect(_pendingServer);
|
||||||
|
_pendingServer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перерисовываем оверлей только при смене статуса (или изредка — чтобы освежить счётчик),
|
||||||
|
// а не каждый кадр rAF.
|
||||||
|
var status = _game.Status;
|
||||||
|
var count = _game.EntityCount;
|
||||||
|
if (
|
||||||
|
status != _lastStatus
|
||||||
|
|| (status == ConnectionStatus.Connected && count != _lastCount && _frame % 15 == 0)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_lastStatus = status;
|
||||||
|
_lastCount = count;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
_frame++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Адрес сервера: ?server=ws://host:port в URL страницы; по умолчанию —
|
private void OnAddressKey(KeyboardEventArgs e)
|
||||||
// хост самой страницы на порту LittleSim.Server.
|
{
|
||||||
private Uri ResolveServerUri()
|
if (e.Key == "Enter")
|
||||||
|
{
|
||||||
|
Connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connect()
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(_serverAddress?.Trim(), UriKind.Absolute, out var uri))
|
||||||
|
{
|
||||||
|
return; // адрес не похож на ws://… — игнорируем
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_game is not null)
|
||||||
|
{
|
||||||
|
_game.Connect(uri);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pendingServer = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string StatusText =>
|
||||||
|
_lastStatus switch
|
||||||
|
{
|
||||||
|
ConnectionStatus.Connecting => "подключение…",
|
||||||
|
ConnectionStatus.Connected => $"подключено · {_lastCount} жит.",
|
||||||
|
ConnectionStatus.Reconnecting => "переподключение…",
|
||||||
|
_ => "не подключено",
|
||||||
|
};
|
||||||
|
|
||||||
|
private string StatusClass =>
|
||||||
|
_lastStatus switch
|
||||||
|
{
|
||||||
|
ConnectionStatus.Connected => "ok",
|
||||||
|
ConnectionStatus.Connecting or ConnectionStatus.Reconnecting => "warn",
|
||||||
|
_ => "idle",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Адрес по умолчанию: ?server=ws://host:port из URL страницы, иначе хост страницы на порту сервера.
|
||||||
|
private string DefaultServerAddress()
|
||||||
{
|
{
|
||||||
var page = new Uri(Navigation.Uri);
|
var page = new Uri(Navigation.Uri);
|
||||||
var query = page.Query.TrimStart('?');
|
var query = page.Query.TrimStart('?');
|
||||||
@@ -45,7 +117,7 @@ namespace LittleSim.Web.Pages
|
|||||||
var separator = pair.IndexOf('=');
|
var separator = pair.IndexOf('=');
|
||||||
if (separator > 0 && pair[..separator] == "server")
|
if (separator > 0 && pair[..separator] == "server")
|
||||||
{
|
{
|
||||||
return new Uri(Uri.UnescapeDataString(pair[(separator + 1)..]));
|
return Uri.UnescapeDataString(pair[(separator + 1)..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +126,7 @@ namespace LittleSim.Web.Pages
|
|||||||
Scheme = "ws",
|
Scheme = "ws",
|
||||||
Host = page.Host,
|
Host = page.Host,
|
||||||
Port = WebNetSchema.DefaultPort,
|
Port = WebNetSchema.DefaultPort,
|
||||||
}.Uri;
|
}.Uri.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
.connect-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(12, 16, 24, 0.82);
|
||||||
|
border-bottom: 1px solid rgba(108, 198, 255, 0.25);
|
||||||
|
color: #d8e0ea;
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6cc6ff;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .server {
|
||||||
|
flex: 0 1 320px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 5px 9px;
|
||||||
|
background: #0c1018;
|
||||||
|
border: 1px solid #2a3850;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e6edf6;
|
||||||
|
font-family: ui-monospace, 'Cascadia Code', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .server:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #6cc6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .connect {
|
||||||
|
padding: 5px 14px;
|
||||||
|
background: #1c2c44;
|
||||||
|
border: 1px solid #3a567f;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d8e0ea;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .connect:hover {
|
||||||
|
background: #25395a;
|
||||||
|
border-color: #6cc6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status {
|
||||||
|
margin-left: auto;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status.ok {
|
||||||
|
color: #7fdca0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status.warn {
|
||||||
|
color: #f0c060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status.idle {
|
||||||
|
color: #8a96a6;
|
||||||
|
}
|
||||||
@@ -9,31 +9,74 @@ 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: подключается к дедикейтед-серверу
|
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
||||||
/// (LittleSim.Server --listen), применяет дельта-снапшоты в свой EntityStore и рисует
|
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
||||||
/// жителей через KNI SpriteBatch (WebGL). Симуляция целиком на сервере — сюда приезжают
|
/// EntityStore и рисует жителей через KNI SpriteBatch (WebGL). Мира не создаёт — только
|
||||||
/// только компоненты схемы (позиция + потребности); позиции сглаживаются до частоты
|
/// наблюдает: симуляция целиком на сервере, сюда приезжают лишь компоненты схемы (позиция +
|
||||||
/// кадра, усталость затемняет квадратик жителя.
|
/// потребности). Позиции сглаживаются до частоты кадра, усталость затемняет квадратик. При
|
||||||
|
/// обрыве переподключается сам с экспоненциальным бэкоффом, очищая устаревшие сущности.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorldViewScene : Scene
|
public sealed class WorldViewScene : Scene
|
||||||
{
|
{
|
||||||
private readonly Uri _server;
|
// Бэкофф переподключения: задержка удваивается с каждой неудачей до потолка.
|
||||||
|
private const float MaxBackoffSeconds = 8f;
|
||||||
|
|
||||||
private readonly Func<SpriteBatch> _spriteBatch;
|
private readonly Func<SpriteBatch> _spriteBatch;
|
||||||
private readonly Func<Texture2D> _pixel;
|
private readonly Func<Texture2D> _pixel;
|
||||||
|
|
||||||
private ReplicationClient _replication = null!;
|
private ReplicationClient _replication = null!;
|
||||||
private Task<WebSocketClient>? _connecting;
|
private Task<WebSocketClient>? _connecting;
|
||||||
private WebSocketClient? _connection;
|
private WebSocketClient? _connection;
|
||||||
|
private Uri? _server; // целевой сервер; null — пользователь ещё не подключался
|
||||||
|
private double _nextAttemptAt;
|
||||||
|
private int _attempt;
|
||||||
|
|
||||||
/// <summary>Сцена, подключающаяся к <paramref name="server"/>.</summary>
|
/// <summary>Текущее состояние соединения — для оверлея подключения.</summary>
|
||||||
public WorldViewScene(Uri server, Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
public ConnectionStatus Status { get; private set; } = ConnectionStatus.Idle;
|
||||||
|
|
||||||
|
/// <summary>Сколько реплицированных жителей сейчас в мире клиента.</summary>
|
||||||
|
public int EntityCount => _replication?.EntityCount ?? 0;
|
||||||
|
|
||||||
|
/// <summary>Сцена-наблюдатель; сервер задаётся позже через <see cref="Connect"/>.</summary>
|
||||||
|
public WorldViewScene(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||||
{
|
{
|
||||||
_server = server;
|
|
||||||
_spriteBatch = spriteBatch;
|
_spriteBatch = spriteBatch;
|
||||||
_pixel = pixel;
|
_pixel = pixel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подключиться к <paramref name="server"/> (ws:// или wss://). Сбрасывает прежнее
|
||||||
|
/// соединение и счётчик попыток — подключение начнётся в ближайшем тике. Дальше клиент сам
|
||||||
|
/// переподключается к этому адресу при обрывах.
|
||||||
|
/// </summary>
|
||||||
|
public void Connect(Uri server)
|
||||||
|
{
|
||||||
|
_connection?.Close();
|
||||||
|
_connection = null;
|
||||||
|
_replication?.Clear();
|
||||||
|
_server = server;
|
||||||
|
_attempt = 0;
|
||||||
|
_nextAttemptAt = 0f;
|
||||||
|
Status = ConnectionStatus.Connecting;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
|
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
|
||||||
private struct NetLerp : IComponent
|
private struct NetLerp : IComponent
|
||||||
{
|
{
|
||||||
@@ -51,35 +94,68 @@ public sealed class WorldViewScene : Scene
|
|||||||
UpdateSystems.Add(new NetSmoothingSystem());
|
UpdateSystems.Add(new NetSmoothingSystem());
|
||||||
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
|
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
|
||||||
|
|
||||||
Log.Info($"LittleSim.Web: connecting to {_server}…");
|
Log.Info("LittleSim.Web ready — awaiting connect");
|
||||||
_connecting = WebSocketClient.ConnectAsync(_server);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUnload() => _connection?.Close();
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
private void Pump()
|
private void Pump()
|
||||||
{
|
{
|
||||||
|
if (_server is null)
|
||||||
|
{
|
||||||
|
return; // адрес ещё не задан — ждём команды Connect из оверлея
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Context.Clock.UnscaledTotalTime;
|
||||||
|
|
||||||
|
// Завершилась попытка подключения: успех — берём соединение; провал — планируем повтор.
|
||||||
if (_connecting is { IsCompleted: true } finished)
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
{
|
{
|
||||||
_connecting = null;
|
_connecting = null;
|
||||||
if (finished.IsFaulted)
|
if (finished.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
_replication.Clear(); // сбрасываем устаревшие сущности перед свежим полным снапшотом
|
||||||
|
_attempt = 0;
|
||||||
|
Status = ConnectionStatus.Connected;
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Log.Error(
|
Log.Error(
|
||||||
$"Connect to {_server} failed: "
|
$"Connect to {_server} failed: "
|
||||||
+ finished.Exception?.GetBaseException().Message
|
+ finished.Exception?.GetBaseException().Message
|
||||||
);
|
);
|
||||||
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
{
|
|
||||||
_connection = finished.Result;
|
// Обрыв установленного соединения: чистим и уходим в переподключение.
|
||||||
Log.Info($"Connected to {_server}");
|
if (_connection is { IsOpen: false })
|
||||||
}
|
{
|
||||||
|
_connection = null;
|
||||||
|
_replication.Clear();
|
||||||
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_connection is not null)
|
if (_connection is not null)
|
||||||
{
|
{
|
||||||
_replication.Pump(_connection);
|
_replication.Pump(_connection);
|
||||||
}
|
}
|
||||||
|
else if (_connecting is null && now >= _nextAttemptAt)
|
||||||
|
{
|
||||||
|
Status = _attempt == 0 ? ConnectionStatus.Connecting : ConnectionStatus.Reconnecting;
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспоненциальный бэкофф: 1, 2, 4, 8, 8, … секунд между попытками.
|
||||||
|
private void ScheduleReconnect(double now)
|
||||||
|
{
|
||||||
|
var delay = MathF.Min(MaxBackoffSeconds, 1f * (1 << Math.Min(_attempt, 3)));
|
||||||
|
_attempt++;
|
||||||
|
_nextAttemptAt = now + delay;
|
||||||
|
Status = ConnectionStatus.Reconnecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
|
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ public sealed class GameSettings
|
|||||||
|
|
||||||
/// <summary>Общая громкость 0..1.</summary>
|
/// <summary>Общая громкость 0..1.</summary>
|
||||||
public float Volume { get; set; } = 1f;
|
public float Volume { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
|
||||||
|
public float UiScale { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Режим разработчика: открывает дев-консоль (backtick) и дев-спавнер. По умолчанию включён.</summary>
|
||||||
|
public bool DeveloperMode { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||||
|
|||||||
@@ -62,6 +62,145 @@ public sealed class PawnSave
|
|||||||
public float ChangeIn { get; set; }
|
public float ChangeIn { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сериализуемая событийная мысль (фаза A8): деф (по имени) и остаток времени до угасания.</summary>
|
||||||
|
public sealed class ThoughtSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа мысли.</summary>
|
||||||
|
public string DefName { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||||
|
public float RemainingDays { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сериализуемое яйцо (тип рождения «яйцекладка»): вид, позиция, геном детёныша, поколение, инкубация.</summary>
|
||||||
|
public sealed class EggSave
|
||||||
|
{
|
||||||
|
/// <summary>Имя дефа вида — по нему берётся индекс/спрайт/тело при вылуплении.</summary>
|
||||||
|
public string Species { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Позиция X в мировых координатах.</summary>
|
||||||
|
public float X { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Позиция Y в мировых координатах.</summary>
|
||||||
|
public float Y { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Поколение будущего детёныша.</summary>
|
||||||
|
public int Generation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Остаток инкубации в игровых днях.</summary>
|
||||||
|
public float IncubateDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Геном будущего детёныша (уже скрещён на момент кладки).</summary>
|
||||||
|
public Dictionary<string, Allele> Genome { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||||
@@ -99,6 +238,15 @@ public sealed class WorldSave
|
|||||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||||
public List<PlantSave> Plants { get; set; } = [];
|
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>
|
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||||
public WorldConfig ToConfig() =>
|
public WorldConfig ToConfig() =>
|
||||||
new()
|
new()
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
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"/>. Системы и
|
||||||
|
/// фабрика индексируются в неё без словарей. Фаза A1 — один спрайт на вид; стадии/пол придут позже.
|
||||||
|
/// </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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'");
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public sealed class ScatterEntry
|
|||||||
public List<string> Options { get; init; } = [];
|
public List<string> Options { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Тип клетки рельефа (Defs/terrain.json): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
||||||
public sealed class TerrainDef : Def
|
public sealed class TerrainDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
||||||
@@ -67,7 +67,7 @@ public sealed class PlantGrowthStage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
|
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||||
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||||
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -75,6 +75,9 @@ public sealed class ProductDef : Def
|
|||||||
{
|
{
|
||||||
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||||
public string Kind { get; init; } = "";
|
public string Kind { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Масса одной единицы продукта (кг) — основа под перенос/инвентарь (грузоподъёмность).</summary>
|
||||||
|
public float MassKg { get; init; } = 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -93,9 +96,15 @@ public sealed class GenomeDef
|
|||||||
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
||||||
public float OptimalTemperature { get; init; } = 18f;
|
public float OptimalTemperature { get; init; } = 18f;
|
||||||
|
|
||||||
/// <summary>Ширина толерантности по температуре (°C).</summary>
|
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
|
||||||
public float TemperatureTolerance { get; init; } = 12f;
|
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>
|
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
||||||
public float OptimalFertility { get; init; } = 1f;
|
public float OptimalFertility { get; init; } = 1f;
|
||||||
|
|
||||||
@@ -137,9 +146,18 @@ public sealed class GenomeDef
|
|||||||
|
|
||||||
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||||
public float LeafHue { get; init; } = 0.33f;
|
public float LeafHue { get; init; } = 0.33f;
|
||||||
|
|
||||||
|
/// <summary>Токсичность (0..1): отравляет поедателя (коэволюция); тормозит рост (цена защиты).</summary>
|
||||||
|
public float Toxicity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Шипы (0..1): травмируют поедателя; тормозят рост (цена защиты).</summary>
|
||||||
|
public float Thorns { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Вкусность (0..1): обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||||
|
public float Palatability { get; init; } = 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||||
public sealed class PlantDef : Def
|
public sealed class PlantDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
||||||
@@ -162,10 +180,13 @@ public sealed class PlantDef : Def
|
|||||||
|
|
||||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||||
public string? FruitProduct { get; init; }
|
public string? FruitProduct { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Эталонная масса (кг) зрелого растения базового размера; масса особи ∝ (размер/эталон)³.</summary>
|
||||||
|
public float BaseMassKg { get; init; } = 20f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Пресет размера мира (Defs/worldpresets.json): габариты сетки и население. Меню «Новый
|
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
|
||||||
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
||||||
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -184,8 +205,8 @@ public sealed class WorldPresetDef : Def
|
|||||||
public int Order { get; init; }
|
public int Order { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Существо (Defs/pawns.json): текстура, размер и вид (житель/животное).</summary>
|
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное). Не sealed — <see cref="AnimalDef"/> наследует.</summary>
|
||||||
public sealed class PawnDef : Def
|
public class PawnDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
||||||
public const string KindBeing = "being";
|
public const string KindBeing = "being";
|
||||||
@@ -202,3 +223,258 @@ public sealed class PawnDef : Def
|
|||||||
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
||||||
public string Kind { get; init; } = "";
|
public string Kind { get; init; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>Урон части тела добычи за игровой день укусов (0 — вид не атакует, не хищник по бою).</summary>
|
||||||
|
public float AttackDamage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Прирост тяжести раны-кровотечения у добычи за игровой день укусов.</summary>
|
||||||
|
public float AttackBleed { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Острая кровопотеря добычи за игровой день укусов (доля объёма крови) — основной путь к смерти.</summary>
|
||||||
|
public float AttackBloodLoss { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пункт дев-инструмента (Defs/DevTools/) — ЧТОБЫ МОДЫ ДОБАВЛЯЛИ кнопки в дев-спавнер без правки кода.
|
||||||
|
/// <see cref="Kind"/> — id обработчика из реестра (spawn-animal/spawn-plant/infect/damage/kill/…),
|
||||||
|
/// <see cref="Target"/> — параметр (defName вида/хедифа). Виды животных/растений спавнер берёт из их
|
||||||
|
/// дефов сам (модовый контент появляется автоматически); DevToolDef добавляет ПРОЧИЕ действия.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevToolDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Категория для группировки в панели (например "tool").</summary>
|
||||||
|
public string Category { get; init; } = "tool";
|
||||||
|
|
||||||
|
/// <summary>Id обработчика-инструмента из реестра кода (spawn-animal/spawn-plant/infect/damage/kill).</summary>
|
||||||
|
public string Kind { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Параметр инструмента: defName вида/хедифа и т.п. (по смыслу Kind).</summary>
|
||||||
|
public string Target { get; init; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Навык (Defs/Skills/) как ДАННЫЕ: уровень особи [0..1] растёт от практики (XP при действии,
|
||||||
|
/// ускоряется страстью) и медленно угасает без неё к полу <see cref="FloorLevel"/>. Эффект навыка —
|
||||||
|
/// код по его id (как у нужд/действий): добыча→выпас, охота→урон, уклонение→бегство. Модер добавляет
|
||||||
|
/// навык строкой JSON; новый эффект — код. <see cref="Def.Label"/> — ключ локализации (skill.*).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SkillDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Категория (для группировки в UI), напр. "survival".</summary>
|
||||||
|
public string Category { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Скорость обучения за игровой день непрерывной практики (при страсти ×1).</summary>
|
||||||
|
public float LearnRate { get; init; } = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>Угасание уровня за игровой день без практики.</summary>
|
||||||
|
public float DecayPerDay { get; init; } = 0.02f;
|
||||||
|
|
||||||
|
/// <summary>Пол угасания (ниже не падает) — «не забывается совсем».</summary>
|
||||||
|
public float FloorLevel { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Часть тела/орган (вложенный объект <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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Анатомия вида (Defs/Bodies/): дерево частей тела/органов. Используется системой здоровья (фаза
|
||||||
|
/// A5): из неё строятся инстансы частей особи, по их HP/крови считаются способности (capacities).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BodyDef : Def
|
||||||
|
{
|
||||||
|
/// <summary>Части тела/органы вида.</summary>
|
||||||
|
public BodyPartDef[] Parts { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,54 @@
|
|||||||
|
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 Dictionary<string, int> _byAction = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Строит реестр из всех <see cref="NeedDef"/> мода (порядок = индексы нужд).</summary>
|
||||||
|
public NeedSet(GameContent content)
|
||||||
|
{
|
||||||
|
_defs = content.Defs.All<NeedDef>().ToArray();
|
||||||
|
for (var i = 0; i < _defs.Length; i++)
|
||||||
|
{
|
||||||
|
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) => string.Equals(_defs[index].Kind, "drive", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ public sealed class PlantSet
|
|||||||
/// <summary>Индекс вида по дефу.</summary>
|
/// <summary>Индекс вида по дефу.</summary>
|
||||||
public int IndexOf(PlantDef def) => _index[def];
|
public int IndexOf(PlantDef def) => _index[def];
|
||||||
|
|
||||||
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр
|
// Переводит базовый геном вида (числа из Defs/Plants/) в набор генов: каждое число — центр
|
||||||
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
|
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
|
||||||
private static GenomeTemplate BuildTemplate(
|
private static GenomeTemplate BuildTemplate(
|
||||||
GenomeDef g,
|
GenomeDef g,
|
||||||
@@ -92,7 +92,7 @@ public sealed class PlantSet
|
|||||||
genes.TryGetValue(id, out var def)
|
genes.TryGetValue(id, out var def)
|
||||||
? def
|
? def
|
||||||
: throw new KeyNotFoundException(
|
: throw new KeyNotFoundException(
|
||||||
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?"
|
$"Gene def '{id}' not found — is Mods/Core/Defs/Genes/ loaded?"
|
||||||
);
|
);
|
||||||
|
|
||||||
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
|
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
|
||||||
@@ -103,6 +103,8 @@ public sealed class PlantSet
|
|||||||
Numeric("GeneLightTolerance", g.LightTolerance),
|
Numeric("GeneLightTolerance", g.LightTolerance),
|
||||||
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
|
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
|
||||||
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
|
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
|
||||||
|
Numeric("GeneColdHardiness", g.ColdHardiness),
|
||||||
|
Numeric("GeneHeatHardiness", g.HeatHardiness),
|
||||||
Numeric("GeneOptimalFertility", g.OptimalFertility),
|
Numeric("GeneOptimalFertility", g.OptimalFertility),
|
||||||
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
|
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
|
||||||
Numeric("GeneVigor", g.Vigor),
|
Numeric("GeneVigor", g.Vigor),
|
||||||
@@ -116,6 +118,10 @@ public sealed class PlantSet
|
|||||||
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
|
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
|
||||||
Numeric("GeneHarvestAmount", g.HarvestAmount),
|
Numeric("GeneHarvestAmount", g.HarvestAmount),
|
||||||
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
|
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
|
||||||
|
// Защита (коэволюция с травоядными): токсичность/шипы/вкусность.
|
||||||
|
Numeric("GeneToxicity", g.Toxicity),
|
||||||
|
Numeric("GeneThorns", g.Thorns),
|
||||||
|
Numeric("GenePalatability", g.Palatability),
|
||||||
new GenomeTemplate.Entry(
|
new GenomeTemplate.Entry(
|
||||||
Gene("GeneMorph"),
|
Gene("GeneMorph"),
|
||||||
0f,
|
0f,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public sealed class BootScene : Scene
|
|||||||
host.Graphics.ApplyChanges();
|
host.Graphics.ApplyChanges();
|
||||||
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
|
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
|
||||||
|
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
_label = new Label
|
_label = new Label
|
||||||
{
|
{
|
||||||
TextColor = Ui.Accent,
|
TextColor = Ui.Accent,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class CreditsScene : Scene
|
|||||||
{
|
{
|
||||||
var lang = Context.Services.Get<GameContent>().Languages;
|
var lang = Context.Services.Get<GameContent>().Languages;
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var column = Ui.Column(12);
|
var column = Ui.Column(12);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ public static class GameLayers
|
|||||||
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
|
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
|
||||||
public static LayerId Beings { get; private set; }
|
public static LayerId Beings { get; private set; }
|
||||||
|
|
||||||
/// <summary>Регистрирует слои при первом вызове.</summary>
|
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
|
||||||
|
public static LayerId Selection { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Регистрирует слои при первом вызове (порядок = порядок отрисовки).</summary>
|
||||||
public static void EnsureRegistered(Renderer2D renderer)
|
public static void EnsureRegistered(Renderer2D renderer)
|
||||||
{
|
{
|
||||||
if (_registered)
|
if (_registered)
|
||||||
@@ -22,6 +25,7 @@ public static class GameLayers
|
|||||||
}
|
}
|
||||||
|
|
||||||
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
|
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
|
||||||
|
Selection = renderer.Layers.Register("Selection", LayerSpace.World, LayerSortMode.Depth);
|
||||||
_registered = true;
|
_registered = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class LoadGameScene : Scene
|
|||||||
{
|
{
|
||||||
_content = Context.Services.Get<GameContent>();
|
_content = Context.Services.Get<GameContent>();
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
_desktop = this.UseUI();
|
_desktop = this.UseScaledUI();
|
||||||
Rebuild();
|
Rebuild();
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
@@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class MainMenuScene : Scene
|
|||||||
{
|
{
|
||||||
var content = Context.Services.Get<GameContent>();
|
var content = Context.Services.Get<GameContent>();
|
||||||
var lang = content.Languages;
|
var lang = content.Languages;
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var column = Ui.Column(12);
|
var column = Ui.Column(12);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public sealed class MultiplayerScene : Scene
|
|||||||
{
|
{
|
||||||
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
|
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
|
||||||
|
|
||||||
|
// Бэкофф переподключения: задержка удваивается с каждой неудачей до потолка.
|
||||||
|
private const float MaxBackoffSeconds = 8f;
|
||||||
|
|
||||||
private readonly Uri _server;
|
private readonly Uri _server;
|
||||||
private Task<WebSocketClient>? _connecting;
|
private Task<WebSocketClient>? _connecting;
|
||||||
private WebSocketClient? _connection;
|
private WebSocketClient? _connection;
|
||||||
@@ -38,6 +41,8 @@ public sealed class MultiplayerScene : Scene
|
|||||||
private GameContent _content = null!;
|
private GameContent _content = null!;
|
||||||
private Label _hud = null!;
|
private Label _hud = null!;
|
||||||
private string _statusKey = "net.connecting";
|
private string _statusKey = "net.connecting";
|
||||||
|
private double _nextAttemptAt; // время (unscaled) следующей попытки подключения
|
||||||
|
private int _attempt; // 0 — первая попытка; растёт при обрывах, задаёт бэкофф
|
||||||
|
|
||||||
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
|
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
|
||||||
public MultiplayerScene(Uri server) => _server = server;
|
public MultiplayerScene(Uri server) => _server = server;
|
||||||
@@ -64,7 +69,7 @@ public sealed class MultiplayerScene : Scene
|
|||||||
entity.AddComponent(new NetLerp());
|
entity.AddComponent(new NetLerp());
|
||||||
};
|
};
|
||||||
|
|
||||||
var desktop = this.UseUI();
|
var desktop = this.UseScaledUI();
|
||||||
_hud = new Label { Left = 10, Top = 8 };
|
_hud = new Label { Left = 10, Top = 8 };
|
||||||
desktop.Root = Ui.Screen(_hud);
|
desktop.Root = Ui.Screen(_hud);
|
||||||
|
|
||||||
@@ -86,39 +91,53 @@ public sealed class MultiplayerScene : Scene
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
_connecting = WebSocketClient.ConnectAsync(_server);
|
// Первую попытку запускает сам Pump (единый путь с переподключением): _nextAttemptAt = 0.
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUnload() => _connection?.Close();
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
private void Pump()
|
private void Pump()
|
||||||
{
|
{
|
||||||
|
var now = Context.Clock.UnscaledTotalTime;
|
||||||
|
|
||||||
|
// Завершилась попытка подключения: успех — берём соединение; провал — планируем повтор.
|
||||||
if (_connecting is { IsCompleted: true } finished)
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
{
|
{
|
||||||
_connecting = null;
|
_connecting = null;
|
||||||
if (finished.IsFaulted)
|
if (finished.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
_replication.Clear(); // сбрасываем устаревшие сущности перед свежим полным снапшотом
|
||||||
|
_attempt = 0;
|
||||||
|
_statusKey = "net.connected";
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_statusKey = "net.failed";
|
|
||||||
Log.Error(
|
Log.Error(
|
||||||
$"Connect to {_server} failed: "
|
$"Connect to {_server} failed: "
|
||||||
+ finished.Exception?.GetBaseException().Message
|
+ finished.Exception?.GetBaseException().Message
|
||||||
);
|
);
|
||||||
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
{
|
|
||||||
_connection = finished.Result;
|
// Обрыв уже установленного соединения: чистим и уходим в переподключение.
|
||||||
_statusKey = "net.connected";
|
if (_connection is { IsOpen: false })
|
||||||
Log.Info($"Connected to {_server}");
|
{
|
||||||
}
|
_connection = null;
|
||||||
|
_replication.Clear();
|
||||||
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_connection is not null)
|
if (_connection is not null)
|
||||||
{
|
{
|
||||||
_replication.Pump(_connection);
|
_replication.Pump(_connection);
|
||||||
if (!_connection.IsOpen)
|
}
|
||||||
{
|
else if (_connecting is null && now >= _nextAttemptAt)
|
||||||
_statusKey = "net.lost";
|
{
|
||||||
}
|
_statusKey = _attempt == 0 ? "net.connecting" : "net.reconnecting";
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
}
|
}
|
||||||
|
|
||||||
_hud.Text = _content.Languages.Format(
|
_hud.Text = _content.Languages.Format(
|
||||||
@@ -133,4 +152,13 @@ public sealed class MultiplayerScene : Scene
|
|||||||
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Экспоненциальный бэкофф: 1, 2, 4, 8, 8, … секунд между попытками.
|
||||||
|
private void ScheduleReconnect(double now)
|
||||||
|
{
|
||||||
|
var delay = MathF.Min(MaxBackoffSeconds, 1f * (1 << Math.Min(_attempt, 3)));
|
||||||
|
_attempt++;
|
||||||
|
_nextAttemptAt = now + delay;
|
||||||
|
_statusKey = "net.reconnecting";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public sealed class NewWorldScene : Scene
|
|||||||
var content = Context.Services.Get<GameContent>();
|
var content = Context.Services.Get<GameContent>();
|
||||||
var lang = content.Languages;
|
var lang = content.Languages;
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
||||||
_preset = presets[0];
|
_preset = presets[0];
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ internal sealed class PauseMenu
|
|||||||
private readonly GameSpeed _speed;
|
private readonly GameSpeed _speed;
|
||||||
private readonly GameHost _host;
|
private readonly GameHost _host;
|
||||||
private readonly AudioManager _audio;
|
private readonly AudioManager _audio;
|
||||||
|
private readonly Desktop _desktop;
|
||||||
private readonly Func<string> _onSave;
|
private readonly Func<string> _onSave;
|
||||||
private readonly Action _onMainMenu;
|
private readonly Action _onMainMenu;
|
||||||
private readonly Action _onQuit;
|
private readonly Action _onQuit;
|
||||||
@@ -36,6 +37,7 @@ internal sealed class PauseMenu
|
|||||||
EngineContext context,
|
EngineContext context,
|
||||||
GameContent content,
|
GameContent content,
|
||||||
GameSpeed speed,
|
GameSpeed speed,
|
||||||
|
Desktop desktop,
|
||||||
Func<string> onSave,
|
Func<string> onSave,
|
||||||
Action onMainMenu,
|
Action onMainMenu,
|
||||||
Action onQuit
|
Action onQuit
|
||||||
@@ -44,6 +46,7 @@ internal sealed class PauseMenu
|
|||||||
_context = context;
|
_context = context;
|
||||||
_content = content;
|
_content = content;
|
||||||
_speed = speed;
|
_speed = speed;
|
||||||
|
_desktop = desktop;
|
||||||
_host = (GameHost)context.Services.Get<Game>();
|
_host = (GameHost)context.Services.Get<Game>();
|
||||||
_audio = context.Services.Get<AudioManager>();
|
_audio = context.Services.Get<AudioManager>();
|
||||||
_onSave = onSave;
|
_onSave = onSave;
|
||||||
@@ -144,6 +147,7 @@ internal sealed class PauseMenu
|
|||||||
{
|
{
|
||||||
GameSettingsStore.Save(_settings);
|
GameSettingsStore.Save(_settings);
|
||||||
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||||
|
UiScaling.Apply(_context, _desktop, _settings.UiScale); // масштаб UI — сразу
|
||||||
ShowSettings(); // язык мог смениться
|
ShowSettings(); // язык мог смениться
|
||||||
},
|
},
|
||||||
onBack: () =>
|
onBack: () =>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public sealed class SettingsScene : Scene
|
|||||||
_host = (GameHost)Context.Services.Get<Game>();
|
_host = (GameHost)Context.Services.Get<Game>();
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
|
|
||||||
_desktop = this.UseUI();
|
_desktop = this.UseScaledUI();
|
||||||
Rebuild();
|
Rebuild();
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
@@ -53,6 +53,7 @@ public sealed class SettingsScene : Scene
|
|||||||
{
|
{
|
||||||
GameSettingsStore.Save(_settings);
|
GameSettingsStore.Save(_settings);
|
||||||
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||||
|
UiScaling.Apply(Context, _desktop, _settings.UiScale); // масштаб UI — сразу
|
||||||
Rebuild(); // язык мог смениться — перестраиваем подписи
|
Rebuild(); // язык мог смениться — перестраиваем подписи
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.Screen(_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1090
-31
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,301 @@
|
|||||||
|
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 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,
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
|
||||||
|
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,610 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
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>Размер мозга (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] — снижает дозу отравления токсичным кормом.</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);
|
||||||
|
return new AnimalPhenotype
|
||||||
|
{
|
||||||
|
BodySize = T("bodySize"),
|
||||||
|
Metabolism = T("metabolism"),
|
||||||
|
MoveSpeed = T("moveSpeed"),
|
||||||
|
BloodVolume = T("bloodVolume"),
|
||||||
|
Vision = T("vision"),
|
||||||
|
BrainSize = T("brainSize"),
|
||||||
|
Insulation = T("insulation"),
|
||||||
|
FurHue = T("furHue"),
|
||||||
|
MaturityAge = T("maturityAge"),
|
||||||
|
Herbivory = T("herbivory"),
|
||||||
|
Carnivory = T("carnivory"),
|
||||||
|
Omnivory = T("omnivory"),
|
||||||
|
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"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>Номер поколения: max(родителей) + 1; у основателей 0.</summary>
|
||||||
|
public int Generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Инстанс части тела особи: ссылка на деф части и текущий/максимальный HP (масштаб по размеру).</summary>
|
||||||
|
public struct PartInstance
|
||||||
|
{
|
||||||
|
/// <summary>Деф части тела (анатомия вида).</summary>
|
||||||
|
public Content.BodyPartDef Def;
|
||||||
|
|
||||||
|
/// <summary>Текущий HP.</summary>
|
||||||
|
public float Hp;
|
||||||
|
|
||||||
|
/// <summary>Максимальный HP (база × размер тела).</summary>
|
||||||
|
public float MaxHp;
|
||||||
|
|
||||||
|
/// <summary>Доля сохранности [0,1].</summary>
|
||||||
|
public readonly float Fraction => MaxHp > 0f ? Math.Clamp(Hp / MaxHp, 0f, 1f) : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Инстанс хедифа на особи: деф + текущая тяжесть и набранный иммунитет (гонка, фаза A6).</summary>
|
||||||
|
public struct Hediff
|
||||||
|
{
|
||||||
|
/// <summary>Деф хедифа (гон/болезнь/…).</summary>
|
||||||
|
public HediffDef Def;
|
||||||
|
|
||||||
|
/// <summary>Текущая тяжесть.</summary>
|
||||||
|
public float Severity;
|
||||||
|
|
||||||
|
/// <summary>Набранный иммунитет [0,1]; при 1 — выздоровление.</summary>
|
||||||
|
public float Immunity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Состояние здоровья особи (фазы A4–A6): активные хедифы (инстансы) + части тела, кровь и вычисленные
|
||||||
|
/// способности (capacities). Managed-объект, как <see cref="Genome"/> (Friflo допускает ссылку в
|
||||||
|
/// компоненте). Болезни (A6) — первый источник урона: снижают способности и могут убить. Растёт по
|
||||||
|
/// фазам — контейнер один.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HealthState
|
||||||
|
{
|
||||||
|
private readonly List<Hediff> _hediffs = [];
|
||||||
|
|
||||||
|
/// <summary>Активные хедифы (гон, болезни).</summary>
|
||||||
|
public IReadOnlyList<Hediff> Hediffs => _hediffs;
|
||||||
|
|
||||||
|
/// <summary>Части тела особи (из анатомии вида); пусто — у вида нет тела.</summary>
|
||||||
|
public PartInstance[] Parts { get; private set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Уровень крови [0,1]; ниже порога — потеря сознания и смерть (когда появятся раны).</summary>
|
||||||
|
public float BloodLevel { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Боль [0,1] — снижает сознание (когда появятся раны).</summary>
|
||||||
|
public float Pain { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Погибла ли особь по здоровью (vital-часть уничтожена / кровь на нуле).</summary>
|
||||||
|
public bool Dead { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Вычисленные способности (capacity id → [0,1]); у здоровой особи все = 1.</summary>
|
||||||
|
public Dictionary<string, float> Capacities { get; private set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Способность по id (1, если не вычислена — напр. у вида без тела), для множителей действий.</summary>
|
||||||
|
public float Capacity(string id) => Capacities.TryGetValue(id, out var v) ? v : 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пересчитывает боль (из хедифов) и способности: базовые из частей/крови/боли, затем модификаторы
|
||||||
|
/// способностей от хедифов (лерп 1→factor по тяжести), затем общий множитель <paramref name="capacityScale"/>
|
||||||
|
/// (старение/сенесценс: дряхлеющий организм слабее по всем способностям). Вызывать после изменения
|
||||||
|
/// частей/крови/хедифов; <paramref name="capacityScale"/>=1 — без возрастного спада.
|
||||||
|
/// </summary>
|
||||||
|
public void RecomputeCapacities(float capacityScale = 1f)
|
||||||
|
{
|
||||||
|
var pain = 0f;
|
||||||
|
foreach (var h in _hediffs)
|
||||||
|
{
|
||||||
|
pain += h.Def.Pain * h.Severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pain = Math.Clamp(pain, 0f, 1f);
|
||||||
|
|
||||||
|
// Множитель старения применяется внутри Compute (без второго прохода/аллокации списка ключей).
|
||||||
|
var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain, capacityScale);
|
||||||
|
foreach (var h in _hediffs)
|
||||||
|
{
|
||||||
|
if (h.Def.CapMods.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var t = Math.Clamp(h.Severity, 0f, 1f);
|
||||||
|
foreach (var (capacity, factor) in h.Def.CapMods)
|
||||||
|
{
|
||||||
|
caps[capacity] = caps.GetValueOrDefault(capacity, 1f) * (1f + (factor - 1f) * t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Capacities = caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Прогрессирует болезни (тяжесть и иммунитет растут со временем); снимает выздоровевшие
|
||||||
|
/// (иммунитет ≥ 1); возвращает true, если какая-то болезнь достигла летальной тяжести.
|
||||||
|
/// <paramref name="immunityScale"/> — множитель набора иммунитета (способность «фильтрация крови»:
|
||||||
|
/// почки/печень выводят токсины и помогают бороться с болезнью; повреждённые → медленнее).
|
||||||
|
/// </summary>
|
||||||
|
public bool AdvanceHediffs(float days, float immunityScale = 1f)
|
||||||
|
{
|
||||||
|
var lethal = false;
|
||||||
|
for (var i = _hediffs.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var h = _hediffs[i];
|
||||||
|
if (h.Def.SeverityPerDay <= 0f && h.Def.ImmunityPerDay <= 0f)
|
||||||
|
{
|
||||||
|
continue; // статичный хедиф (гон) не прогрессирует
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Severity += h.Def.SeverityPerDay * days;
|
||||||
|
h.Immunity += h.Def.ImmunityPerDay * days * immunityScale;
|
||||||
|
if (h.Immunity >= 1f)
|
||||||
|
{
|
||||||
|
_hediffs.RemoveAt(i); // иммунитет победил — выздоровление
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h.Def.LethalSeverity > 0f && h.Severity >= h.Def.LethalSeverity)
|
||||||
|
{
|
||||||
|
lethal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hediffs[i] = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lethal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Строит здоровье особи: части из анатомии вида (HP × размер тела) + начальные способности.</summary>
|
||||||
|
public static HealthState Create(Content.BodyDef? body, float bodySize)
|
||||||
|
{
|
||||||
|
var state = new HealthState();
|
||||||
|
if (body is { Parts.Length: > 0 })
|
||||||
|
{
|
||||||
|
var scale = MathF.Max(0.2f, bodySize);
|
||||||
|
state.Parts = new PartInstance[body.Parts.Length];
|
||||||
|
for (var i = 0; i < body.Parts.Length; i++)
|
||||||
|
{
|
||||||
|
var hp = body.Parts[i].MaxHp * scale;
|
||||||
|
state.Parts[i] = new PartInstance
|
||||||
|
{
|
||||||
|
Def = body.Parts[i],
|
||||||
|
Hp = hp,
|
||||||
|
MaxHp = hp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.RecomputeCapacities();
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Несёт ли особь хедиф с данным именем дефа.</summary>
|
||||||
|
public bool Has(string defName)
|
||||||
|
{
|
||||||
|
foreach (var h in _hediffs)
|
||||||
|
{
|
||||||
|
if (h.Def.DefName == defName)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Навешивает хедиф (если ещё не навешан) с начальной тяжестью из дефа.</summary>
|
||||||
|
public void Add(HediffDef def)
|
||||||
|
{
|
||||||
|
if (!Has(def.DefName))
|
||||||
|
{
|
||||||
|
_hediffs.Add(new Hediff { Def = def, Severity = def.InitialSeverity });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Восстанавливает хедиф с заданной тяжестью и иммунитетом (для загрузки сейва).</summary>
|
||||||
|
public void AddHediff(HediffDef def, float severity, float immunity)
|
||||||
|
{
|
||||||
|
if (!Has(def.DefName))
|
||||||
|
{
|
||||||
|
_hediffs.Add(
|
||||||
|
new Hediff
|
||||||
|
{
|
||||||
|
Def = def,
|
||||||
|
Severity = severity,
|
||||||
|
Immunity = immunity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Снимает хедиф по имени дефа; возвращает, был ли он.</summary>
|
||||||
|
public bool Remove(string defName)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _hediffs.Count; i++)
|
||||||
|
{
|
||||||
|
if (_hediffs[i].Def.DefName == defName)
|
||||||
|
{
|
||||||
|
_hediffs.RemoveAt(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Травма (укус хищника): повреждает часть тела (выбор по весу попадания <c>coverage</c>), вызывает
|
||||||
|
/// мгновенную кровопотерю и навешивает/усиливает кровотечение, затем пересчитывает способности.
|
||||||
|
/// Первый травматический источник урона — оживляет каркас здоровья A5.
|
||||||
|
/// </summary>
|
||||||
|
public void ApplyInjury(
|
||||||
|
float partDamage,
|
||||||
|
float bloodLoss,
|
||||||
|
HediffDef? bleeding,
|
||||||
|
float bleedSeverity,
|
||||||
|
Random rng
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (Parts.Length > 0 && partDamage > 0f)
|
||||||
|
{
|
||||||
|
var idx = PickPart(rng);
|
||||||
|
var part = Parts[idx];
|
||||||
|
part.Hp = MathF.Max(0f, part.Hp - partDamage);
|
||||||
|
Parts[idx] = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
BloodLevel = Math.Clamp(BloodLevel - bloodLoss, 0f, 1f);
|
||||||
|
if (bleeding is not null && bleedSeverity > 0f)
|
||||||
|
{
|
||||||
|
Intensify(bleeding, bleedSeverity);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecomputeCapacities();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выбор задетой части по весу попадания (coverage); детерминируется переданным rng.
|
||||||
|
private int PickPart(Random rng)
|
||||||
|
{
|
||||||
|
var total = 0f;
|
||||||
|
foreach (var p in Parts)
|
||||||
|
{
|
||||||
|
total += p.Def?.Coverage ?? 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total <= 0f)
|
||||||
|
{
|
||||||
|
return rng.Next(Parts.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var roll = (float)rng.NextDouble() * total;
|
||||||
|
for (var i = 0; i < Parts.Length; i++)
|
||||||
|
{
|
||||||
|
if (Parts[i].Def is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
roll -= Parts[i].Def.Coverage;
|
||||||
|
if (roll <= 0f)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parts.Length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Навешивает хедиф или усиливает существующий: тяжесть растёт (до 1), а заживление (иммунитет)
|
||||||
|
/// сбрасывается — свежая рана начинает затягиваться заново. Для повторных укусов/ранений.
|
||||||
|
/// </summary>
|
||||||
|
public void Intensify(HediffDef def, float addSeverity)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _hediffs.Count; i++)
|
||||||
|
{
|
||||||
|
if (_hediffs[i].Def.DefName == def.DefName)
|
||||||
|
{
|
||||||
|
var h = _hediffs[i];
|
||||||
|
h.Severity = Math.Clamp(h.Severity + addSeverity, 0f, 1f);
|
||||||
|
h.Immunity = 0f; // свежая рана — заживление с нуля
|
||||||
|
_hediffs[i] = h;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hediffs.Add(new Hediff { Def = def, Severity = Math.Clamp(addSeverity, 0f, 1f) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Здоровье особи: managed-состояние со списком хедифов (см. <see cref="HealthState"/>).</summary>
|
||||||
|
public struct Health : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Состояние здоровья (хедифы; позже части тела/кровь).</summary>
|
||||||
|
public HealthState State;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Труп животного (фаза A5): отдельная сущность после смерти. Разлагается во времени по стадиям
|
||||||
|
/// (Fresh→Rotting→Skeleton→исчезает), скорость зависит от температуры. Несёт запас мяса (∝ размер тела)
|
||||||
|
/// для будущей разделки/падальщиков. Управляется <see cref="Sim.CorpseSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct Corpse : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта, разделки, сейва).</summary>
|
||||||
|
public int Species;
|
||||||
|
|
||||||
|
/// <summary>Накоплено игровых дней разложения.</summary>
|
||||||
|
public float RotDays;
|
||||||
|
|
||||||
|
/// <summary>Стадия: 0 свежий, 1 гниющий, 2 скелет.</summary>
|
||||||
|
public int Stage;
|
||||||
|
|
||||||
|
/// <summary>Доступное мясо (∝ масса тела) — для разделки/падальщиков (фаза-горизонт).</summary>
|
||||||
|
public float Meat;
|
||||||
|
|
||||||
|
/// <summary>Линейный размер тела (для масштаба спрайта трупа), независимо от запаса мяса.</summary>
|
||||||
|
public float BodySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Яйцо (тип рождения «яйцекладка»): отдельная сущность, отложенная яйцекладущей самкой вместо живых
|
||||||
|
/// родов. Несёт уже скрещённый геном будущего детёныша, вид и поколение; инкубируется во времени и
|
||||||
|
/// вылупляется в детёныша (<see cref="Sim.EggSystem"/>). Геном — managed-ссылка (как у трупа/беременности).
|
||||||
|
/// </summary>
|
||||||
|
public struct Egg : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Индекс вида в <see cref="Content.AnimalSet"/> (для спрайта при вылуплении и сейва).</summary>
|
||||||
|
public int Species;
|
||||||
|
|
||||||
|
/// <summary>Геном будущего детёныша (уже скрещён из родителей на момент кладки).</summary>
|
||||||
|
public Genome Genome;
|
||||||
|
|
||||||
|
/// <summary>Поколение будущего детёныша.</summary>
|
||||||
|
public int Generation;
|
||||||
|
|
||||||
|
/// <summary>Остаток инкубации в игровых днях; ≤0 — вылупление.</summary>
|
||||||
|
public float IncubateDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Id навыков-эффектов (на них ссылается код начисления XP и применения бонуса).</summary>
|
||||||
|
public static class AnimalSkills
|
||||||
|
{
|
||||||
|
/// <summary>Добыча корма: эффективность выпаса/поедания растений.</summary>
|
||||||
|
public const string Foraging = "Foraging";
|
||||||
|
|
||||||
|
/// <summary>Охота: урон по добыче.</summary>
|
||||||
|
public const string Hunting = "Hunting";
|
||||||
|
|
||||||
|
/// <summary>Уклонение: скорость бегства от хищника.</summary>
|
||||||
|
public const string Evasion = "Evasion";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Навыки особи (как нужды/способности — managed-массивы по индексам <see cref="Content.SkillSet"/>):
|
||||||
|
/// уровни [0..1] (растут от практики, угасают без неё) и страсть к каждому навыку (скорость обучения).
|
||||||
|
/// Есть у животных; те же навыки получат будущие люди. Добавление навыка не меняет структуру.
|
||||||
|
/// </summary>
|
||||||
|
public struct Skills : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Уровни навыков [0..1] по индексам <see cref="Content.SkillSet"/>.</summary>
|
||||||
|
public float[] Levels;
|
||||||
|
|
||||||
|
/// <summary>Страсть к каждому навыку (0 нет / 1 интерес / 2 страсть) — множитель обучения.</summary>
|
||||||
|
public byte[] Passions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ноша существа (задел под перенос предметов/хаул): суммарная масса переносимого груза в кг. Перегруз
|
||||||
|
/// сверх грузоподъёмности (<see cref="Sim.Mass.CarryCapacity"/>) замедляет движение. Компонент появляется
|
||||||
|
/// у существа, когда оно что-то несёт; пока источников груза нет — спящий каркас под будущий инвентарь.
|
||||||
|
/// </summary>
|
||||||
|
public struct Carrier : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Суммарная масса переносимого груза (кг).</summary>
|
||||||
|
public float CarriedKg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Проглоченное семя (эндозоохория): травоядное, съевшее плодоносящее растение, какое-то время несёт
|
||||||
|
/// его семя в кишечнике, затем «высаживает» его в другом месте (<see cref="Sim.SeedDispersalSystem"/>) —
|
||||||
|
/// зверь становится разносчиком флоры. Геном — managed-ссылка (клон родительского растения).
|
||||||
|
/// </summary>
|
||||||
|
public struct GutSeed : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Индекс вида растения в <see cref="Content.PlantSet"/>.</summary>
|
||||||
|
public int PlantSpecies;
|
||||||
|
|
||||||
|
/// <summary>Геном будущего ростка (клон съеденного растения).</summary>
|
||||||
|
public Genome Genome;
|
||||||
|
|
||||||
|
/// <summary>Остаток времени до высадки в игровых днях; ≤0 — росток падает на землю.</summary>
|
||||||
|
public float DropInDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Инстанс мысли настроения на особи (фаза A8): деф события + остаток времени до угасания.</summary>
|
||||||
|
public struct ThoughtInstance
|
||||||
|
{
|
||||||
|
/// <summary>Деф мысли (сдвиг настроения и длительность).</summary>
|
||||||
|
public Content.ThoughtDef Def;
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней мысль ещё действует.</summary>
|
||||||
|
public float RemainingDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Настроение особи (фаза A8): значение [0,1] и активные событийные мысли. Считается
|
||||||
|
/// <see cref="Sim.AnimalMoodSystem"/> из непрерывного фона (нужды/боль) и суммы мыслей. Есть только у
|
||||||
|
/// тира 4+ (<see cref="Active"/> по размеру мозга) — у примитивных настроения нет. Низкое настроение
|
||||||
|
/// влияет на поведение (стресс не даёт спариваться) и видно в инспекторе. Список мыслей — managed-ссылка
|
||||||
|
/// (как <c>AnimalNeeds.Values</c>): события добавляют мысли через <see cref="Add"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct Mood : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Текущее настроение [0,1]; у неактивного (тир < 4) держится нейтральным.</summary>
|
||||||
|
public float Value;
|
||||||
|
|
||||||
|
/// <summary>Есть ли у особи настроение (гейт тиром мозга) — иначе фон/мысли не считаются.</summary>
|
||||||
|
public bool Active;
|
||||||
|
|
||||||
|
/// <summary>Активные событийные мысли (затухают со временем).</summary>
|
||||||
|
public List<ThoughtInstance> Thoughts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Вешает событийную мысль (или обновляет её длительность, если уже висит — без накопления
|
||||||
|
/// дубликатов). Вызывается из систем-источников событий (роды, спаривание, утоление жажды).
|
||||||
|
/// </summary>
|
||||||
|
public readonly void Add(Content.ThoughtDef? def)
|
||||||
|
{
|
||||||
|
if (Thoughts is null || def is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Thoughts.Count; i++)
|
||||||
|
{
|
||||||
|
if (Thoughts[i].Def == def)
|
||||||
|
{
|
||||||
|
var refreshed = Thoughts[i];
|
||||||
|
refreshed.RemainingDays = def.DurationDays;
|
||||||
|
Thoughts[i] = refreshed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thoughts.Add(new ThoughtInstance { Def = def, RemainingDays = def.DurationDays });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Беременность самки (фаза A4): managed-ссылка на геном отца («слепок» на момент зачатия), остаток
|
||||||
|
/// срока вынашивания и размер помёта. По истечении срока <see cref="Sim.AnimalPregnancySystem"/>
|
||||||
|
/// рожает детёнышей через <see cref="Genome.Breed"/> и снимает компонент.
|
||||||
|
/// </summary>
|
||||||
|
public struct Pregnant : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Геном отца на момент зачатия.</summary>
|
||||||
|
public Genome FatherGenome;
|
||||||
|
|
||||||
|
/// <summary>Остаток срока вынашивания (игровых дней).</summary>
|
||||||
|
public float DueInDays;
|
||||||
|
|
||||||
|
/// <summary>Сколько детёнышей родится.</summary>
|
||||||
|
public int Litter;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
|||||||
|
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 Talking = "Talking";
|
||||||
|
public const string Eating = "Eating";
|
||||||
|
public const string Digestion = "Digestion";
|
||||||
|
|
||||||
|
/// <summary>Порядок отображения способностей в UI (сверху вниз). Прочие модовые — после, как есть.</summary>
|
||||||
|
public static readonly string[] DisplayOrder =
|
||||||
|
[
|
||||||
|
Consciousness,
|
||||||
|
Moving,
|
||||||
|
Sight,
|
||||||
|
Hearing,
|
||||||
|
Talking,
|
||||||
|
Eating,
|
||||||
|
Breathing,
|
||||||
|
BloodPumping,
|
||||||
|
BloodFiltration,
|
||||||
|
Digestion,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обобщённый расчёт способностей (capacities) из частей тела и крови (фаза A5). Сырой вклад способности
|
||||||
|
/// = Σ(доля части × доля её HP). Затем известные способности домножаются по зависимостям (кровь →
|
||||||
|
/// сердце/лёгкие → сознание → движение/зрение), как в модели RimWorld; неизвестные (модовые) проходят
|
||||||
|
/// сырыми. У здоровой особи (полное HP, кровь 1, боль 0) все способности = 1.
|
||||||
|
/// </summary>
|
||||||
|
public static class CapacityCalc
|
||||||
|
{
|
||||||
|
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1);
|
||||||
|
/// <paramref name="capacityScale"/> — общий множитель (старение), применяется к итогу без лишних аллокаций.</summary>
|
||||||
|
public static Dictionary<string, float> Compute(
|
||||||
|
PartInstance[] parts,
|
||||||
|
float blood,
|
||||||
|
float pain,
|
||||||
|
float capacityScale = 1f
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Сырой вклад каждой способности (Σ доля·HP) и НАБОР объявленных телом способностей: вид имеет
|
||||||
|
// только те, к которым его части вообще причастны (у оленя нет манипуляции). Объявленность —
|
||||||
|
// статична (по дефам частей), поэтому уничтоженный орган всё равно покажет способность на 0%.
|
||||||
|
var raw = new Dictionary<string, float>(System.StringComparer.Ordinal);
|
||||||
|
var declared = new HashSet<string>(System.StringComparer.Ordinal);
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
if (part.Def is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fraction = part.Fraction;
|
||||||
|
foreach (var (capacity, contribution) in part.Def.Capacities)
|
||||||
|
{
|
||||||
|
declared.Add(capacity);
|
||||||
|
raw[capacity] = raw.GetValueOrDefault(capacity) + contribution * fraction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float Raw(string id) => raw.GetValueOrDefault(id);
|
||||||
|
static float Clamp01(float v) =>
|
||||||
|
v < 0f ? 0f
|
||||||
|
: v > 1f ? 1f
|
||||||
|
: v;
|
||||||
|
|
||||||
|
// Базовые (независимые) способности: кровоснабжение/дыхание/фильтрация × уровень крови.
|
||||||
|
var bloodLevel = Clamp01(blood);
|
||||||
|
var bloodPumping = Raw(AnimalCapacities.BloodPumping) * bloodLevel;
|
||||||
|
var breathing = Raw(AnimalCapacities.Breathing) * bloodLevel;
|
||||||
|
var bloodFiltration = Raw(AnimalCapacities.BloodFiltration) * bloodLevel;
|
||||||
|
// Сознание зависит от кровоснабжения, дыхания и боли (модель RimWorld).
|
||||||
|
var consciousness =
|
||||||
|
Raw(AnimalCapacities.Consciousness)
|
||||||
|
* Clamp01(bloodPumping)
|
||||||
|
* Clamp01(breathing)
|
||||||
|
* (1f - Clamp01(pain));
|
||||||
|
var cons = Clamp01(consciousness);
|
||||||
|
var pump = Clamp01(bloodPumping);
|
||||||
|
|
||||||
|
// Производные способности зависят от сознания (и кровоснабжения для движения/пищеварения).
|
||||||
|
var computed = new Dictionary<string, float>(System.StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[AnimalCapacities.BloodPumping] = bloodPumping,
|
||||||
|
[AnimalCapacities.Breathing] = breathing,
|
||||||
|
[AnimalCapacities.BloodFiltration] = bloodFiltration,
|
||||||
|
[AnimalCapacities.Consciousness] = consciousness,
|
||||||
|
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * cons * pump,
|
||||||
|
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * cons,
|
||||||
|
[AnimalCapacities.Hearing] = Raw(AnimalCapacities.Hearing) * cons,
|
||||||
|
[AnimalCapacities.Talking] = Raw(AnimalCapacities.Talking) * cons,
|
||||||
|
[AnimalCapacities.Eating] = Raw(AnimalCapacities.Eating) * cons,
|
||||||
|
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * pump,
|
||||||
|
};
|
||||||
|
|
||||||
|
// В результат — только объявленные телом способности: известные по формуле зависимостей, прочие
|
||||||
|
// (модовые) — сырым вкладом. Так у вида видны ровно его способности, а не весь каталог.
|
||||||
|
var scale = Clamp01(capacityScale);
|
||||||
|
var result = new Dictionary<string, float>(System.StringComparer.Ordinal);
|
||||||
|
foreach (var capacity in declared)
|
||||||
|
{
|
||||||
|
var value = computed.TryGetValue(capacity, out var v) ? v : Raw(capacity);
|
||||||
|
result[capacity] = scale < 1f ? value * scale : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using LittleSim.Content;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Система веса (масса тел и грузоподъёмность). Масса — производная от РАЗМЕРА по кубическому закону
|
||||||
|
/// (вдвое крупнее ⇒ в 8 раз тяжелее): <c>масса = BaseMassKg × (размер / эталон)³</c>, где эталон —
|
||||||
|
/// базовый размер вида. Так детёныш/росток легче взрослого/зрелого, а ген размера двигает и массу.
|
||||||
|
/// Грузоподъёмность — задел под перенос предметов: доля массы тела × способность (манипуляция/движение).
|
||||||
|
/// </summary>
|
||||||
|
public static class Mass
|
||||||
|
{
|
||||||
|
/// <summary>Какую долю собственной массы существо может нести (×способность). Человек (руки) — больше.</summary>
|
||||||
|
public const float CarryFraction = 0.35f;
|
||||||
|
|
||||||
|
/// <summary>Доля массы тела, превращающаяся в мясо трупа (для разделки/падальщиков).</summary>
|
||||||
|
public const float MeatFraction = 0.5f;
|
||||||
|
|
||||||
|
private static float Cube(float v) => v * v * v;
|
||||||
|
|
||||||
|
/// <summary>Масса особи-животного (кг): эталон вида × куб (текущий линейный размер / эталонный).</summary>
|
||||||
|
public static float OfAnimal(AnimalDef def, in AnimalPhenotype traits, float stageScale)
|
||||||
|
{
|
||||||
|
var reference = def.Genome.GetValueOrDefault("GeneMaxBodySize", 1f);
|
||||||
|
if (reference <= 0f)
|
||||||
|
{
|
||||||
|
reference = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var linear = MathF.Max(0.01f, traits.BodySize) * MathF.Max(0.05f, stageScale);
|
||||||
|
return def.BaseMassKg * Cube(linear / reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Масса растения (кг): эталон вида × куб (размер текущей стадии / размер зрелой).</summary>
|
||||||
|
public static float OfPlant(PlantDef def, float stageSizeCells, float matureSizeCells)
|
||||||
|
{
|
||||||
|
var ratio = matureSizeCells > 0f ? stageSizeCells / matureSizeCells : 1f;
|
||||||
|
return def.BaseMassKg * Cube(ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Грузоподъёмность (кг): доля массы тела × способность нести (манипуляция/движение, 0..1+).</summary>
|
||||||
|
public static float CarryCapacity(float bodyMassKg, float capability) =>
|
||||||
|
bodyMassKg * CarryFraction * MathF.Max(0f, capability);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Метаболический множитель по закону Клайбера: расход энергии ∝ масса^0.75. Нормирован на эталонную
|
||||||
|
/// массу вида (взрослый базового размера = 1), поэтому крупные особи прожорливее, а молодняк — экономнее,
|
||||||
|
/// без межвидового взрыва баланса.
|
||||||
|
/// </summary>
|
||||||
|
public static float KleiberFactor(float massKg, float referenceMassKg)
|
||||||
|
{
|
||||||
|
if (referenceMassKg <= 0f || massKg <= 0f)
|
||||||
|
{
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MathF.Pow(massKg / referenceMassKg, 0.75f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Множитель скорости от ноши: в пределах грузоподъёмности — 1; перегруз тормозит (до 0.25).</summary>
|
||||||
|
public static float LoadSpeedFactor(float carriedKg, float capacityKg)
|
||||||
|
{
|
||||||
|
if (capacityKg <= 0f || carriedKg <= capacityKg)
|
||||||
|
{
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MathF.Max(0.25f, capacityKg / carriedKg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,8 @@ public static class PlantFactory
|
|||||||
CellFertility = cellFertility,
|
CellFertility = cellFertility,
|
||||||
},
|
},
|
||||||
new PlantOrganism { Genome = genome, Traits = traits },
|
new PlantOrganism { Genome = genome, Traits = traits },
|
||||||
new Fruiting()
|
new Fruiting(),
|
||||||
|
new TemperatureStress()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,13 @@ public sealed class PlantGrowthSystem(
|
|||||||
|
|
||||||
ref var traits = ref orgs[i].Traits;
|
ref var traits = ref orgs[i].Traits;
|
||||||
var light = lighting.SampleAt(t[i].Position);
|
var light = lighting.SampleAt(t[i].Position);
|
||||||
|
// Температура — трапеция RimWorld: плато полного роста, жёсткие края (дормантность вне).
|
||||||
|
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||||
var rate =
|
var rate =
|
||||||
traits.Vigor
|
traits.Vigor
|
||||||
|
* traits.DefenseGrowthFactor() // цена защиты: токсичные/колючие растут медленнее
|
||||||
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
|
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
|
||||||
* Suitability.Gaussian(
|
* Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
|
||||||
temperature,
|
|
||||||
traits.OptimalTemperature,
|
|
||||||
traits.TemperatureTolerance
|
|
||||||
)
|
|
||||||
* Suitability.Gaussian(
|
* Suitability.Gaussian(
|
||||||
grow.CellFertility,
|
grow.CellFertility,
|
||||||
traits.OptimalFertility,
|
traits.OptimalFertility,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Friflo.Engine.ECS;
|
|||||||
using Friflo.Engine.ECS.Systems;
|
using Friflo.Engine.ECS.Systems;
|
||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
using MrGameEng.Genetics;
|
using MrGameEng.Genetics;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
@@ -9,21 +10,30 @@ using MrGameEng.Graphics;
|
|||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается
|
/// Жизненный цикл растений: размножение (наследование по Менделю), смерть от старости и от
|
||||||
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку
|
/// температурного стресса (вымерзание/перегрев). Запускается не каждый кадр, а раз в
|
||||||
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с
|
/// <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку (счётчик плотности +
|
||||||
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления
|
/// «представитель» вида/генома), копит/рассасывает температурный стресс по текущей температуре,
|
||||||
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в
|
/// собирает умерших (старость или превышенный стресс), затем зрелые с вероятностью
|
||||||
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения
|
/// <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления (зрелый сосед
|
||||||
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно.
|
/// того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в случайную клетку
|
||||||
|
/// радиуса, если это суша и не превышен лимит плотности. Структурные изменения (создание/удаление
|
||||||
|
/// сущностей) применяются после проходов по запросу — это безопасно.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlantLifecycleSystem : BaseSystem
|
public sealed class PlantLifecycleSystem : BaseSystem
|
||||||
{
|
{
|
||||||
private const float IntervalDays = 0.25f;
|
private const float IntervalDays = 0.25f;
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней дормантности вне диапазона роста убивают растение.</summary>
|
||||||
|
private const float LethalStressDays = 6f;
|
||||||
|
|
||||||
|
/// <summary>Во сколько раз стресс рассасывается быстрее в пригодной температуре, чем копится вне.</summary>
|
||||||
|
private const float StressRecovery = 1.5f;
|
||||||
|
|
||||||
private readonly EntityStore _store;
|
private readonly EntityStore _store;
|
||||||
private readonly PlantSet _plants;
|
private readonly PlantSet _plants;
|
||||||
private readonly Calendar _calendar;
|
private readonly Calendar _calendar;
|
||||||
|
private readonly Climate _climate;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
private readonly int _width;
|
private readonly int _width;
|
||||||
private readonly int _height;
|
private readonly int _height;
|
||||||
@@ -32,7 +42,12 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
private readonly bool[] _cellLand;
|
private readonly bool[] _cellLand;
|
||||||
private readonly int _densityCap;
|
private readonly int _densityCap;
|
||||||
private readonly Random _rng;
|
private readonly Random _rng;
|
||||||
private readonly ArchetypeQuery<PlantGrowth, PlantOrganism, Transform2D> _query;
|
private readonly ArchetypeQuery<
|
||||||
|
PlantGrowth,
|
||||||
|
PlantOrganism,
|
||||||
|
Transform2D,
|
||||||
|
TemperatureStress
|
||||||
|
> _query;
|
||||||
|
|
||||||
private readonly int[] _count;
|
private readonly int[] _count;
|
||||||
private readonly CellRep[] _rep;
|
private readonly CellRep[] _rep;
|
||||||
@@ -45,6 +60,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
EntityStore store,
|
EntityStore store,
|
||||||
PlantSet plants,
|
PlantSet plants,
|
||||||
Calendar calendar,
|
Calendar calendar,
|
||||||
|
Climate climate,
|
||||||
GameClock clock,
|
GameClock clock,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -58,6 +74,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
_store = store;
|
_store = store;
|
||||||
_plants = plants;
|
_plants = plants;
|
||||||
_calendar = calendar;
|
_calendar = calendar;
|
||||||
|
_climate = climate;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
_width = width;
|
_width = width;
|
||||||
_height = height;
|
_height = height;
|
||||||
@@ -66,7 +83,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
_cellLand = cellLand;
|
_cellLand = cellLand;
|
||||||
_densityCap = densityCap;
|
_densityCap = densityCap;
|
||||||
_rng = new Random(seed);
|
_rng = new Random(seed);
|
||||||
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D>();
|
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D, TemperatureStress>();
|
||||||
_count = new int[width * height];
|
_count = new int[width * height];
|
||||||
_rep = new CellRep[width * height];
|
_rep = new CellRep[width * height];
|
||||||
}
|
}
|
||||||
@@ -82,13 +99,13 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
var elapsed = _accumulator;
|
var elapsed = _accumulator;
|
||||||
_accumulator = 0f;
|
_accumulator = 0f;
|
||||||
|
|
||||||
BuildGridAndCollect();
|
BuildGridAndCollect(elapsed);
|
||||||
Reproduce(elapsed);
|
Reproduce(elapsed);
|
||||||
Apply();
|
Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов.
|
// Проход 1: сетка плотности + представители, температурный стресс, сбор умерших и зрелых-кандидатов.
|
||||||
private void BuildGridAndCollect()
|
private void BuildGridAndCollect(float elapsed)
|
||||||
{
|
{
|
||||||
Array.Clear(_count);
|
Array.Clear(_count);
|
||||||
for (var c = 0; c < _rep.Length; c++)
|
for (var c = 0; c < _rep.Length; c++)
|
||||||
@@ -100,11 +117,14 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
_candidates.Clear();
|
_candidates.Clear();
|
||||||
_births.Clear();
|
_births.Clear();
|
||||||
|
|
||||||
foreach (var (growths, organisms, transforms, entities) in _query.Chunks)
|
var temperature = _climate.Temperature; // глобальная — едина для всех растений в этот тик
|
||||||
|
|
||||||
|
foreach (var (growths, organisms, transforms, stresses, entities) in _query.Chunks)
|
||||||
{
|
{
|
||||||
var g = growths.Span;
|
var g = growths.Span;
|
||||||
var orgs = organisms.Span;
|
var orgs = organisms.Span;
|
||||||
var t = transforms.Span;
|
var t = transforms.Span;
|
||||||
|
var st = stresses.Span;
|
||||||
for (var i = 0; i < g.Length; i++)
|
for (var i = 0; i < g.Length; i++)
|
||||||
{
|
{
|
||||||
ref var grow = ref g[i];
|
ref var grow = ref g[i];
|
||||||
@@ -115,6 +135,23 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Температурный стресс: вне диапазона роста (трапеция = 0) копится, внутри — рассасывается.
|
||||||
|
var (tMin, tLow, tHigh, tMax) = org.Traits.TemperatureBand();
|
||||||
|
ref var stress = ref st[i].Days;
|
||||||
|
if (Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) <= 0f)
|
||||||
|
{
|
||||||
|
stress += elapsed;
|
||||||
|
if (stress >= LethalStressDays)
|
||||||
|
{
|
||||||
|
_deaths.Add(entities.EntityAt(i)); // вымерзло/перегрелось
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stress = MathF.Max(0f, stress - elapsed * StressRecovery);
|
||||||
|
}
|
||||||
|
|
||||||
var pos = t[i].Position;
|
var pos = t[i].Position;
|
||||||
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
||||||
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using MrGameEng.Genetics;
|
|||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>genes.json</c>):
|
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>Defs/Genes/</c>):
|
||||||
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
|
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
|
||||||
/// Имена полей соответствуют именам признаков в эффектах генов.
|
/// Имена полей соответствуют именам признаков в эффектах генов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -15,6 +15,8 @@ public struct PlantPhenotype
|
|||||||
public float LightTolerance;
|
public float LightTolerance;
|
||||||
public float OptimalTemperature;
|
public float OptimalTemperature;
|
||||||
public float TemperatureTolerance;
|
public float TemperatureTolerance;
|
||||||
|
public float ColdHardiness;
|
||||||
|
public float HeatHardiness;
|
||||||
public float OptimalFertility;
|
public float OptimalFertility;
|
||||||
public float FertilityTolerance;
|
public float FertilityTolerance;
|
||||||
public float Vigor;
|
public float Vigor;
|
||||||
@@ -30,9 +32,31 @@ public struct PlantPhenotype
|
|||||||
public float HarvestAmount;
|
public float HarvestAmount;
|
||||||
public float LeafHue;
|
public float LeafHue;
|
||||||
|
|
||||||
|
// --- Защита (коэволюция с травоядными) ---
|
||||||
|
/// <summary>Токсичность [0..1]: отравляет поедателя; цена — замедление роста.</summary>
|
||||||
|
public float Toxicity;
|
||||||
|
|
||||||
|
/// <summary>Шипы [0..1]: травмируют поедателя; цена — замедление роста.</summary>
|
||||||
|
public float Thorns;
|
||||||
|
|
||||||
|
/// <summary>Вкусность [0..1]: обратная привлекательность для умных травоядных (избегание в C2).</summary>
|
||||||
|
public float Palatability;
|
||||||
|
|
||||||
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
||||||
public bool IsVariant;
|
public bool IsVariant;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Четыре точки температурной трапеции роста (модель RimWorld): жёсткий минимум, нижний край плато,
|
||||||
|
/// верхний край плато, жёсткий максимум. Плато — оптимум ± толерантность; жёсткие края отстоят от
|
||||||
|
/// плато на морозо-/жаростойкость. Кормит <see cref="MrGameEng.AI.Suitability.Trapezoid"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly (float Min, float OptimalLow, float OptimalHigh, float Max) TemperatureBand()
|
||||||
|
{
|
||||||
|
var low = OptimalTemperature - TemperatureTolerance;
|
||||||
|
var high = OptimalTemperature + TemperatureTolerance;
|
||||||
|
return (low - ColdHardiness, low, high, high + HeatHardiness);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||||
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
||||||
{
|
{
|
||||||
@@ -43,6 +67,8 @@ public struct PlantPhenotype
|
|||||||
LightTolerance = T("lightTolerance"),
|
LightTolerance = T("lightTolerance"),
|
||||||
OptimalTemperature = T("optimalTemperature"),
|
OptimalTemperature = T("optimalTemperature"),
|
||||||
TemperatureTolerance = T("temperatureTolerance"),
|
TemperatureTolerance = T("temperatureTolerance"),
|
||||||
|
ColdHardiness = T("coldHardiness"),
|
||||||
|
HeatHardiness = T("heatHardiness"),
|
||||||
OptimalFertility = T("optimalFertility"),
|
OptimalFertility = T("optimalFertility"),
|
||||||
FertilityTolerance = T("fertilityTolerance"),
|
FertilityTolerance = T("fertilityTolerance"),
|
||||||
Vigor = T("vigor"),
|
Vigor = T("vigor"),
|
||||||
@@ -55,9 +81,26 @@ public struct PlantPhenotype
|
|||||||
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
|
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
|
||||||
HarvestAmount = T("harvestAmount"),
|
HarvestAmount = T("harvestAmount"),
|
||||||
LeafHue = T("leafHue"),
|
LeafHue = T("leafHue"),
|
||||||
|
Toxicity = T("toxicity"),
|
||||||
|
Thorns = T("thorns"),
|
||||||
|
Palatability = T("palatability"),
|
||||||
IsVariant = T("variant") >= 0.5f,
|
IsVariant = T("variant") >= 0.5f,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Цена защиты: токсичность и шипы отнимают ресурсы у роста. Без этой платы отбор гнал бы защиту к
|
||||||
|
// максимуму у всех растений и коэволюция бы встала — а так под слабым выпасом выгоднее расти быстрее.
|
||||||
|
private const float ToxicityGrowthCost = 0.4f;
|
||||||
|
private const float ThornsGrowthCost = 0.3f;
|
||||||
|
private const float MinGrowthFactor = 0.2f;
|
||||||
|
|
||||||
|
/// <summary>Множитель скорости роста с учётом цены защиты (1 — без защиты, ≥ <c>MinGrowthFactor</c>).</summary>
|
||||||
|
public readonly float DefenseGrowthFactor() =>
|
||||||
|
Math.Clamp(
|
||||||
|
1f - ToxicityGrowthCost * Toxicity - ThornsGrowthCost * Thorns,
|
||||||
|
MinGrowthFactor,
|
||||||
|
1f
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -85,3 +128,15 @@ public struct Fruiting : IComponent
|
|||||||
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
||||||
public float RipeFruit;
|
public float RipeFruit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Накопленный температурный стресс растения в игровых днях. Пока температура вне диапазона роста
|
||||||
|
/// (трапеция <see cref="PlantPhenotype.TemperatureBand"/> даёт ноль — слишком холодно или жарко),
|
||||||
|
/// стресс копится; внутри диапазона — рассасывается. Превышение порога убивает растение
|
||||||
|
/// (вымерзание/перегрев). Управляется <c>PlantLifecycleSystem</c>.
|
||||||
|
/// </summary>
|
||||||
|
public struct TemperatureStress : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Накоплено дней дормантности вне диапазона роста; при превышении лимита — гибель.</summary>
|
||||||
|
public float Days;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текущее выделение игрока (как в RimWorld): id выбранной сущности или -1. Общий источник истины
|
||||||
|
/// для системы пикинга (<see cref="SelectionSystem"/>) и панели осмотра (<c>InspectPanel</c>):
|
||||||
|
/// первая пишет, вторая читает и рисует рамку/инфо. Хранит id, а не <see cref="Entity"/>, чтобы
|
||||||
|
/// безопасно переживать удаление сущности (растение умерло) — валидность проверяется через
|
||||||
|
/// <c>EntityStore.TryGetEntityById</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Selection
|
||||||
|
{
|
||||||
|
/// <summary>Runtime-id выбранной сущности, или -1, если ничего не выбрано.</summary>
|
||||||
|
public int EntityId { get; private set; } = -1;
|
||||||
|
|
||||||
|
/// <summary>Есть ли активное выделение.</summary>
|
||||||
|
public bool HasSelection => EntityId >= 0;
|
||||||
|
|
||||||
|
/// <summary>Выбрать сущность по id.</summary>
|
||||||
|
public void Select(int entityId) => EntityId = entityId;
|
||||||
|
|
||||||
|
/// <summary>Снять выделение.</summary>
|
||||||
|
public void Clear() => EntityId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пикинг мышью: ЛКМ выбирает верхнюю сущность под курсором (ближайшую по ограничивающей окружности
|
||||||
|
/// спрайта), клик по пустому месте или ПКМ снимает выделение. Молчит, пока ввод захвачен оверлеем
|
||||||
|
/// (консоль/инспектор) или активен <paramref name="blocked"/> (меню-пауза), и игнорирует клики над
|
||||||
|
/// UI-панелями (<paramref name="overUi"/>). Выбор работает и на паузе — мир можно осматривать стоя.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SelectionSystem(
|
||||||
|
EntityStore store,
|
||||||
|
InputManager input,
|
||||||
|
Renderer2D renderer,
|
||||||
|
Selection selection,
|
||||||
|
Func<bool> blocked,
|
||||||
|
Func<bool> overUi
|
||||||
|
) : BaseSystem
|
||||||
|
{
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
if (blocked() || overUi())
|
||||||
|
{
|
||||||
|
return; // меню-пауза/консоль или клик пришёлся на виджет UI — это не пикинг мира
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsMousePressed(MouseButton.Right))
|
||||||
|
{
|
||||||
|
selection.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.IsMousePressed(MouseButton.Left))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouse = input.MousePosition;
|
||||||
|
|
||||||
|
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||||||
|
if (TryPick(world, out var id))
|
||||||
|
{
|
||||||
|
selection.Select(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selection.Clear(); // клик по пустой земле снимает выделение
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ближайший осматриваемый объект (растение или животное), чья ограничивающая окружность спрайта
|
||||||
|
// накрывает точку мира. Запросы ограничены PlantGrowth/AnimalOrganism — так из пикинга выпадают
|
||||||
|
// спрайты рамки выделения, трупы и прочий не-контент. Из двух кандидатов берётся более близкий.
|
||||||
|
private bool TryPick(Vector2 world, out int id)
|
||||||
|
{
|
||||||
|
id = -1;
|
||||||
|
var best = float.MaxValue;
|
||||||
|
foreach (
|
||||||
|
var (transforms, sprites, _, entities) in store
|
||||||
|
.Query<Transform2D, Sprite, PlantGrowth>()
|
||||||
|
.Chunks
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (
|
||||||
|
var (transforms, sprites, _, entities) in store
|
||||||
|
.Query<Transform2D, Sprite, AnimalOrganism>()
|
||||||
|
.Chunks
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляет лучшего кандидата (id/дистанцию) по чанку спрайтов: точка должна попасть в окружность.
|
||||||
|
private static void Scan(
|
||||||
|
Vector2 world,
|
||||||
|
Span<Transform2D> transforms,
|
||||||
|
Span<Sprite> sprites,
|
||||||
|
ChunkEntities entities,
|
||||||
|
ref int id,
|
||||||
|
ref float best
|
||||||
|
)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < transforms.Length; i++)
|
||||||
|
{
|
||||||
|
if (sprites[i].Region is not { } region)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (center, radius) = CullingMath.SpriteBoundingCircle(
|
||||||
|
in transforms[i],
|
||||||
|
region,
|
||||||
|
sprites[i].Origin
|
||||||
|
);
|
||||||
|
var distance = Vector2.DistanceSquared(center, world);
|
||||||
|
if (distance <= radius * radius && distance < best)
|
||||||
|
{
|
||||||
|
best = distance;
|
||||||
|
id = entities.EntityAt(i).Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рамка выделения в стиле RimWorld: четыре угловые скобки вокруг выбранного объекта. Реализована
|
||||||
|
/// как четыре спрайт-сущности на слое-оверлее (<see cref="Scenes.GameLayers.Selection"/>) — их рисует
|
||||||
|
/// штатный спрайт-рендер, а одна и та же текстура уголка (<c>ui/overlays/selectionbracket</c>)
|
||||||
|
/// зеркалится во все четыре угла флагами <see cref="SpriteFlip"/> (батчер при флипе меняет только UV,
|
||||||
|
/// квадрат остаётся на месте). Скобки не имеют <c>PlantGrowth</c>, поэтому не попадают под пикинг и
|
||||||
|
/// прочие запросы растений. <see cref="Show"/> двигает их к объекту, <see cref="Hide"/> прячет.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SelectionBrackets
|
||||||
|
{
|
||||||
|
// По уголку на каждый угол: исходный арт лежит в левом-нижнем, флип разносит его по остальным.
|
||||||
|
private static readonly SpriteFlip[] Corners =
|
||||||
|
[
|
||||||
|
SpriteFlip.None,
|
||||||
|
SpriteFlip.X,
|
||||||
|
SpriteFlip.Y,
|
||||||
|
SpriteFlip.X | SpriteFlip.Y,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly Color Tint = new(150, 210, 255);
|
||||||
|
|
||||||
|
private readonly Entity[] _brackets = new Entity[4];
|
||||||
|
private readonly Texture2DRegion _region;
|
||||||
|
|
||||||
|
/// <summary>Создаёт четыре скрытые скобки на слое <paramref name="layer"/> из текстуры <paramref name="region"/>.</summary>
|
||||||
|
public SelectionBrackets(EntityStore store, Texture2DRegion region, LayerId layer)
|
||||||
|
{
|
||||||
|
_region = region;
|
||||||
|
var origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||||
|
for (var i = 0; i < _brackets.Length; i++)
|
||||||
|
{
|
||||||
|
_brackets[i] = store.CreateEntity(
|
||||||
|
Transform2D.At(Vector2.Zero),
|
||||||
|
new Sprite
|
||||||
|
{
|
||||||
|
Region = null, // скрыта, пока ничего не выбрано
|
||||||
|
Color = Tint,
|
||||||
|
Origin = origin, // центр текстуры → квад центрируется на позиции
|
||||||
|
Layer = layer,
|
||||||
|
Flip = Corners[i],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ставит рамку вокруг точки <paramref name="center"/> с полуразмером <paramref name="radius"/>.</summary>
|
||||||
|
public void Show(Vector2 center, float radius)
|
||||||
|
{
|
||||||
|
// Квад чуть больше объекта — скобки садятся снаружи (как в RimWorld); арт-уголок ~⅓ ширины.
|
||||||
|
var scale = new Vector2(radius * 2.2f / _region.Width);
|
||||||
|
for (var i = 0; i < _brackets.Length; i++)
|
||||||
|
{
|
||||||
|
ref var transform = ref _brackets[i].GetComponent<Transform2D>();
|
||||||
|
ref var sprite = ref _brackets[i].GetComponent<Sprite>();
|
||||||
|
transform.Position = center;
|
||||||
|
transform.Scale = scale;
|
||||||
|
sprite.Region = _region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Прячет рамку (сбрасывает регион — рендер пропускает спрайты без текстуры).</summary>
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _brackets.Length; i++)
|
||||||
|
{
|
||||||
|
_brackets[i].GetComponent<Sprite>().Region = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,13 +125,13 @@ public sealed class GodCameraSystem(
|
|||||||
Entity cameraEntity,
|
Entity cameraEntity,
|
||||||
InputManager input,
|
InputManager input,
|
||||||
Renderer2D renderer,
|
Renderer2D renderer,
|
||||||
MrGameEng.DevConsole.DevConsole console,
|
MrGameEng.DevConsole.DevConsole? console,
|
||||||
Func<bool>? blocked = null
|
Func<bool>? blocked = null
|
||||||
) : BaseSystem
|
) : BaseSystem
|
||||||
{
|
{
|
||||||
protected override void OnUpdateGroup()
|
protected override void OnUpdateGroup()
|
||||||
{
|
{
|
||||||
if (console.IsOpen || blocked?.Invoke() == true)
|
if (console?.IsOpen == true || blocked?.Invoke() == true)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -155,11 +155,15 @@ public sealed class GodCameraSystem(
|
|||||||
if (newZoom != oldZoom)
|
if (newZoom != oldZoom)
|
||||||
{
|
{
|
||||||
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
|
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
|
||||||
|
// Якорь и базовая позиция берутся из одного кадра камеры: WorldCenter — это позиция
|
||||||
|
// ПОСЛЕ клампа к границам (то, что реально нарисовано), а не сырой camera.Position —
|
||||||
|
// иначе у краёв карты/при широком обзоре зум «уезжает» от курсора.
|
||||||
var anchor = renderer.ScreenToWorld(
|
var anchor = renderer.ScreenToWorld(
|
||||||
new Vector2(input.MousePosition.X, input.MousePosition.Y)
|
new Vector2(input.MousePosition.X, input.MousePosition.Y)
|
||||||
);
|
);
|
||||||
|
var effective = renderer.Camera.WorldCenter;
|
||||||
camera.Zoom = newZoom;
|
camera.Zoom = newZoom;
|
||||||
camera.Position = anchor - (anchor - camera.Position) * (oldZoom / newZoom);
|
camera.Position = anchor - (anchor - effective) * (oldZoom / newZoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>Запись пространственного индекса: позиция (для раскладки по ячейкам) и id (для tie-break/исключения себя).</summary>
|
||||||
|
public interface ISpatialEntry
|
||||||
|
{
|
||||||
|
Vector2 Pos { get; }
|
||||||
|
int Id { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Растение для поиска корма: позиция/id + признаки, нужные выбору (токсичность, вкусность).</summary>
|
||||||
|
public readonly record struct ForageEntry(int Id, Vector2 Pos, float Toxicity, float Palatability)
|
||||||
|
: ISpatialEntry;
|
||||||
|
|
||||||
|
/// <summary>Сосед-животное для ИИ: позиция/id + предпосчитанные признаки (вид, размер, мясоед, пол, взрослость),
|
||||||
|
/// чтобы запросы угрозы/добычи/партнёра/стада не дорефетчили компоненты.</summary>
|
||||||
|
public readonly record struct NeighborEntry(
|
||||||
|
int Id,
|
||||||
|
Vector2 Pos,
|
||||||
|
int Species,
|
||||||
|
float BodySize,
|
||||||
|
bool EatsMeat,
|
||||||
|
bool IsMale,
|
||||||
|
bool IsAdult
|
||||||
|
) : ISpatialEntry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пространственный индекс (равномерная сетка ячеек) для радиус-запросов: вместо прохода по ВСЕМ
|
||||||
|
/// сущностям на каждый запрос — только из ближних ячеек. Перестраивается раз в кадр (O(n) линейно);
|
||||||
|
/// списки ячеек берутся из пула, поэтому перестройка не аллоцирует. Обход детерминирован (вложенные
|
||||||
|
/// циклы по ячейкам + порядок вставки); выбор всё равно ломает ничьи по меньшему id — детерминизм цел.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SpatialGrid<T>
|
||||||
|
where T : struct, ISpatialEntry
|
||||||
|
{
|
||||||
|
private readonly float _cell;
|
||||||
|
private readonly Dictionary<(int, int), List<T>> _cells = new();
|
||||||
|
private readonly Stack<List<T>> _pool = new();
|
||||||
|
|
||||||
|
public SpatialGrid(float cellSize) => _cell = cellSize > 0f ? cellSize : 1f;
|
||||||
|
|
||||||
|
/// <summary>Очищает сетку (списки ячеек возвращаются в пул).</summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
foreach (var list in _cells.Values)
|
||||||
|
{
|
||||||
|
list.Clear();
|
||||||
|
_pool.Push(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cells.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Добавляет элемент в его ячейку.</summary>
|
||||||
|
public void Add(T item)
|
||||||
|
{
|
||||||
|
var key = CellOf(item.Pos);
|
||||||
|
if (!_cells.TryGetValue(key, out var list))
|
||||||
|
{
|
||||||
|
list = _pool.Count > 0 ? _pool.Pop() : new List<T>();
|
||||||
|
_cells[key] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Собирает в <paramref name="results"/> элементы из ячеек, перекрывающих радиус (точную
|
||||||
|
/// дистанцию проверяет вызывающий). Буфер переиспользуется — без аллокаций на запрос.</summary>
|
||||||
|
public void Collect(Vector2 center, float radius, List<T> results)
|
||||||
|
{
|
||||||
|
results.Clear();
|
||||||
|
var minX = (int)MathF.Floor((center.X - radius) / _cell);
|
||||||
|
var maxX = (int)MathF.Floor((center.X + radius) / _cell);
|
||||||
|
var minY = (int)MathF.Floor((center.Y - radius) / _cell);
|
||||||
|
var maxY = (int)MathF.Floor((center.Y + radius) / _cell);
|
||||||
|
for (var cx = minX; cx <= maxX; cx++)
|
||||||
|
{
|
||||||
|
for (var cy = minY; cy <= maxY; cy++)
|
||||||
|
{
|
||||||
|
if (_cells.TryGetValue((cx, cy), out var list))
|
||||||
|
{
|
||||||
|
results.AddRange(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int, int) CellOf(Vector2 p) =>
|
||||||
|
((int)MathF.Floor(p.X / _cell), (int)MathF.Floor(p.Y / _cell));
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Дев-спавнер в духе RimWorld (только в режиме разработчика): боковая панель со списком по категориям.
|
||||||
|
/// Виды животных/растений берутся ИЗ ДЕФОВ (модовый контент появляется сам); «инструменты» (урон/смерть/
|
||||||
|
/// болезнь) и прочие модовые пункты — из <see cref="DevToolDef"/>. Спавн-пункт «берётся в руку»: клик по
|
||||||
|
/// миру создаёт сущность в курсоре (ПКМ снимает). Инструменты-действия применяются к выбранной особи
|
||||||
|
/// сразу по нажатию. Так удобно сравнивать сущности и наносить им урон. Управляет <see cref="DevSpawnerSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevSpawner
|
||||||
|
{
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly AnimalSet _animals;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly NeedSet _needs;
|
||||||
|
private readonly SkillSet _skills;
|
||||||
|
private readonly GameContent _content;
|
||||||
|
private readonly Selection _selection;
|
||||||
|
private readonly float[] _cellFertility;
|
||||||
|
private readonly int _width;
|
||||||
|
private readonly int _height;
|
||||||
|
private readonly int _cellSize;
|
||||||
|
private readonly HediffDef? _fever;
|
||||||
|
private readonly HediffDef? _bleeding;
|
||||||
|
private readonly Random _rng = new(0x5A11);
|
||||||
|
|
||||||
|
private readonly VerticalStackPanel _panel;
|
||||||
|
private readonly Label _title;
|
||||||
|
private string? _armedKind;
|
||||||
|
private string? _armedTarget;
|
||||||
|
|
||||||
|
public DevSpawner(
|
||||||
|
EntityStore store,
|
||||||
|
AnimalSet animals,
|
||||||
|
PlantSet plants,
|
||||||
|
NeedSet needs,
|
||||||
|
SkillSet skills,
|
||||||
|
GameContent content,
|
||||||
|
Selection selection,
|
||||||
|
float[] cellFertility,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int cellSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_animals = animals;
|
||||||
|
_plants = plants;
|
||||||
|
_needs = needs;
|
||||||
|
_skills = skills;
|
||||||
|
_content = content;
|
||||||
|
_selection = selection;
|
||||||
|
_cellFertility = cellFertility;
|
||||||
|
_width = width;
|
||||||
|
_height = height;
|
||||||
|
_cellSize = cellSize;
|
||||||
|
_fever = content.Defs.TryGet<HediffDef>("Fever", out var f) ? f : null;
|
||||||
|
_bleeding = content.Defs.TryGet<HediffDef>("Bleeding", out var b) ? b : null;
|
||||||
|
|
||||||
|
var lang = content.Languages;
|
||||||
|
_title = new Label { TextColor = Ui.Accent, Wrap = true };
|
||||||
|
|
||||||
|
var list = new VerticalStackPanel { Spacing = 2 };
|
||||||
|
|
||||||
|
// Инструменты-действия (по выбранной особи) + модовые DevToolDef.
|
||||||
|
Header(list, lang.Get("dev.tools"));
|
||||||
|
Entry(list, lang.Get("dev.damage"), () => ActOnSelection("damage", ""));
|
||||||
|
Entry(list, lang.Get("dev.kill"), () => ActOnSelection("kill", ""));
|
||||||
|
Entry(list, lang.Get("dev.infect"), () => ActOnSelection("infect", ""));
|
||||||
|
foreach (var tool in content.Defs.All<DevToolDef>())
|
||||||
|
{
|
||||||
|
var kind = tool.Kind;
|
||||||
|
var target = tool.Target;
|
||||||
|
var label = lang.Get(tool.Label);
|
||||||
|
if (kind is "spawn-animal" or "spawn-plant")
|
||||||
|
{
|
||||||
|
Entry(list, label, () => Arm(kind, target));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Entry(list, label, () => ActOnSelection(kind, target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Виды — автоматически из дефов (включая модовые): «взять в руку» спавн.
|
||||||
|
Header(list, lang.Get("dev.animals"));
|
||||||
|
foreach (var name in content.Defs.NamesOf("Animal"))
|
||||||
|
{
|
||||||
|
var defName = name;
|
||||||
|
Entry(list, "+ " + lang.Get(AnimalLabel(defName)), () => Arm("spawn-animal", defName));
|
||||||
|
}
|
||||||
|
|
||||||
|
Header(list, lang.Get("dev.plants"));
|
||||||
|
foreach (var name in content.Defs.NamesOf("Plant"))
|
||||||
|
{
|
||||||
|
var defName = name;
|
||||||
|
Entry(list, "+ " + lang.Get(PlantLabel(defName)), () => Arm("spawn-plant", defName));
|
||||||
|
}
|
||||||
|
|
||||||
|
_panel = new VerticalStackPanel
|
||||||
|
{
|
||||||
|
Spacing = 6,
|
||||||
|
Padding = new Thickness(12),
|
||||||
|
Width = 250,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 14, 12, 0),
|
||||||
|
Background = new SolidBrush(new Color(10, 13, 18, 240)),
|
||||||
|
Border = new SolidBrush(new Color(96, 134, 168)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Visible = false,
|
||||||
|
};
|
||||||
|
_panel.Widgets.Add(new Label { Text = lang.Get("dev.title"), TextColor = Ui.Accent });
|
||||||
|
_panel.Widgets.Add(_title);
|
||||||
|
_panel.Widgets.Add(new HorizontalSeparator());
|
||||||
|
_panel.Widgets.Add(new ScrollViewer { Height = 460, Content = list });
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Виджет панели (добавляется в корень экрана сцены).</summary>
|
||||||
|
public Widget Panel => _panel;
|
||||||
|
|
||||||
|
/// <summary>Взведён ли спавн-инструмент (клик по миру создаёт сущность).</summary>
|
||||||
|
public bool ArmedSpawn => _armedKind is "spawn-animal" or "spawn-plant";
|
||||||
|
|
||||||
|
/// <summary>Показать/скрыть панель (горячая клавиша из системы).</summary>
|
||||||
|
public void Toggle() => _panel.Visible = !_panel.Visible;
|
||||||
|
|
||||||
|
/// <summary>Снять «инструмент с руки» (ПКМ).</summary>
|
||||||
|
public void Disarm()
|
||||||
|
{
|
||||||
|
_armedKind = null;
|
||||||
|
_armedTarget = null;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Создаёт взведённую сущность в точке мира (вызывает система по клику ЛКМ).</summary>
|
||||||
|
public void SpawnAt(Vector2 world)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
_armedKind == "spawn-animal"
|
||||||
|
&& _content.Defs.TryGet<AnimalDef>(_armedTarget ?? "", out var adef)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var idx = _animals.IndexOf(adef);
|
||||||
|
var age = adef.Genome.GetValueOrDefault("GeneMaturityAge", 90f); // взрослый
|
||||||
|
AnimalFactory.Create(
|
||||||
|
_store,
|
||||||
|
_animals,
|
||||||
|
_needs,
|
||||||
|
_skills,
|
||||||
|
idx,
|
||||||
|
world,
|
||||||
|
age,
|
||||||
|
_animals.GenerateGenome(idx, _rng),
|
||||||
|
generation: 0,
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
_armedKind == "spawn-plant"
|
||||||
|
&& _content.Defs.TryGet<PlantDef>(_armedTarget ?? "", out var pdef)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var idx = _plants.IndexOf(pdef);
|
||||||
|
var cx = Math.Clamp((int)(world.X / _cellSize), 0, _width - 1);
|
||||||
|
var cy = Math.Clamp((int)(world.Y / _cellSize), 0, _height - 1);
|
||||||
|
var cell = cy * _width + cx;
|
||||||
|
var fertility = cell >= 0 && cell < _cellFertility.Length ? _cellFertility[cell] : 1f;
|
||||||
|
PlantFactory.Create(
|
||||||
|
_store,
|
||||||
|
_plants,
|
||||||
|
idx,
|
||||||
|
world,
|
||||||
|
_plants[idx].MaturityDays, // зрелое
|
||||||
|
_plants[idx].Template.Generate(_rng),
|
||||||
|
fertility,
|
||||||
|
_cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяет инструмент-действие к выбранной особи (урон/смерть/болезнь) — для сравнения/тестов.
|
||||||
|
private void ActOnSelection(string kind, string target)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!_selection.HasSelection
|
||||||
|
|| !_store.TryGetEntityById(_selection.EntityId, out var e)
|
||||||
|
|| e.IsNull
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case "kill" when e.HasComponent<AnimalOrganism>():
|
||||||
|
AnimalFactory.Die(_store, _animals, e, _cellSize);
|
||||||
|
break;
|
||||||
|
case "damage" when e.TryGetComponent<Health>(out var h) && h.State is not null:
|
||||||
|
h.State.ApplyInjury(15f, 0.08f, _bleeding, 1.5f, _rng);
|
||||||
|
break;
|
||||||
|
case "infect" when e.TryGetComponent<Health>(out var h2) && h2.State is not null:
|
||||||
|
var def =
|
||||||
|
target.Length > 0 && _content.Defs.TryGet<HediffDef>(target, out var hd)
|
||||||
|
? hd
|
||||||
|
: _fever;
|
||||||
|
if (def is not null)
|
||||||
|
{
|
||||||
|
h2.State.Add(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Arm(string kind, string target)
|
||||||
|
{
|
||||||
|
_armedKind = kind;
|
||||||
|
_armedTarget = target;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTitle() =>
|
||||||
|
_title.Text = _armedKind is null
|
||||||
|
? _content.Languages.Get("dev.hint")
|
||||||
|
: _content.Languages.Format("dev.armed", _armedTarget ?? "");
|
||||||
|
|
||||||
|
private static void Header(VerticalStackPanel list, string text) =>
|
||||||
|
list.Widgets.Add(new Label { Text = text, TextColor = Ui.Muted });
|
||||||
|
|
||||||
|
private static void Entry(VerticalStackPanel list, string text, Action onClick)
|
||||||
|
{
|
||||||
|
var button = new TextButton { Text = text };
|
||||||
|
button.Click += (_, _) => onClick();
|
||||||
|
list.Widgets.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string AnimalLabel(string defName) =>
|
||||||
|
_content.Defs.TryGet<AnimalDef>(defName, out var def) ? def.Label : defName;
|
||||||
|
|
||||||
|
private string PlantLabel(string defName) =>
|
||||||
|
_content.Defs.TryGet<PlantDef>(defName, out var def) ? def.Label : defName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Управление дев-спавнером: горячая клавиша F9 открывает/закрывает панель; при взведённом спавн-
|
||||||
|
/// инструменте ЛКМ по миру (не над UI) создаёт сущность в курсоре, ПКМ снимает инструмент.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DevSpawnerSystem(Renderer2D renderer, DevSpawner spawner, Func<bool> overUi)
|
||||||
|
: BaseSystem
|
||||||
|
{
|
||||||
|
private bool _prevToggle;
|
||||||
|
private bool _prevLeft;
|
||||||
|
private bool _prevRight;
|
||||||
|
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
var keyboard = Keyboard.GetState();
|
||||||
|
var toggle = keyboard.IsKeyDown(Keys.F9);
|
||||||
|
if (toggle && !_prevToggle)
|
||||||
|
{
|
||||||
|
spawner.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevToggle = toggle;
|
||||||
|
|
||||||
|
var mouse = Mouse.GetState();
|
||||||
|
var left = mouse.LeftButton == ButtonState.Pressed;
|
||||||
|
var right = mouse.RightButton == ButtonState.Pressed;
|
||||||
|
|
||||||
|
if (spawner.ArmedSpawn && left && !_prevLeft && !overUi())
|
||||||
|
{
|
||||||
|
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||||||
|
spawner.SpawnAt(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right && !_prevRight)
|
||||||
|
{
|
||||||
|
spawner.Disarm();
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevLeft = left;
|
||||||
|
_prevRight = right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта
|
||||||
|
/// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками. Набор вкладок
|
||||||
|
/// зависит от типа: растение — «Обзор/Гены/Продукты», животное — «Обзор/Нужды/Здоровье/Настроение».
|
||||||
|
/// Источник выделения — <see cref="Selection"/>; панель только читает компоненты и обновляется каждый
|
||||||
|
/// кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут через
|
||||||
|
/// <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class InspectPanel
|
||||||
|
{
|
||||||
|
private enum Tab
|
||||||
|
{
|
||||||
|
Overview,
|
||||||
|
Genes,
|
||||||
|
Products,
|
||||||
|
Needs,
|
||||||
|
Skills,
|
||||||
|
Health,
|
||||||
|
Mood,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Tab[] PlantTabs = [Tab.Overview, Tab.Genes, Tab.Products];
|
||||||
|
private static readonly Tab[] AnimalTabs =
|
||||||
|
[
|
||||||
|
Tab.Overview,
|
||||||
|
Tab.Needs,
|
||||||
|
Tab.Skills,
|
||||||
|
Tab.Health,
|
||||||
|
Tab.Mood,
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly AnimalSet _animals;
|
||||||
|
private readonly NeedSet _needs;
|
||||||
|
private readonly SkillSet _skills;
|
||||||
|
private readonly GameContent _content;
|
||||||
|
private readonly Climate _climate;
|
||||||
|
private readonly Selection _selection;
|
||||||
|
private readonly SelectionBrackets _brackets;
|
||||||
|
|
||||||
|
private readonly VerticalStackPanel _panel;
|
||||||
|
private readonly Label _title;
|
||||||
|
private readonly Label _body;
|
||||||
|
private readonly List<(Tab Tab, TextButton Button)> _tabs = [];
|
||||||
|
private Tab _tab = Tab.Overview;
|
||||||
|
|
||||||
|
public InspectPanel(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
AnimalSet animals,
|
||||||
|
NeedSet needs,
|
||||||
|
SkillSet skills,
|
||||||
|
GameContent content,
|
||||||
|
Climate climate,
|
||||||
|
Selection selection,
|
||||||
|
SelectionBrackets brackets
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_plants = plants;
|
||||||
|
_animals = animals;
|
||||||
|
_needs = needs;
|
||||||
|
_skills = skills;
|
||||||
|
_content = content;
|
||||||
|
_climate = climate;
|
||||||
|
_selection = selection;
|
||||||
|
_brackets = brackets;
|
||||||
|
|
||||||
|
_title = new Label { TextColor = Ui.Accent, Wrap = true };
|
||||||
|
_body = new Label { TextColor = new Color(210, 216, 226), Wrap = true };
|
||||||
|
|
||||||
|
var tabBar = new HorizontalStackPanel { Spacing = 4 };
|
||||||
|
AddTab(tabBar, Tab.Overview, "inspect.tab.overview");
|
||||||
|
AddTab(tabBar, Tab.Genes, "inspect.tab.genes");
|
||||||
|
AddTab(tabBar, Tab.Products, "inspect.tab.products");
|
||||||
|
AddTab(tabBar, Tab.Needs, "inspect.tab.needs");
|
||||||
|
AddTab(tabBar, Tab.Skills, "inspect.tab.skills");
|
||||||
|
AddTab(tabBar, Tab.Health, "inspect.tab.health");
|
||||||
|
AddTab(tabBar, Tab.Mood, "inspect.tab.mood");
|
||||||
|
|
||||||
|
_panel = new VerticalStackPanel
|
||||||
|
{
|
||||||
|
Spacing = 6,
|
||||||
|
Padding = new Thickness(14),
|
||||||
|
Width = 380,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = VerticalAlignment.Bottom,
|
||||||
|
Margin = new Thickness(12, 0, 0, 14),
|
||||||
|
Background = new SolidBrush(new Color(10, 13, 18, 240)),
|
||||||
|
Border = new SolidBrush(new Color(96, 134, 168)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Visible = false,
|
||||||
|
};
|
||||||
|
_panel.Widgets.Add(_title);
|
||||||
|
_panel.Widgets.Add(tabBar);
|
||||||
|
_panel.Widgets.Add(new HorizontalSeparator());
|
||||||
|
_panel.Widgets.Add(_body);
|
||||||
|
_panel.Widgets.Add(
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = _content.Languages.Get("inspect.hint"),
|
||||||
|
TextColor = Ui.Muted,
|
||||||
|
Wrap = true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Боковая инфо-панель — добавляется в корень экрана.</summary>
|
||||||
|
public VerticalStackPanel Panel => _panel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает
|
||||||
|
/// рамку к объекту и пересобирает текст активной вкладки. Осматриваются растения и животные.
|
||||||
|
/// </summary>
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!_selection.HasSelection
|
||||||
|
|| !_store.TryGetEntityById(_selection.EntityId, out var entity)
|
||||||
|
|| entity.IsNull
|
||||||
|
|| !entity.HasComponent<Transform2D>()
|
||||||
|
|| !entity.HasComponent<Sprite>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPlant = entity.HasComponent<PlantGrowth>() && entity.HasComponent<PlantOrganism>();
|
||||||
|
var isAnimal = entity.HasComponent<AnimalOrganism>();
|
||||||
|
if (!isPlant && !isAnimal)
|
||||||
|
{
|
||||||
|
ClearSelection(); // не контент (труп/прочее) — пока не осматривается
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transform = entity.GetComponent<Transform2D>();
|
||||||
|
var sprite = entity.GetComponent<Sprite>();
|
||||||
|
if (sprite.Region is { } region)
|
||||||
|
{
|
||||||
|
var (center, radius) = CullingMath.SpriteBoundingCircle(
|
||||||
|
in transform,
|
||||||
|
region,
|
||||||
|
sprite.Origin
|
||||||
|
);
|
||||||
|
_brackets.Show(center, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlant)
|
||||||
|
{
|
||||||
|
UpdatePlant(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateAnimal(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
_panel.Visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSelection()
|
||||||
|
{
|
||||||
|
if (_selection.HasSelection)
|
||||||
|
{
|
||||||
|
_selection.Clear(); // сущность исчезла (растение/зверь погибли) или не осматривается
|
||||||
|
}
|
||||||
|
|
||||||
|
_panel.Visible = false;
|
||||||
|
_brackets.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTab(HorizontalStackPanel bar, Tab tab, string labelKey)
|
||||||
|
{
|
||||||
|
var button = new TextButton { Text = _content.Languages.Get(labelKey) };
|
||||||
|
button.Click += (_, _) => _tab = tab;
|
||||||
|
bar.Widgets.Add(button);
|
||||||
|
_tabs.Add((tab, button));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показывает только вкладки текущего типа, подсвечивает активную; если активная не из набора —
|
||||||
|
// откатывает на первую (Обзор) при смене типа выбранного объекта.
|
||||||
|
private void ShowTabs(Tab[] valid)
|
||||||
|
{
|
||||||
|
if (Array.IndexOf(valid, _tab) < 0)
|
||||||
|
{
|
||||||
|
_tab = valid[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (tab, button) in _tabs)
|
||||||
|
{
|
||||||
|
button.Visible = Array.IndexOf(valid, tab) >= 0;
|
||||||
|
button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePlant(Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
ref readonly var grow = ref entity.GetComponent<PlantGrowth>();
|
||||||
|
ref readonly var org = ref entity.GetComponent<PlantOrganism>();
|
||||||
|
var species = _plants[grow.Species];
|
||||||
|
var def = species.Def;
|
||||||
|
var traits = org.Traits;
|
||||||
|
|
||||||
|
_title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : "");
|
||||||
|
ShowTabs(PlantTabs);
|
||||||
|
|
||||||
|
var text = new StringBuilder();
|
||||||
|
switch (_tab)
|
||||||
|
{
|
||||||
|
case Tab.Overview:
|
||||||
|
BuildOverview(text, entity, grow, traits, species);
|
||||||
|
break;
|
||||||
|
case Tab.Genes:
|
||||||
|
BuildGenes(text, traits);
|
||||||
|
break;
|
||||||
|
case Tab.Products:
|
||||||
|
BuildProducts(text, entity, def, traits);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_body.Text = text.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAnimal(Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
|
||||||
|
var sp = _animals[org.Species];
|
||||||
|
|
||||||
|
_title.Text = languages.Get(sp.Def.Label);
|
||||||
|
ShowTabs(AnimalTabs);
|
||||||
|
|
||||||
|
var text = new StringBuilder();
|
||||||
|
switch (_tab)
|
||||||
|
{
|
||||||
|
case Tab.Overview:
|
||||||
|
BuildAnimalOverview(text, entity, org, sp);
|
||||||
|
break;
|
||||||
|
case Tab.Needs:
|
||||||
|
BuildAnimalNeeds(text, entity);
|
||||||
|
break;
|
||||||
|
case Tab.Skills:
|
||||||
|
BuildAnimalSkills(text, entity);
|
||||||
|
break;
|
||||||
|
case Tab.Health:
|
||||||
|
BuildAnimalHealth(text, entity);
|
||||||
|
break;
|
||||||
|
case Tab.Mood:
|
||||||
|
BuildAnimalMood(text, entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_body.Text = text.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildOverview(
|
||||||
|
StringBuilder text,
|
||||||
|
Entity entity,
|
||||||
|
in PlantGrowth grow,
|
||||||
|
in PlantPhenotype traits,
|
||||||
|
PlantSet.Species species
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
var lastStage = species.Stages.Length - 1;
|
||||||
|
var mature = grow.Stage >= lastStage;
|
||||||
|
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.stage",
|
||||||
|
StageLabel(species.Def, grow.Stage),
|
||||||
|
grow.Stage + 1,
|
||||||
|
species.Stages.Length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (mature)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.mature"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var from = species.Stages[grow.Stage].EnterDay;
|
||||||
|
var to = species.Stages[grow.Stage + 1].EnterDay;
|
||||||
|
var pct = to > from ? (grow.AgeDays - from) / (to - from) * 100f : 0f;
|
||||||
|
text.AppendLine(languages.Format("inspect.growing", Math.Clamp(pct, 0f, 100f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan));
|
||||||
|
|
||||||
|
// Вес растения (∝ кубу размера текущей стадии относительно зрелой).
|
||||||
|
var plantMass = Mass.OfPlant(
|
||||||
|
species.Def,
|
||||||
|
species.Stages[grow.Stage].SizeCells,
|
||||||
|
species.Stages[^1].SizeCells
|
||||||
|
);
|
||||||
|
text.AppendLine(languages.Format("inspect.plantmass", plantMass));
|
||||||
|
|
||||||
|
// Состояние по температуре (рост/покой) и накопленный стресс.
|
||||||
|
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||||
|
var temperature = _climate.Temperature;
|
||||||
|
var suit = Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax);
|
||||||
|
var state =
|
||||||
|
suit <= 0f
|
||||||
|
? languages.Get(
|
||||||
|
temperature <= tMin ? "inspect.state.dormantcold" : "inspect.state.dormanthot"
|
||||||
|
)
|
||||||
|
: languages.Get(mature ? "inspect.state.mature" : "inspect.state.growing");
|
||||||
|
text.AppendLine(languages.Format("inspect.state", state));
|
||||||
|
if (entity.TryGetComponent<TemperatureStress>(out var stress) && stress.Days > 0.05f)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Format("inspect.stress", stress.Days));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine(languages.Format("inspect.temp", tMin, tMax, tLow, tHigh));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildGenes(StringBuilder text, in PlantPhenotype traits)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
text.AppendLine(languages.Format("inspect.gene.vigor", traits.Vigor, traits.Lifespan));
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.env", traits.OptimalLight, traits.OptimalFertility)
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.repro", traits.DispersalRange, traits.ReproduceInterval)
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.repro2", traits.SelfPollination, traits.MutationRate)
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.gene.hardy",
|
||||||
|
traits.ColdHardiness,
|
||||||
|
traits.HeatHardiness,
|
||||||
|
traits.LeafHue
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (traits.Toxicity > 0.01f || traits.Thorns > 0.01f)
|
||||||
|
{
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.defense", traits.Toxicity, traits.Thorns)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (traits.IsVariant)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.gene.variant"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildProducts(
|
||||||
|
StringBuilder text,
|
||||||
|
Entity entity,
|
||||||
|
PlantDef def,
|
||||||
|
in PlantPhenotype traits
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
if (def.HarvestProduct is { } harvest)
|
||||||
|
{
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.harvest", ProductLabel(harvest), traits.HarvestAmount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||||||
|
{
|
||||||
|
var ripe = entity.TryGetComponent<Fruiting>(out var fruiting) ? fruiting.RipeFruit : 0f;
|
||||||
|
var season = languages.Get(
|
||||||
|
"season." + ((Season)traits.FruitSeason).ToString().ToLowerInvariant()
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.fruit",
|
||||||
|
ProductLabel(fruit),
|
||||||
|
traits.FruitYield,
|
||||||
|
season,
|
||||||
|
ripe
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.barren"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// — Животное: Обзор — стадия, возраст, пол/поколение, беременность.
|
||||||
|
private void BuildAnimalOverview(
|
||||||
|
StringBuilder text,
|
||||||
|
Entity entity,
|
||||||
|
in AnimalOrganism org,
|
||||||
|
AnimalSet.Species sp
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
var grow = entity.GetComponent<AnimalGrowth>();
|
||||||
|
var stageName = languages.Get(
|
||||||
|
"animalstage." + sp.Stages[grow.Stage].Name.ToLowerInvariant()
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.stage", stageName, grow.Stage + 1, sp.Stages.Length)
|
||||||
|
);
|
||||||
|
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, org.Traits.Lifespan));
|
||||||
|
var sex = languages.Get(org.IsMale ? "inspect.sex.male" : "inspect.sex.female");
|
||||||
|
text.AppendLine(languages.Format("inspect.animal.sex", sex, org.Generation));
|
||||||
|
|
||||||
|
// Вес тела и грузоподъёмность (∝ массе × способность нести; четвероногие — через Moving).
|
||||||
|
var stageScale = sp.Stages[grow.Stage].Scale;
|
||||||
|
var mass = Mass.OfAnimal(sp.Def, org.Traits, stageScale);
|
||||||
|
var carryCap =
|
||||||
|
entity.TryGetComponent<Health>(out var health) && health.State is not null
|
||||||
|
? Mass.CarryCapacity(mass, health.State.Capacity(AnimalCapacities.Moving))
|
||||||
|
: Mass.CarryCapacity(mass, 1f);
|
||||||
|
text.AppendLine(languages.Format("inspect.mass", mass, carryCap));
|
||||||
|
|
||||||
|
if (entity.TryGetComponent<Pregnant>(out var preg))
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Format("inspect.animal.pregnant", preg.DueInDays));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// — Животное: Нужды — значение каждой нужды (голод/жажда/отдых/влечение) в процентах.
|
||||||
|
private void BuildAnimalNeeds(StringBuilder text, Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
var values = entity.GetComponent<AnimalNeeds>().Values;
|
||||||
|
if (values is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var k = 0; k < _needs.Count && k < values.Length; k++)
|
||||||
|
{
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.needline",
|
||||||
|
languages.Get(_needs[k].Label),
|
||||||
|
values[k] * 100f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// — Животное: Навыки — уровень каждого навыка (%) и страсть (★/★★), растут от практики.
|
||||||
|
private void BuildAnimalSkills(StringBuilder text, Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
if (!entity.TryGetComponent<Skills>(out var sk) || sk.Levels is null)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.health.none"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < _skills.Count && i < sk.Levels.Length; i++)
|
||||||
|
{
|
||||||
|
var passion =
|
||||||
|
sk.Passions is not null && i < sk.Passions.Length ? sk.Passions[i] : (byte)0;
|
||||||
|
var stars = passion switch
|
||||||
|
{
|
||||||
|
2 => "★★",
|
||||||
|
1 => "★",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.skillline",
|
||||||
|
languages.Get(_skills[i].Label),
|
||||||
|
sk.Levels[i] * 100f,
|
||||||
|
stars
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// — Животное: Здоровье — кровь/боль, ключевые способности, активные хедифы (раны/болезни/гон).
|
||||||
|
private void BuildAnimalHealth(StringBuilder text, Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
var state = entity.GetComponent<Health>().State;
|
||||||
|
if (state is null)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.health.none"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.health.blood", state.BloodLevel * 100f, state.Pain * 100f)
|
||||||
|
);
|
||||||
|
if (state.Capacities.Count > 0)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.health.caps"));
|
||||||
|
// Сначала известные способности в порядке отображения, затем прочие (модовые) — как есть.
|
||||||
|
var shown = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var capId in AnimalCapacities.DisplayOrder)
|
||||||
|
{
|
||||||
|
if (state.Capacities.ContainsKey(capId))
|
||||||
|
{
|
||||||
|
AppendCapacity(text, state, capId, CapacityLabel(capId));
|
||||||
|
shown.Add(capId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var capId in state.Capacities.Keys)
|
||||||
|
{
|
||||||
|
if (shown.Add(capId))
|
||||||
|
{
|
||||||
|
AppendCapacity(text, state, capId, CapacityLabel(capId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.Hediffs.Count > 0)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.health.hediffs"));
|
||||||
|
foreach (var hediff in state.Hediffs)
|
||||||
|
{
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.health.hediffline",
|
||||||
|
languages.Get(hediff.Def.Label),
|
||||||
|
hediff.Severity * 100f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.health.none"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ключ локализации метки способности: cap.<id в нижнем регистре> (cap.consciousness, cap.bloodpumping…).
|
||||||
|
private static string CapacityLabel(string capId) => "cap." + capId.ToLowerInvariant();
|
||||||
|
|
||||||
|
private void AppendCapacity(
|
||||||
|
StringBuilder text,
|
||||||
|
HealthState state,
|
||||||
|
string capId,
|
||||||
|
string labelKey
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.health.cap",
|
||||||
|
languages.Get(labelKey),
|
||||||
|
state.Capacity(capId) * 100f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// — Животное: Настроение — значение и активные событийные мысли с их сдвигом (тир 4+).
|
||||||
|
private void BuildAnimalMood(StringBuilder text, Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
var mood = entity.GetComponent<Mood>();
|
||||||
|
if (!mood.Active)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.mood.none"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine(languages.Format("inspect.mood.value", mood.Value * 100f));
|
||||||
|
if (mood.Thoughts is { Count: > 0 })
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.mood.thoughts"));
|
||||||
|
foreach (var thought in mood.Thoughts)
|
||||||
|
{
|
||||||
|
var pct = thought.Def.MoodOffset * 100f;
|
||||||
|
var offset = (pct >= 0f ? "+" : "") + pct.ToString("0") + "%";
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.mood.thoughtline",
|
||||||
|
languages.Get(thought.Def.Label),
|
||||||
|
offset
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.mood.calm"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string StageLabel(PlantDef def, int stage) =>
|
||||||
|
stage < def.Stages.Count && def.Stages[stage].Label is { } key
|
||||||
|
? _content.Languages.Get(key)
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
private string ProductLabel(string productDefName) =>
|
||||||
|
_content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||||
|
? _content.Languages.Get(product.Label)
|
||||||
|
: productDefName;
|
||||||
|
}
|
||||||
@@ -82,9 +82,32 @@ internal static class SettingsPanel
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Масштаб интерфейса — сегменты пресетов.
|
||||||
|
var scales = new List<float> { 0.75f, 1f, 1.25f, 1.5f };
|
||||||
|
column.Widgets.Add(
|
||||||
|
Segment(
|
||||||
|
lang.Get("settings.uiscale"),
|
||||||
|
scales,
|
||||||
|
s => $"{(int)MathF.Round(s * 100)}%",
|
||||||
|
() => settings.UiScale,
|
||||||
|
s => settings.UiScale = s,
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Громкость — кнопки −/+ с подписью процента.
|
// Громкость — кнопки −/+ с подписью процента.
|
||||||
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
|
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
|
||||||
|
|
||||||
|
// Режим разработчика — тумблер (дев-консоль и спавнер; по умолчанию включён).
|
||||||
|
column.Widgets.Add(
|
||||||
|
Toggle(
|
||||||
|
lang.Get("settings.devmode"),
|
||||||
|
() => settings.DeveloperMode,
|
||||||
|
v => settings.DeveloperMode = v,
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
column.Widgets.Add(new Label { Height = 8 });
|
column.Widgets.Add(new Label { Height = 8 });
|
||||||
var buttons = Ui.Row(10);
|
var buttons = Ui.Row(10);
|
||||||
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));
|
buttons.Widgets.Add(Ui.Button(lang.Get("settings.apply"), onApply, width: 150));
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using LittleSim.App;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Масштаб интерфейса для Myra-десктопа. Scale увеличивает всю отрисовку, а <c>BoundsFetcher</c>
|
||||||
|
/// отдаёт логический размер «экран ÷ масштаб», поэтому раскладка перетекает (а не обрезается):
|
||||||
|
/// при масштабе 1.25 виджеты крупнее, но углы/центры остаются прижаты к краям экрана. Ввод Myra
|
||||||
|
/// трансформируется тем же преобразованием, так что попадания/<c>IsMouseOverGUI</c> остаются верными.
|
||||||
|
/// </summary>
|
||||||
|
internal static class UiScaling
|
||||||
|
{
|
||||||
|
/// <summary>Применяет масштаб <paramref name="scale"/> (клампится в разумные пределы) к десктопу.</summary>
|
||||||
|
public static void Apply(EngineContext context, Desktop desktop, float scale)
|
||||||
|
{
|
||||||
|
scale = System.Math.Clamp(scale, 0.75f, 2f);
|
||||||
|
desktop.TransformOrigin = Vector2.Zero; // масштабируем от левого-верхнего угла — края на месте
|
||||||
|
desktop.Scale = new Vector2(scale);
|
||||||
|
desktop.BoundsFetcher = () =>
|
||||||
|
{
|
||||||
|
var viewport = context.GetGraphicsDevice().Viewport;
|
||||||
|
return new Rectangle(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
(int)MathF.Round(viewport.Width / scale),
|
||||||
|
(int)MathF.Round(viewport.Height / scale)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Расширение сцены: создаёт Myra-десктоп и сразу применяет масштаб UI из настроек.</summary>
|
||||||
|
internal static class ScaledUiExtensions
|
||||||
|
{
|
||||||
|
/// <summary>Как <c>UseUI</c>, но десктоп уже масштабирован по <see cref="GameSettings.UiScale"/>.</summary>
|
||||||
|
public static Desktop UseScaledUI(this Scene scene)
|
||||||
|
{
|
||||||
|
var desktop = scene.UseUI();
|
||||||
|
UiScaling.Apply(scene.Context, desktop, GameSettingsStore.Load().UiScale);
|
||||||
|
return desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user