Merge branch 'animals' into main: animal/zoology system (A1-A8 + serialization)
Brings the full animal foundation onto main and folds its new defs into the Defs/ subfolder layout introduced on main: - animal genes -> Defs/Genes/Animal.json (split out of genes.json) - animals/bodies/hediffs/needs/thoughts -> own Defs/<Type>/ subfolders - ProductMeat/Bone/Leather + bigger world presets auto-merged via rename detection - PawnDef un-sealed (AnimalDef : PawnDef); def-location doc comments updated to subfolders - engine pointer kept at main's newer 96e19c7 (only adds directional sun shadows) Build + --check-content clean: 11 def types, 33 genes, 35 plants, 32 traits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"type": "Animal",
|
||||
// Виды-животные (фаза A1): подтип Pawn (kind=animal) + базовый геном вида из общих Gene-дефов
|
||||
// (см. genes.json). Особь при спавне получает аллели вокруг этих баз, фенотип — формулами генов.
|
||||
// Пол, стадии роста, размножение, здоровье и поведение приходят в фазах A2+.
|
||||
"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,
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "Body",
|
||||
// Анатомия видов (фаза A5): дерево частей/органов. Вклады в способности (capacities) по телу
|
||||
// суммируются ≈1 у здоровой особи. maxHp умножается на размер тела особи при создании.
|
||||
// Источников урона пока нет — части на полном HP, способности = 1; каркас под болезни/хищников.
|
||||
"defs": [
|
||||
{
|
||||
"defName": "Quadruped",
|
||||
"parts": [
|
||||
{ "name": "torso", "coverage": 0.32, "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 },
|
||||
{ "name": "stomach", "parent": "torso", "coverage": 0.03, "maxHp": 12,
|
||||
"capacities": { "Digestion": 1.0 } },
|
||||
{ "name": "head", "coverage": 0.1, "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": "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,51 @@
|
||||
{
|
||||
"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" } },
|
||||
|
||||
// Пол как локус (фаза 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"] },
|
||||
|
||||
// Размножение (фаза 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,17 @@
|
||||
{
|
||||
"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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,11 @@
|
||||
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material" },
|
||||
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
||||
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" },
|
||||
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" }
|
||||
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" },
|
||||
|
||||
// Животные продукты (фаза A5): добываются из трупа (разделка/гниение). Инвентарь/добыча — позже.
|
||||
{ "defName": "ProductMeat", "label": "product.meat", "kind": "food" },
|
||||
{ "defName": "ProductBone", "label": "product.bone", "kind": "material" },
|
||||
{ "defName": "ProductLeather", "label": "product.leather", "kind": "material" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"defs": [
|
||||
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 80, "height": 50, "population": 40 },
|
||||
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 120, "height": 70, "population": 80 },
|
||||
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 180, "height": 110, "population": 140 }
|
||||
{ "defName": "Small", "label": "preset.small", "order": 0, "width": 128, "height": 80, "population": 60 },
|
||||
{ "defName": "Medium", "label": "preset.medium", "order": 1, "width": 200, "height": 120, "population": 110 },
|
||||
{ "defName": "Large", "label": "preset.large", "order": 2, "width": 300, "height": 180, "population": 180 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"inspect.tab.overview": "Overview",
|
||||
"inspect.tab.genes": "Genes",
|
||||
"inspect.tab.products": "Products",
|
||||
"inspect.tab.needs": "Needs",
|
||||
"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",
|
||||
@@ -31,6 +34,35 @@
|
||||
"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.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",
|
||||
"hediff.rut": "rut",
|
||||
"hediff.fever": "fever",
|
||||
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
||||
"hud.paused": "PAUSED",
|
||||
"menu.title": "LittleSim",
|
||||
@@ -123,6 +155,9 @@
|
||||
"pawn.wolf": "wolf",
|
||||
"pawn.muffalo": "muffalo",
|
||||
"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.connecting": "connecting…",
|
||||
"net.connected": "connected",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"inspect.tab.overview": "Обзор",
|
||||
"inspect.tab.genes": "Гены",
|
||||
"inspect.tab.products": "Продукты",
|
||||
"inspect.tab.needs": "Нужды",
|
||||
"inspect.tab.health": "Здоровье",
|
||||
"inspect.tab.mood": "Настроение",
|
||||
"inspect.stage": "Стадия: {0} ({1}/{2})",
|
||||
"inspect.growing": "Рост: {0:0}% до следующей стадии",
|
||||
"inspect.mature": "Полностью выросло",
|
||||
@@ -31,6 +34,35 @@
|
||||
"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.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": "зрение",
|
||||
"hediff.rut": "гон",
|
||||
"hediff.fever": "лихорадка",
|
||||
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
||||
"hud.paused": "ПАУЗА",
|
||||
"menu.title": "LittleSim",
|
||||
@@ -123,6 +155,9 @@
|
||||
"pawn.wolf": "волк",
|
||||
"pawn.muffalo": "муффало",
|
||||
"pawn.squirrel": "белка",
|
||||
"thought.gaveBirth": "родила потомство",
|
||||
"thought.mated": "спарилась",
|
||||
"thought.quenchedThirst": "напилась вволю",
|
||||
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||
"net.connecting": "подключение…",
|
||||
"net.connected": "подключено",
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
# Животные (зоология)
|
||||
|
||||
> Статус: **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 — экологический горизонт
|
||||
|
||||
То, ради чего вся глубина: превращает «зоопарк» в живую экосистему. Каждая фича опирается
|
||||
на уже построенные системы.
|
||||
|
||||
- **Хищный кластер.** Хищничество (волк→олень): диета = живая добыча; действие `Hunt`
|
||||
(выследить → преследовать → атака → **урон через здоровье A5** → добить → съесть труп).
|
||||
Это **первый «активный» (травмирующий) источник урона** для системы здоровья и
|
||||
полноценная Лотка–Вольтерра с хищником. Рядом: **падальщики** (едят трупы Fresh/Rotting)
|
||||
и **угроза/бегство** (хищник в радиусе `Sight` → страх → действие `Flee`, удар по
|
||||
настроению; стадо снижает страх).
|
||||
- **Родительская опека / импринтинг.** Детёныш ходит за матерью, голод гасится близостью
|
||||
к лактирующей матери до ювенильной стадии; мать ведёт/защищает. Реализм + детёныши не
|
||||
мрут от голода в одиночку.
|
||||
- **Эпидемии.** Болезни (A6) распространяются по контакту/плотности → плотностно-зависимый
|
||||
регулятор популяции (вспышки-крахи). Исход решает иммунитет (`GeneImmunityGain`).
|
||||
- **Миграция.** При локальном истощении корма / некомфортном климате — уход к лучшим
|
||||
клеткам (концепт: «голод+неурожай→миграция»). Эмерджентно из нужд + пространственного
|
||||
поиска с большим радиусом.
|
||||
|
||||
Сюда же тяготеют пункты бэклога: половой отбор (выбор партнёра), сезонный окрас,
|
||||
инбридинг-депрессия, возврат нутриентов в почву.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- Конкретный список `ThoughtDef`-событий для оленя (набор и величины offset/длительности)
|
||||
— детализируется при реализации A8.
|
||||
- Ассет: создать/найти спрайт скелета (`things/pawn/animal/deer/Skeleton_*`) к фазе A5.
|
||||
@@ -62,6 +62,117 @@ public sealed class PawnSave
|
||||
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>Сериализуемый труп (фаза 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>
|
||||
/// Полное состояние мира для сохранения/загрузки: конфиг мира (имя/размер/сид/сглаживание),
|
||||
/// прошедшее время симуляции и снимок всех жителей. Рельеф не сохраняется — он
|
||||
@@ -99,6 +210,12 @@ public sealed class WorldSave
|
||||
/// <summary>Снимок всех растений (геномы, позиции, возраст, стадия).</summary>
|
||||
public List<PlantSave> Plants { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок всех животных (геном, стадия/возраст, нужды, здоровье, настроение, беременность).</summary>
|
||||
public List<AnimalSave> Animals { get; set; } = [];
|
||||
|
||||
/// <summary>Снимок трупов (вид, позиция, таймер/стадия разложения, мясо).</summary>
|
||||
public List<CorpseSave> Corpses { get; set; } = [];
|
||||
|
||||
/// <summary>Конфиг мира для пересоздания сцены.</summary>
|
||||
public WorldConfig ToConfig() =>
|
||||
new()
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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>Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи.</summary>
|
||||
public sealed class Species
|
||||
{
|
||||
/// <summary>Исходный деф животного.</summary>
|
||||
public required AnimalDef Def { get; init; }
|
||||
|
||||
/// <summary>Спрайт взрослой самки (он же общий/fallback).</summary>
|
||||
public required Texture2DRegion Female { get; init; }
|
||||
|
||||
/// <summary>Спрайт взрослого самца (fallback — самка).</summary>
|
||||
public required Texture2DRegion Male { get; init; }
|
||||
|
||||
/// <summary>Спрайт детёныша/подростка (fallback — самка).</summary>
|
||||
public required Texture2DRegion Baby { get; init; }
|
||||
|
||||
/// <summary>Спрайт трупа (fallback — самка).</summary>
|
||||
public required Texture2DRegion Corpse { 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; }
|
||||
}
|
||||
|
||||
// Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные.
|
||||
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 female = atlases.GetRegion(device, def.Texture);
|
||||
_species[i] = new Species
|
||||
{
|
||||
Def = def,
|
||||
Female = female,
|
||||
Male = string.IsNullOrEmpty(def.MaleTexture)
|
||||
? female
|
||||
: atlases.GetRegion(device, def.MaleTexture),
|
||||
Baby = string.IsNullOrEmpty(def.BabyTexture)
|
||||
? female
|
||||
: atlases.GetRegion(device, def.BabyTexture),
|
||||
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
|
||||
? female
|
||||
: atlases.GetRegion(device, def.CorpseTexture),
|
||||
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),
|
||||
};
|
||||
_index[def] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
// Строит шаблон генома вида из ДАННЫХ (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,11 @@ public sealed class GameContent
|
||||
defs.RegisterType<ProductDef>("Product");
|
||||
defs.RegisterType<PlantDef>("Plant");
|
||||
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<WorldPresetDef>("WorldPreset");
|
||||
// Валидация имён при загрузке (фаза G5): гены и продукты обязаны иметь префикс типа.
|
||||
defs.RegisterValidator("Gene", "defName", "^Gene", "gene defName must start with 'Gene'");
|
||||
|
||||
@@ -190,8 +190,8 @@ public sealed class WorldPresetDef : Def
|
||||
public int Order { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное).</summary>
|
||||
public sealed class PawnDef : Def
|
||||
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное). Не sealed — <see cref="AnimalDef"/> наследует.</summary>
|
||||
public class PawnDef : Def
|
||||
{
|
||||
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
||||
public const string KindBeing = "being";
|
||||
@@ -208,3 +208,195 @@ public sealed class PawnDef : Def
|
||||
/// <summary>Вид: <see cref="KindBeing"/> или <see cref="KindAnimal"/>.</summary>
|
||||
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>Имя <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>
|
||||
/// Нужда (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>
|
||||
/// Часть тела/орган (вложенный объект <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>Модификаторы способностей: capacity id → множитель при тяжести 1 (лерп 1→factor по severity).</summary>
|
||||
public Dictionary<string, float> CapMods { get; init; } = new();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public static class GameLayers
|
||||
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
|
||||
public static LayerId Selection { get; private set; }
|
||||
|
||||
/// <summary>Регистрирует слои при первом вызове.</summary>
|
||||
/// <summary>Регистрирует слои при первом вызове (порядок = порядок отрисовки).</summary>
|
||||
public static void EnsureRegistered(Renderer2D renderer)
|
||||
{
|
||||
if (_registered)
|
||||
|
||||
@@ -65,6 +65,8 @@ public sealed class WorldScene : Scene
|
||||
private readonly List<Action> _speedRefreshers = [];
|
||||
|
||||
private PlantSet _plants = null!;
|
||||
private AnimalSet _animals = null!;
|
||||
private NeedSet _needs = null!;
|
||||
private float[] _cellFertility = [];
|
||||
private bool[] _cellLand = [];
|
||||
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
|
||||
@@ -109,6 +111,8 @@ public sealed class WorldScene : Scene
|
||||
|
||||
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
||||
_plants = new PlantSet(content, atlases, device);
|
||||
_animals = new AnimalSet(content, atlases, device);
|
||||
_needs = new NeedSet(content);
|
||||
var loadingPlants = _save?.Plants is { Count: > 0 };
|
||||
BuildTerrain(
|
||||
content,
|
||||
@@ -122,6 +126,17 @@ public sealed class WorldScene : Scene
|
||||
RestorePlants(content);
|
||||
}
|
||||
|
||||
// Животные: из сейва (сохранённая популяция — эволюция/состояние не теряются) либо
|
||||
// детерминированный спавн из сида (новый мир или старый сейв без животных).
|
||||
if (_save?.Animals is { Count: > 0 })
|
||||
{
|
||||
RestoreAnimals(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpawnAnimals();
|
||||
}
|
||||
|
||||
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
|
||||
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
|
||||
var lighting = this.UseLighting(
|
||||
@@ -155,7 +170,16 @@ public sealed class WorldScene : Scene
|
||||
atlases.GetRegion(device, "ui/overlays/selectionbracket"),
|
||||
GameLayers.Selection
|
||||
);
|
||||
_inspect = new InspectPanel(Store, _plants, content, climate, _selection, brackets);
|
||||
_inspect = new InspectPanel(
|
||||
Store,
|
||||
_plants,
|
||||
_animals,
|
||||
_needs,
|
||||
content,
|
||||
climate,
|
||||
_selection,
|
||||
brackets
|
||||
);
|
||||
desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root);
|
||||
|
||||
this.UseInspector(renderer);
|
||||
@@ -203,6 +227,68 @@ public sealed class WorldScene : Scene
|
||||
_config.Seed
|
||||
)
|
||||
);
|
||||
// Животные (фаза A2): нужды падают, ИИ выбирает действие, исполнение ведёт к цели и утоляет
|
||||
// нужду (еда выедает растения — контур ёмкости среды). Внешний вид тускнеет с острой нуждой.
|
||||
var shore = ComputeShore();
|
||||
var thoughts = new ThoughtSet(content);
|
||||
UpdateSystems.Add(new AnimalNeedsSystem(Context.Clock, SecondsPerDay, _needs));
|
||||
UpdateSystems.Add(
|
||||
new AnimalRutSystem(_animals, climate, content.Defs.Get<HediffDef>("Rut"))
|
||||
);
|
||||
UpdateSystems.Add(
|
||||
new AnimalHealthSystem(
|
||||
Store,
|
||||
_animals,
|
||||
CellSize,
|
||||
Context.Clock,
|
||||
SecondsPerDay,
|
||||
content.Defs.All<HediffDef>().Where(h => h.AmbientPerDay > 0f).ToArray(),
|
||||
_config.Seed + 0x3EAD
|
||||
)
|
||||
);
|
||||
// Настроение (A8) до выбора решения: стресс (низкое настроение) подавляет спаривание.
|
||||
UpdateSystems.Add(new AnimalMoodSystem(Context.Clock, SecondsPerDay, _needs));
|
||||
UpdateSystems.Add(
|
||||
new AnimalDecisionSystem(
|
||||
Store,
|
||||
Context.Clock,
|
||||
_animals,
|
||||
_needs,
|
||||
CellSize,
|
||||
shore,
|
||||
_config.Seed + 0x5EED
|
||||
)
|
||||
);
|
||||
UpdateSystems.Add(
|
||||
new AnimalActionSystem(
|
||||
Store,
|
||||
_plants,
|
||||
_animals,
|
||||
_needs,
|
||||
thoughts,
|
||||
Context.Clock,
|
||||
SecondsPerDay,
|
||||
_bounds
|
||||
)
|
||||
);
|
||||
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
|
||||
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
|
||||
UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, CellSize));
|
||||
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
|
||||
UpdateSystems.Add(
|
||||
new AnimalPregnancySystem(
|
||||
Store,
|
||||
_animals,
|
||||
_needs,
|
||||
thoughts,
|
||||
Context.Clock,
|
||||
SecondsPerDay,
|
||||
CellSize,
|
||||
maxAnimals: 500,
|
||||
_config.Seed + 0x4BED
|
||||
)
|
||||
);
|
||||
|
||||
UpdateSystems.Add(
|
||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||
);
|
||||
@@ -422,11 +508,108 @@ public sealed class WorldScene : Scene
|
||||
}
|
||||
);
|
||||
|
||||
SaveAnimals(save);
|
||||
|
||||
new SaveStore().Write(save);
|
||||
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
|
||||
Log.Info(
|
||||
$"World '{_config.Name}' saved ({save.Plants.Count} plants, "
|
||||
+ $"{save.Animals.Count} animals, {save.Corpses.Count} corpses)"
|
||||
);
|
||||
return _config.Name;
|
||||
}
|
||||
|
||||
// Снимок популяции животных и трупов: всё нужное, чтобы эволюция/состояние пережили сейв/лоад
|
||||
// (а не пересыпались из сида). Health/Mood/Pregnant берём через entity — запрос ограничен 5 типами.
|
||||
private void SaveAnimals(WorldSave save)
|
||||
{
|
||||
Store
|
||||
.Query<AnimalOrganism, AnimalGrowth, AnimalNeeds, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(
|
||||
ref AnimalOrganism org,
|
||||
ref AnimalGrowth grow,
|
||||
ref AnimalNeeds needs,
|
||||
ref Transform2D transform,
|
||||
Entity entity
|
||||
) =>
|
||||
{
|
||||
var record = new AnimalSave
|
||||
{
|
||||
Species = _animals[org.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
Generation = org.Generation,
|
||||
Stage = grow.Stage,
|
||||
AgeDays = grow.AgeDays,
|
||||
Genome = org.Genome.ToDictionary(),
|
||||
Needs = needs.Values,
|
||||
Starve = needs.Starve,
|
||||
};
|
||||
|
||||
if (entity.GetComponent<Health>().State is { } health)
|
||||
{
|
||||
record.BloodLevel = health.BloodLevel;
|
||||
record.PartHp = health.Parts.Select(p => p.Hp).ToArray();
|
||||
foreach (var hediff in health.Hediffs)
|
||||
{
|
||||
record.Hediffs.Add(
|
||||
new HediffSave
|
||||
{
|
||||
DefName = hediff.Def.DefName,
|
||||
Severity = hediff.Severity,
|
||||
Immunity = hediff.Immunity,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var mood = entity.GetComponent<Mood>();
|
||||
record.Mood = mood.Value;
|
||||
record.MoodActive = mood.Active;
|
||||
if (mood.Thoughts is not null)
|
||||
{
|
||||
foreach (var thought in mood.Thoughts)
|
||||
{
|
||||
record.Thoughts.Add(
|
||||
new ThoughtSave
|
||||
{
|
||||
DefName = thought.Def.DefName,
|
||||
RemainingDays = thought.RemainingDays,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent<Pregnant>(out var preg))
|
||||
{
|
||||
record.Pregnant = true;
|
||||
record.FatherGenome = preg.FatherGenome.ToDictionary();
|
||||
record.DueInDays = preg.DueInDays;
|
||||
record.Litter = preg.Litter;
|
||||
}
|
||||
|
||||
save.Animals.Add(record);
|
||||
}
|
||||
);
|
||||
|
||||
Store
|
||||
.Query<Corpse, Transform2D>()
|
||||
.ForEachEntity(
|
||||
(ref Corpse corpse, ref Transform2D transform, Entity _) =>
|
||||
save.Corpses.Add(
|
||||
new CorpseSave
|
||||
{
|
||||
Species = _animals[corpse.Species].Def.DefName,
|
||||
X = transform.Position.X,
|
||||
Y = transform.Position.Y,
|
||||
RotDays = corpse.RotDays,
|
||||
BodySize = corpse.Meat / AnimalFactory.MeatPerBodySize,
|
||||
Meat = corpse.Meat,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void RestorePlants(GameContent content)
|
||||
{
|
||||
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
|
||||
@@ -454,6 +637,218 @@ public sealed class WorldScene : Scene
|
||||
}
|
||||
}
|
||||
|
||||
// Восстанавливает популяцию животных и трупы из сейва: фабрика собирает особь из генома (геном
|
||||
// несёт и пол), затем динамическое состояние (нужды/стадия/здоровье/настроение/беременность)
|
||||
// накладывается поверх. Старый/битый сейв без генома — особь регенерируется из шаблона вида.
|
||||
private void RestoreAnimals(GameContent content)
|
||||
{
|
||||
var fallback = new Random(_config.Seed);
|
||||
foreach (var saved in _save!.Animals)
|
||||
{
|
||||
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||
{
|
||||
continue; // вид пропал (мод убрали) — пропускаем особь
|
||||
}
|
||||
|
||||
var index = _animals.IndexOf(def);
|
||||
var genome = saved.Genome is { Count: > 0 }
|
||||
? new Genome(saved.Genome)
|
||||
: _animals.GenerateGenome(index, fallback);
|
||||
var entity = AnimalFactory.Create(
|
||||
Store,
|
||||
_animals,
|
||||
_needs,
|
||||
index,
|
||||
new Vector2(saved.X, saved.Y),
|
||||
saved.AgeDays,
|
||||
genome,
|
||||
saved.Generation,
|
||||
CellSize
|
||||
);
|
||||
RestoreAnimalState(entity, saved, content);
|
||||
}
|
||||
|
||||
foreach (var saved in _save.Corpses)
|
||||
{
|
||||
if (!content.Defs.TryGet<AnimalDef>(saved.Species, out var def))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var corpse = AnimalFactory.CreateCorpse(
|
||||
Store,
|
||||
_animals,
|
||||
_animals.IndexOf(def),
|
||||
new Vector2(saved.X, saved.Y),
|
||||
saved.BodySize,
|
||||
CellSize
|
||||
);
|
||||
ref var data = ref corpse.GetComponent<Corpse>();
|
||||
data.RotDays = saved.RotDays;
|
||||
data.Stage = -1; // форсируем пере-тинт стадии в CorpseSystem на следующем тике
|
||||
data.Meat = saved.Meat;
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Restored {_save.Animals.Count} animals, {_save.Corpses.Count} corpses from save"
|
||||
);
|
||||
}
|
||||
|
||||
// Накладывает на свежесозданную особь сохранённое динамическое состояние. Ref-компоненты правим до
|
||||
// AddComponent(Pregnant) (структурное изменение инвалидирует ссылки); Health — managed, безопасен.
|
||||
private void RestoreAnimalState(Entity entity, AnimalSave saved, GameContent content)
|
||||
{
|
||||
ref var needs = ref entity.GetComponent<AnimalNeeds>();
|
||||
if (saved.Needs is { Length: > 0 } && saved.Needs.Length == _needs.Count)
|
||||
{
|
||||
needs.Values = saved.Needs; // состав совпал — иначе остаются свежие из фабрики
|
||||
}
|
||||
|
||||
needs.Starve = saved.Starve;
|
||||
entity.GetComponent<AnimalGrowth>().Stage = saved.Stage;
|
||||
|
||||
ref var mood = ref entity.GetComponent<Mood>();
|
||||
mood.Value = saved.Mood;
|
||||
mood.Active = saved.MoodActive;
|
||||
mood.Thoughts.Clear();
|
||||
foreach (var thought in saved.Thoughts)
|
||||
{
|
||||
if (content.Defs.TryGet<ThoughtDef>(thought.DefName, out var tdef))
|
||||
{
|
||||
mood.Thoughts.Add(
|
||||
new ThoughtInstance { Def = tdef, RemainingDays = thought.RemainingDays }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.GetComponent<Health>().State is { } health)
|
||||
{
|
||||
health.BloodLevel = saved.BloodLevel;
|
||||
for (var i = 0; i < health.Parts.Length && i < saved.PartHp.Length; i++)
|
||||
{
|
||||
health.Parts[i].Hp = saved.PartHp[i];
|
||||
}
|
||||
|
||||
foreach (var hediff in saved.Hediffs)
|
||||
{
|
||||
if (content.Defs.TryGet<HediffDef>(hediff.DefName, out var hdef))
|
||||
{
|
||||
health.AddHediff(hdef, hediff.Severity, hediff.Immunity);
|
||||
}
|
||||
}
|
||||
|
||||
health.RecomputeCapacities();
|
||||
}
|
||||
|
||||
if (saved.Pregnant && saved.FatherGenome is { Count: > 0 })
|
||||
{
|
||||
entity.AddComponent(
|
||||
new Pregnant
|
||||
{
|
||||
FatherGenome = new Genome(saved.FatherGenome),
|
||||
DueInDays = saved.DueInDays,
|
||||
Litter = saved.Litter,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Детерминированный начальный спавн животных по данным: каждый вид с SpawnPer1000Cells > 0
|
||||
// рассыпается по клеткам суши с плотностью из дефа; геном и возраст особи — из шаблона вида.
|
||||
private void SpawnAnimals()
|
||||
{
|
||||
var landCells = 0;
|
||||
for (var i = 0; i < _cellLand.Length; i++)
|
||||
{
|
||||
if (_cellLand[i])
|
||||
{
|
||||
landCells++;
|
||||
}
|
||||
}
|
||||
|
||||
if (_animals.Count == 0 || landCells == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var random = new Random(_config.Seed + 0x5EED);
|
||||
var spawned = 0;
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var def = _animals[s].Def;
|
||||
if (def.SpawnPer1000Cells <= 0f)
|
||||
{
|
||||
continue; // вид не спавнится сам (напр. появляется только в сейве/событием)
|
||||
}
|
||||
|
||||
var count = Math.Max(1, (int)MathF.Round(landCells / 1000f * def.SpawnPer1000Cells));
|
||||
var maxAge =
|
||||
(def.Genome.TryGetValue("GeneLifespan", out var lifespan) ? lifespan : 160f)
|
||||
* 0.85f;
|
||||
var placed = 0;
|
||||
var attempts = 0;
|
||||
while (placed < count && attempts < count * 50)
|
||||
{
|
||||
attempts++;
|
||||
var cell = random.Next(_cellLand.Length);
|
||||
if (!_cellLand[cell])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cx = cell % _config.Width;
|
||||
var cy = cell / _config.Width;
|
||||
var px = (cx + 0.2f + random.NextSingle() * 0.6f) * CellSize;
|
||||
var py = (cy + 0.2f + random.NextSingle() * 0.6f) * CellSize;
|
||||
AnimalFactory.Create(
|
||||
Store,
|
||||
_animals,
|
||||
_needs,
|
||||
s,
|
||||
new Vector2(px, py),
|
||||
ageDays: random.NextSingle() * maxAge,
|
||||
_animals.GenerateGenome(s, random),
|
||||
generation: 0,
|
||||
CellSize
|
||||
);
|
||||
placed++;
|
||||
}
|
||||
|
||||
spawned += placed;
|
||||
}
|
||||
|
||||
Log.Info($"Spawned {spawned} animals ({_animals.Count} species)");
|
||||
}
|
||||
|
||||
// Кромка воды: центры клеток суши, граничащих с водой — куда звери ходят пить (вода непроходима).
|
||||
private Vector2[] ComputeShore()
|
||||
{
|
||||
var shore = new List<Vector2>();
|
||||
for (var y = 0; y < _config.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < _config.Width; x++)
|
||||
{
|
||||
var cell = y * _config.Width + x;
|
||||
if (!_cellLand[cell])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nearWater =
|
||||
(x > 0 && !_cellLand[cell - 1])
|
||||
|| (x < _config.Width - 1 && !_cellLand[cell + 1])
|
||||
|| (y > 0 && !_cellLand[cell - _config.Width])
|
||||
|| (y < _config.Height - 1 && !_cellLand[cell + _config.Width]);
|
||||
if (nearWater)
|
||||
{
|
||||
shore.Add(new Vector2((x + 0.5f) * CellSize, (y + 0.5f) * CellSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shore.ToArray();
|
||||
}
|
||||
|
||||
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
|
||||
private bool[] BuildOccluders()
|
||||
{
|
||||
@@ -552,6 +947,36 @@ public sealed class WorldScene : Scene
|
||||
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
||||
(c, args) => RunPlantDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"animal",
|
||||
"animal <species> [seed] — sample an animal genome and show its gene-driven traits",
|
||||
(c, args) => RunAnimalDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"popstats",
|
||||
"popstats [species] — live population trait means and generation span (selection drift)",
|
||||
(c, args) => RunPopStats(c, args)
|
||||
);
|
||||
console.Register(
|
||||
"body",
|
||||
"body <species> — dump a species body parts and a healthy individual's capacities",
|
||||
(c, args) => RunBodyDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"infect",
|
||||
"infect [hediff] — give every animal a disease (default Fever) to test the health system",
|
||||
(c, args) => RunInfect(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"intel",
|
||||
"intel <species> [consciousness] — effective intelligence (brain x consciousness) and which needs it gates",
|
||||
(c, args) => RunIntel(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"mood",
|
||||
"mood [species] — live population mood (avg/min/max) and content/stressed counts",
|
||||
(c, args) => RunMood(c, args)
|
||||
);
|
||||
console.Register(
|
||||
"menu",
|
||||
"menu — return to the main menu",
|
||||
@@ -659,6 +1084,305 @@ public sealed class WorldScene : Scene
|
||||
}
|
||||
}
|
||||
|
||||
// Демонстрация животного (фаза A1): по виду берём его набор генов, генерируем особь и печатаем
|
||||
// признаки, вычисленные формулами генов (тело/мозг/метаболизм/чувства/мех).
|
||||
private void RunAnimalDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine("usage: animal <species> [seed] e.g. animal Deer");
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _animals.IndexOf(def);
|
||||
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||
var genome = _animals.GenerateGenome(index, new Random(seed));
|
||||
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
|
||||
var sex = AnimalFactory.IsMaleGenome(genome) ? "male" : "female";
|
||||
|
||||
console.WriteLine($"{def.DefName} ({sex}, seed {seed}):");
|
||||
console.WriteLine(
|
||||
$" body {traits.BodySize:0.##} cells, maturity {traits.MaturityAge:0} d, "
|
||||
+ $"lifespan {traits.Lifespan:0} d, brain {traits.BrainSize:0.##}, metabolism {traits.Metabolism:0.##}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" move {traits.MoveSpeed:0.##}, vision {traits.Vision:0.##}, blood {traits.BloodVolume:0.##}, "
|
||||
+ $"insulation {traits.Insulation:0.##}, furHue {traits.FurHue:0.##}"
|
||||
);
|
||||
}
|
||||
|
||||
// Наблюдаемость отбора (фаза A2): средние ключевых признаков живой популяции и размах поколений —
|
||||
// видно дрейф генов под отбором. Без аргумента — все виды; с аргументом — один вид.
|
||||
private void RunPopStats(DevConsole console, string[] args)
|
||||
{
|
||||
if (_animals.Count == 0)
|
||||
{
|
||||
console.WriteLine("no animal species");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = new int[_animals.Count];
|
||||
var body = new double[_animals.Count];
|
||||
var brain = new double[_animals.Count];
|
||||
var life = new double[_animals.Count];
|
||||
var move = new double[_animals.Count];
|
||||
var maxGen = new int[_animals.Count];
|
||||
|
||||
Store
|
||||
.Query<AnimalOrganism>()
|
||||
.ForEachEntity(
|
||||
(ref AnimalOrganism o, Entity _) =>
|
||||
{
|
||||
var s = o.Species;
|
||||
count[s]++;
|
||||
body[s] += o.Traits.BodySize;
|
||||
brain[s] += o.Traits.BrainSize;
|
||||
life[s] += o.Traits.Lifespan;
|
||||
move[s] += o.Traits.MoveSpeed;
|
||||
if (o.Generation > maxGen[s])
|
||||
{
|
||||
maxGen[s] = o.Generation;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var filter = args.Length > 0 ? args[0] : null;
|
||||
var any = false;
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var name = _animals[s].Def.DefName;
|
||||
if (
|
||||
count[s] == 0
|
||||
|| (
|
||||
filter is not null
|
||||
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
var c = count[s];
|
||||
console.WriteLine(
|
||||
$"{name}: n={c}, gen 0..{maxGen[s]}, body {body[s] / c:0.##}, "
|
||||
+ $"brain {brain[s] / c:0.##}, lifespan {life[s] / c:0} d, move {move[s] / c:0.##}"
|
||||
);
|
||||
}
|
||||
|
||||
if (!any)
|
||||
{
|
||||
console.WriteLine(filter is null ? "no animals alive" : $"no '{filter}' alive");
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдаемость здоровья (фаза A5): дерево частей тела вида и способности здоровой особи.
|
||||
private void RunBodyDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine("usage: body <species> e.g. body Deer");
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(def.Body) || !content.Defs.TryGet<BodyDef>(def.Body, out var body))
|
||||
{
|
||||
console.WriteLine($"{def.DefName}: no body");
|
||||
return;
|
||||
}
|
||||
|
||||
console.WriteLine($"{def.DefName} body '{body.DefName}': {body.Parts.Length} parts");
|
||||
foreach (var part in body.Parts)
|
||||
{
|
||||
var caps =
|
||||
part.Capacities.Count > 0
|
||||
? " ["
|
||||
+ string.Join(
|
||||
", ",
|
||||
part.Capacities.Select(kv => $"{kv.Key} {kv.Value:0.##}")
|
||||
)
|
||||
+ "]"
|
||||
: "";
|
||||
console.WriteLine(
|
||||
$" {part.Name}{(part.Vital ? " *vital" : "")} hp{part.MaxHp:0}{caps}"
|
||||
);
|
||||
}
|
||||
|
||||
var state = HealthState.Create(body, 1f);
|
||||
console.WriteLine(
|
||||
"capacities (healthy): "
|
||||
+ string.Join(
|
||||
", ",
|
||||
state.Capacities.OrderBy(k => k.Key).Select(kv => $"{kv.Key} {kv.Value:0.##}")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Тест системы здоровья (фаза A6): навешивает болезнь всем живым животным.
|
||||
private void RunInfect(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var name = args.Length > 0 ? args[0] : "Fever";
|
||||
if (!content.Defs.TryGet<HediffDef>(name, out var def))
|
||||
{
|
||||
console.WriteLine($"no hediff '{name}'");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
Store
|
||||
.Query<Health>()
|
||||
.ForEachEntity(
|
||||
(ref Health h, Entity _) =>
|
||||
{
|
||||
h.State?.Add(def);
|
||||
count++;
|
||||
}
|
||||
);
|
||||
console.WriteLine($"infected {count} animals with {def.DefName}");
|
||||
}
|
||||
|
||||
// Наблюдаемость интеллекта (фаза A7): мозг особи (ген) × сознание = эффективный интеллект, который
|
||||
// гейтит набор нужд/действий. Показывает активные/отключённые нужды при заданном уровне сознания
|
||||
// (по умолчанию 1 — здоров; меньше — имитация повреждения мозга/болезни/боли).
|
||||
private void RunIntel(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Animal");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine(
|
||||
"usage: intel <species> [consciousness 0..1] [seed] e.g. intel Deer 0.6"
|
||||
);
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<AnimalDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no animal '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var consciousness =
|
||||
args.Length > 1
|
||||
? Math.Clamp(
|
||||
float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture),
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
: 1f;
|
||||
var seed = args.Length > 2 ? int.Parse(args[2]) : Random.Shared.Next();
|
||||
var index = _animals.IndexOf(def);
|
||||
var genome = _animals.GenerateGenome(index, new Random(seed));
|
||||
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, _animals.GeneRegistry));
|
||||
var intelligence = traits.BrainSize * consciousness;
|
||||
|
||||
console.WriteLine(
|
||||
$"{def.DefName} (seed {seed}): brain {traits.BrainSize:0.##} x consciousness {consciousness:0.##} "
|
||||
+ $"= intelligence {intelligence:0.###}"
|
||||
);
|
||||
var active = new List<string>();
|
||||
var gated = new List<string>();
|
||||
for (var k = 0; k < _needs.Count; k++)
|
||||
{
|
||||
var need = _needs[k];
|
||||
(need.MinBrain > intelligence ? gated : active).Add(
|
||||
$"{need.DefName}({need.MinBrain:0.##})"
|
||||
);
|
||||
}
|
||||
|
||||
console.WriteLine($" active: {(active.Count > 0 ? string.Join(", ", active) : "—")}");
|
||||
console.WriteLine($" gated: {(gated.Count > 0 ? string.Join(", ", gated) : "—")}");
|
||||
}
|
||||
|
||||
// Наблюдаемость настроения (фаза A8): среднее/мин/макс настроения живой популяции по виду и счётчики
|
||||
// довольных (>0.7) / угнетённых (<0.35). Считает только особей с активным настроением (тир 4+).
|
||||
private void RunMood(DevConsole console, string[] args)
|
||||
{
|
||||
if (_animals.Count == 0)
|
||||
{
|
||||
console.WriteLine("no animal species");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = new int[_animals.Count];
|
||||
var sum = new double[_animals.Count];
|
||||
var min = new float[_animals.Count];
|
||||
var max = new float[_animals.Count];
|
||||
var content_ = new int[_animals.Count];
|
||||
var stressed = new int[_animals.Count];
|
||||
Array.Fill(min, 1f);
|
||||
|
||||
Store
|
||||
.Query<AnimalOrganism, Mood>()
|
||||
.ForEachEntity(
|
||||
(ref AnimalOrganism o, ref Mood m, Entity _) =>
|
||||
{
|
||||
if (!m.Active)
|
||||
{
|
||||
return; // тир ниже зверя — настроения нет
|
||||
}
|
||||
|
||||
var s = o.Species;
|
||||
count[s]++;
|
||||
sum[s] += m.Value;
|
||||
min[s] = MathF.Min(min[s], m.Value);
|
||||
max[s] = MathF.Max(max[s], m.Value);
|
||||
if (m.Value > 0.7f)
|
||||
{
|
||||
content_[s]++;
|
||||
}
|
||||
else if (m.Value < 0.35f)
|
||||
{
|
||||
stressed[s]++;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var filter = args.Length > 0 ? args[0] : null;
|
||||
var any = false;
|
||||
for (var s = 0; s < _animals.Count; s++)
|
||||
{
|
||||
var name = _animals[s].Def.DefName;
|
||||
if (
|
||||
count[s] == 0
|
||||
|| (
|
||||
filter is not null
|
||||
&& !string.Equals(name, filter, StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
any = true;
|
||||
console.WriteLine(
|
||||
$"{name}: n={count[s]}, mood {sum[s] / count[s]:0.##} (min {min[s]:0.##}, max {max[s]:0.##}), "
|
||||
+ $"content {content_[s]}, stressed {stressed[s]}"
|
||||
);
|
||||
}
|
||||
|
||||
if (!any)
|
||||
{
|
||||
console.WriteLine(filter is null ? "no animals with mood" : $"no '{filter}' with mood");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProductLabel(GameContent content, string productDefName) =>
|
||||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||
? content.Languages.Get(product.Label)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
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,
|
||||
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);
|
||||
|
||||
return 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 = [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Сколько мяса даёт труп на единицу размера тела (для будущей разделки/падальщиков).</summary>
|
||||
public const float MeatPerBodySize = 25f;
|
||||
|
||||
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
|
||||
private static readonly Color CorpseFreshTint = new(176, 148, 128);
|
||||
|
||||
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль, запас мяса (∝ размер тела).</summary>
|
||||
public static Entity CreateCorpse(
|
||||
EntityStore store,
|
||||
AnimalSet animals,
|
||||
int species,
|
||||
Vector2 position,
|
||||
float bodySize,
|
||||
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 = bodySize * MeatPerBodySize,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <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) =>
|
||||
string.Equals(sp.Stages[stage].Texture, "baby", StringComparison.Ordinal) ? sp.Baby
|
||||
: isMale ? sp.Male
|
||||
: sp.Female;
|
||||
|
||||
/// <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,417 @@
|
||||
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>Продолжительность жизни (игровых дней).</summary>
|
||||
public float Lifespan;
|
||||
|
||||
/// <summary>Сезон гона (0 весна … 3 зима).</summary>
|
||||
public int BreedingSeason;
|
||||
|
||||
/// <summary>Срок вынашивания (игровых дней).</summary>
|
||||
public float GestationDays;
|
||||
|
||||
/// <summary>Размер помёта.</summary>
|
||||
public float LitterSize;
|
||||
|
||||
/// <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"),
|
||||
Lifespan = T("lifespan"),
|
||||
BreedingSeason = (int)MathF.Round(Math.Clamp(T("breedingSeason"), 0f, 3f)),
|
||||
GestationDays = T("gestationDays"),
|
||||
LitterSize = T("litterSize"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 по тяжести). Вызывать после изменения частей/крови/хедифов.
|
||||
/// </summary>
|
||||
public void RecomputeCapacities()
|
||||
{
|
||||
var pain = 0f;
|
||||
foreach (var h in _hediffs)
|
||||
{
|
||||
pain += h.Def.Pain * h.Severity;
|
||||
}
|
||||
|
||||
Pain = Math.Clamp(pain, 0f, 1f);
|
||||
|
||||
var caps = CapacityCalc.Compute(Parts, BloodLevel, Pain);
|
||||
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, если какая-то болезнь достигла летальной тяжести.
|
||||
/// </summary>
|
||||
public bool AdvanceHediffs(float days)
|
||||
{
|
||||
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;
|
||||
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>Здоровье особи: 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>Инстанс мысли настроения на особи (фаза 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,72 @@
|
||||
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 Sight = "Sight";
|
||||
public const string Digestion = "Digestion";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обобщённый расчёт способностей (capacities) из частей тела и крови (фаза A5). Сырой вклад способности
|
||||
/// = Σ(доля части × доля её HP). Затем известные способности домножаются по зависимостям (кровь →
|
||||
/// сердце/лёгкие → сознание → движение/зрение), как в модели RimWorld; неизвестные (модовые) проходят
|
||||
/// сырыми. У здоровой особи (полное HP, кровь 1, боль 0) все способности = 1.
|
||||
/// </summary>
|
||||
public static class CapacityCalc
|
||||
{
|
||||
/// <summary>Считает карту способностей из инстансов частей, уровня крови (0..1) и боли (0..1).</summary>
|
||||
public static Dictionary<string, float> Compute(PartInstance[] parts, float blood, float pain)
|
||||
{
|
||||
var raw = new Dictionary<string, float>(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)
|
||||
{
|
||||
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 consciousness =
|
||||
Raw(AnimalCapacities.Consciousness)
|
||||
* Clamp01(bloodPumping)
|
||||
* Clamp01(breathing)
|
||||
* (1f - Clamp01(pain));
|
||||
|
||||
var result = new Dictionary<string, float>(System.StringComparer.Ordinal)
|
||||
{
|
||||
[AnimalCapacities.BloodPumping] = bloodPumping,
|
||||
[AnimalCapacities.Breathing] = breathing,
|
||||
[AnimalCapacities.Consciousness] = consciousness,
|
||||
[AnimalCapacities.Moving] = Raw(AnimalCapacities.Moving) * consciousness * Clamp01(bloodPumping),
|
||||
[AnimalCapacities.Sight] = Raw(AnimalCapacities.Sight) * consciousness,
|
||||
[AnimalCapacities.Digestion] = Raw(AnimalCapacities.Digestion) * Clamp01(bloodPumping),
|
||||
};
|
||||
|
||||
// Модовые способности, не охваченные зависимостями, — сырыми.
|
||||
foreach (var (capacity, value) in raw)
|
||||
{
|
||||
result.TryAdd(capacity, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -74,41 +75,62 @@ public sealed class SelectionSystem(
|
||||
}
|
||||
}
|
||||
|
||||
// Ближайшее растение, чья ограничивающая окружность спрайта накрывает точку мира. Запрос
|
||||
// ограничен PlantGrowth — так из пикинга выпадают спрайты рамки выделения и прочий не-контент.
|
||||
// Ближайший осматриваемый объект (растение или животное), чья ограничивающая окружность спрайта
|
||||
// накрывает точку мира. Запросы ограничены PlantGrowth/AnimalOrganism — так из пикинга выпадают
|
||||
// спрайты рамки выделения, трупы и прочий не-контент. Из двух кандидатов берётся более близкий.
|
||||
private bool TryPick(Vector2 world, out int id)
|
||||
{
|
||||
id = -1;
|
||||
var bestDistance = float.MaxValue;
|
||||
var best = float.MaxValue;
|
||||
foreach (
|
||||
var (transforms, sprites, _, entities) in store
|
||||
.Query<Transform2D, Sprite, PlantGrowth>()
|
||||
.Chunks
|
||||
)
|
||||
{
|
||||
var t = transforms.Span;
|
||||
var s = sprites.Span;
|
||||
for (var i = 0; i < t.Length; i++)
|
||||
Scan(world, transforms.Span, sprites.Span, entities, ref id, ref best);
|
||||
}
|
||||
|
||||
foreach (
|
||||
var (transforms, sprites, _, entities) in store
|
||||
.Query<Transform2D, Sprite, AnimalOrganism>()
|
||||
.Chunks
|
||||
)
|
||||
{
|
||||
if (s[i].Region is not { } region)
|
||||
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 t[i],
|
||||
in transforms[i],
|
||||
region,
|
||||
s[i].Origin
|
||||
sprites[i].Origin
|
||||
);
|
||||
var distance = Vector2.DistanceSquared(center, world);
|
||||
if (distance <= radius * radius && distance < bestDistance)
|
||||
if (distance <= radius * radius && distance < best)
|
||||
{
|
||||
bestDistance = distance;
|
||||
best = distance;
|
||||
id = entities.EntityAt(i).Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return id >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Friflo.Engine.ECS;
|
||||
@@ -16,10 +17,11 @@ namespace LittleSim.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта
|
||||
/// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками — «Обзор»,
|
||||
/// «Гены», «Продукты». Источник выделения — <see cref="Selection"/>; панель только читает компоненты
|
||||
/// и обновляется каждый кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут
|
||||
/// через <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
|
||||
/// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками. Набор вкладок
|
||||
/// зависит от типа: растение — «Обзор/Гены/Продукты», животное — «Обзор/Нужды/Здоровье/Настроение».
|
||||
/// Источник выделения — <see cref="Selection"/>; панель только читает компоненты и обновляется каждый
|
||||
/// кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут через
|
||||
/// <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
|
||||
/// </summary>
|
||||
internal sealed class InspectPanel
|
||||
{
|
||||
@@ -28,10 +30,18 @@ internal sealed class InspectPanel
|
||||
Overview,
|
||||
Genes,
|
||||
Products,
|
||||
Needs,
|
||||
Health,
|
||||
Mood,
|
||||
}
|
||||
|
||||
private static readonly Tab[] PlantTabs = [Tab.Overview, Tab.Genes, Tab.Products];
|
||||
private static readonly Tab[] AnimalTabs = [Tab.Overview, Tab.Needs, Tab.Health, Tab.Mood];
|
||||
|
||||
private readonly EntityStore _store;
|
||||
private readonly PlantSet _plants;
|
||||
private readonly AnimalSet _animals;
|
||||
private readonly NeedSet _needs;
|
||||
private readonly GameContent _content;
|
||||
private readonly Climate _climate;
|
||||
private readonly Selection _selection;
|
||||
@@ -46,6 +56,8 @@ internal sealed class InspectPanel
|
||||
public InspectPanel(
|
||||
EntityStore store,
|
||||
PlantSet plants,
|
||||
AnimalSet animals,
|
||||
NeedSet needs,
|
||||
GameContent content,
|
||||
Climate climate,
|
||||
Selection selection,
|
||||
@@ -54,6 +66,8 @@ internal sealed class InspectPanel
|
||||
{
|
||||
_store = store;
|
||||
_plants = plants;
|
||||
_animals = animals;
|
||||
_needs = needs;
|
||||
_content = content;
|
||||
_climate = climate;
|
||||
_selection = selection;
|
||||
@@ -66,6 +80,9 @@ internal sealed class InspectPanel
|
||||
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.Health, "inspect.tab.health");
|
||||
AddTab(tabBar, Tab.Mood, "inspect.tab.mood");
|
||||
|
||||
_panel = new VerticalStackPanel
|
||||
{
|
||||
@@ -99,7 +116,7 @@ internal sealed class InspectPanel
|
||||
|
||||
/// <summary>
|
||||
/// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает
|
||||
/// рамку к объекту и пересобирает текст активной вкладки. Не-растения панель не показывает.
|
||||
/// рамку к объекту и пересобирает текст активной вкладки. Осматриваются растения и животные.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
@@ -107,19 +124,19 @@ internal sealed class InspectPanel
|
||||
!_selection.HasSelection
|
||||
|| !_store.TryGetEntityById(_selection.EntityId, out var entity)
|
||||
|| entity.IsNull
|
||||
|| !entity.HasComponent<PlantGrowth>()
|
||||
|| !entity.HasComponent<PlantOrganism>()
|
||||
|| !entity.HasComponent<Transform2D>()
|
||||
|| !entity.HasComponent<Sprite>()
|
||||
)
|
||||
{
|
||||
if (_selection.HasSelection)
|
||||
{
|
||||
_selection.Clear(); // сущность исчезла (растение погибло)
|
||||
ClearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
_panel.Visible = false;
|
||||
_brackets.Hide();
|
||||
var isPlant = entity.HasComponent<PlantGrowth>() && entity.HasComponent<PlantOrganism>();
|
||||
var isAnimal = entity.HasComponent<AnimalOrganism>();
|
||||
if (!isPlant && !isAnimal)
|
||||
{
|
||||
ClearSelection(); // не контент (труп/прочее) — пока не осматривается
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,10 +152,29 @@ internal sealed class InspectPanel
|
||||
_brackets.Show(center, radius);
|
||||
}
|
||||
|
||||
UpdateText(entity);
|
||||
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) };
|
||||
@@ -147,7 +183,23 @@ internal sealed class InspectPanel
|
||||
_tabs.Add((tab, button));
|
||||
}
|
||||
|
||||
private void UpdateText(Entity entity)
|
||||
// Показывает только вкладки текущего типа, подсвечивает активную; если активная не из набора —
|
||||
// откатывает на первую (Обзор) при смене типа выбранного объекта.
|
||||
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>();
|
||||
@@ -157,10 +209,7 @@ internal sealed class InspectPanel
|
||||
var traits = org.Traits;
|
||||
|
||||
_title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : "");
|
||||
foreach (var (tab, button) in _tabs)
|
||||
{
|
||||
button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted;
|
||||
}
|
||||
ShowTabs(PlantTabs);
|
||||
|
||||
var text = new StringBuilder();
|
||||
switch (_tab)
|
||||
@@ -179,6 +228,35 @@ internal sealed class InspectPanel
|
||||
_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.Health:
|
||||
BuildAnimalHealth(text, entity);
|
||||
break;
|
||||
case Tab.Mood:
|
||||
BuildAnimalMood(text, entity);
|
||||
break;
|
||||
}
|
||||
|
||||
_body.Text = text.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private void BuildOverview(
|
||||
StringBuilder text,
|
||||
Entity entity,
|
||||
@@ -296,6 +374,146 @@ internal sealed class InspectPanel
|
||||
}
|
||||
}
|
||||
|
||||
// — Животное: Обзор — стадия, возраст, пол/поколение, беременность.
|
||||
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));
|
||||
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 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.Parts.Length > 0)
|
||||
{
|
||||
text.AppendLine(languages.Get("inspect.health.caps"));
|
||||
AppendCapacity(text, state, AnimalCapacities.Consciousness, "cap.consciousness");
|
||||
AppendCapacity(text, state, AnimalCapacities.Moving, "cap.moving");
|
||||
AppendCapacity(text, state, AnimalCapacities.Sight, "cap.sight");
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user