Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c47f99bc29 | ||
|
|
9c9502414b | ||
|
|
b9bb7006be | ||
|
|
c60ae072e9 | ||
|
|
f46ae15dc6 | ||
|
|
6b29a40b7f | ||
|
|
6f38d949ab | ||
|
|
cc1841c2e2 | ||
|
|
4fda693994 | ||
|
|
d712afa518 |
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Контент-гены (фаза G4+): плодоношение, добыча, цвет, производные и дискретные морфы.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
|
||||||
|
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
|
||||||
|
"effects": { "fruitYield": "value" } },
|
||||||
|
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
|
||||||
|
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
|
||||||
|
"effects": { "fruitSeason": "value" } },
|
||||||
|
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
|
||||||
|
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
|
||||||
|
"effects": { "harvestAmount": "value" } },
|
||||||
|
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
|
||||||
|
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
|
||||||
|
"effects": { "leafHue": "value" } },
|
||||||
|
|
||||||
|
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
|
||||||
|
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
|
||||||
|
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
|
||||||
|
"default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"],
|
||||||
|
"effects": { "hardiness": "gsum('Gene.*Tolerance')" } },
|
||||||
|
|
||||||
|
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
||||||
|
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
||||||
|
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
||||||
|
"effects": { "variant": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Гены среды: свет, температура, почва. Покрывают экологические измерения генома растения,
|
||||||
|
// которые читают системы роста/жизненного цикла.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight",
|
||||||
|
"default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"],
|
||||||
|
"effects": { "optimalLight": "value" } },
|
||||||
|
{ "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance",
|
||||||
|
"default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"],
|
||||||
|
"effects": { "lightTolerance": "value" } },
|
||||||
|
{ "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature",
|
||||||
|
"default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "optimalTemperature": "value" } },
|
||||||
|
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
|
||||||
|
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "temperatureTolerance": "value" } },
|
||||||
|
// Жёсткие края диапазона роста (модель RimWorld): на сколько °C ниже/выше плато оптимума рост
|
||||||
|
// спадает до нуля. Ниже (optimal-tolerance-coldHardiness) или выше (optimal+tolerance+heatHardiness)
|
||||||
|
// растение дормантно и копит температурный стресс. Асимметрично: вид может терпеть жару лучше холода.
|
||||||
|
{ "defName": "GeneColdHardiness", "parent": "BaseNumericGene", "label": "gene.coldHardiness",
|
||||||
|
"default": 8, "min": 0, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "coldHardiness": "value" } },
|
||||||
|
{ "defName": "GeneHeatHardiness", "parent": "BaseNumericGene", "label": "gene.heatHardiness",
|
||||||
|
"default": 10, "min": 0, "max": 40, "tags": ["environment", "temperature"],
|
||||||
|
"effects": { "heatHardiness": "value" } },
|
||||||
|
{ "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
|
||||||
|
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
||||||
|
"effects": { "optimalFertility": "value" } },
|
||||||
|
{ "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance",
|
||||||
|
"default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
||||||
|
"effects": { "fertilityTolerance": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Гены роста и размножения: бодрость, продолжительность жизни, расселение, репродукция.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor",
|
||||||
|
"default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"],
|
||||||
|
"effects": { "vigor": "value" } },
|
||||||
|
{ "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan",
|
||||||
|
"default": 160, "min": 5, "max": 500, "tags": ["lifecycle"],
|
||||||
|
"effects": { "lifespan": "value" } },
|
||||||
|
{ "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange",
|
||||||
|
"default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"],
|
||||||
|
"effects": { "dispersalRange": "value" } },
|
||||||
|
{ "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval",
|
||||||
|
"default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"],
|
||||||
|
"effects": { "reproduceInterval": "value" } },
|
||||||
|
{ "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination",
|
||||||
|
"default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
||||||
|
"effects": { "selfPollination": "value" } },
|
||||||
|
{ "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate",
|
||||||
|
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
||||||
|
"effects": { "mutationRate": "value" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"type": "Gene",
|
||||||
|
// Общие (абстрактные) гены: база наследования для конкретных генов. Каждый ген задаёт, как
|
||||||
|
// генерируются/мутируют аллели и как ген вкладывается в признаки (effects: имя признака →
|
||||||
|
// формула; value — выраженное значение гена). Конкретные гены лежат рядом по подтемам.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
|
||||||
|
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"type": "Pawn",
|
"type": "Pawn",
|
||||||
|
// Животные.
|
||||||
"defs": [
|
"defs": [
|
||||||
{ "defName": "BaseBeing", "abstract": true, "kind": "being", "sizeCells": 1.1 },
|
|
||||||
{ "defName": "BeingMale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Male_south" },
|
|
||||||
{ "defName": "BeingFemale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Female_south" },
|
|
||||||
{ "defName": "BeingThin", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Thin_south" },
|
|
||||||
{ "defName": "BeingFat", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Fat_south" },
|
|
||||||
{ "defName": "BeingHulk", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Hulk_south" },
|
|
||||||
|
|
||||||
{ "defName": "BaseAnimal", "abstract": true, "kind": "animal" },
|
|
||||||
{ "defName": "Bear", "parent": "BaseAnimal", "label": "pawn.bear", "texture": "things/pawn/animal/bear/Bear_east", "sizeCells": 1.7 },
|
{ "defName": "Bear", "parent": "BaseAnimal", "label": "pawn.bear", "texture": "things/pawn/animal/bear/Bear_east", "sizeCells": 1.7 },
|
||||||
{ "defName": "DeerDoe", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerFemale_east", "sizeCells": 1.4 },
|
{ "defName": "DeerDoe", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerFemale_east", "sizeCells": 1.4 },
|
||||||
{ "defName": "DeerBuck", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerMale_east", "sizeCells": 1.6 },
|
{ "defName": "DeerBuck", "parent": "BaseAnimal", "label": "pawn.deer", "texture": "things/pawn/animal/deer/DeerMale_east", "sizeCells": 1.6 },
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "Pawn",
|
||||||
|
// Жители: варианты тел.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BeingMale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Male_south" },
|
||||||
|
{ "defName": "BeingFemale", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Female_south" },
|
||||||
|
{ "defName": "BeingThin", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Thin_south" },
|
||||||
|
{ "defName": "BeingFat", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Fat_south" },
|
||||||
|
{ "defName": "BeingHulk", "parent": "BaseBeing", "label": "pawn.being", "texture": "things/pawn/humanlike/bodies/FurCovered_Hulk_south" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "Pawn",
|
||||||
|
// Общие (абстрактные) существа: база для жителей и животных.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseBeing", "abstract": true, "kind": "being", "sizeCells": 1.1 },
|
||||||
|
{ "defName": "BaseAnimal", "abstract": true, "kind": "animal" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Ягодный почвопокров луга (еда).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "Strawberry", "parent": "BaseBerryBush", "label": "plant.strawberry", "sizeCells": 1.0, "texture": "things/plant/strawberryplant/StrawberryPlant" },
|
||||||
|
{ "defName": "Raspberry", "parent": "BaseBerryBush", "label": "plant.raspberry", "texture": "things/plant/raspberryplant/raspberrybusha" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Кусты: ягодный подлесок. Патч даёт им древесину при сборе (веточки) — см. Patches/Patches.json.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
|
||||||
|
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
|
||||||
|
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Пустыня: кактусы и агава на песке. Большинство без плодов; сагуаро/агава плодоносят.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "SaguaroCactusA", "parent": "BaseCactus", "label": "plant.saguaro", "sizeCells": 2.2, "trunkRadiusCells": 0.18,
|
||||||
|
"fruitProduct": "ProductBerry", "texture": "things/plant/saguarocactus/SaguaroCactusA" },
|
||||||
|
{ "defName": "SaguaroCactusB", "parent": "BaseCactus", "label": "plant.saguaro", "sizeCells": 2.2, "trunkRadiusCells": 0.18,
|
||||||
|
"fruitProduct": "ProductBerry", "texture": "things/plant/saguarocactus/SaguaroCactusB" },
|
||||||
|
{ "defName": "PebbleCactusA", "parent": "BaseCactus", "sizeCells": 1.0, "texture": "things/plant/pebblecactus/PebbleCactusA" },
|
||||||
|
{ "defName": "PebbleCactusB", "parent": "BaseCactus", "sizeCells": 1.0, "texture": "things/plant/pebblecactus/PebbleCactusB" },
|
||||||
|
{ "defName": "PincushionCactusA", "parent": "BaseCactus", "sizeCells": 0.9, "texture": "things/plant/pincushioncactus/PincushionCactusA" },
|
||||||
|
{ "defName": "AgaveA", "parent": "BaseCactus", "label": "plant.agave", "sizeCells": 1.3, "fruitProduct": "ProductBerry", "texture": "things/plant/agave/AgaveA" },
|
||||||
|
{ "defName": "AgaveB", "parent": "BaseCactus", "label": "plant.agave", "sizeCells": 1.3, "fruitProduct": "ProductBerry", "texture": "things/plant/agave/AgaveB" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Луг: декоративные цветы с быстрым расселением.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "DandelionA", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/Dandelion" },
|
||||||
|
{ "defName": "DandelionB", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/DandelionB" },
|
||||||
|
{ "defName": "DandelionC", "parent": "BaseFlower", "label": "plant.dandelion", "texture": "things/plant/dandelion/DandelionC" },
|
||||||
|
{ "defName": "DaylilyA", "parent": "BaseFlower", "label": "plant.daylily", "texture": "things/plant/daylily/DaylilyA" },
|
||||||
|
{ "defName": "DaylilyB", "parent": "BaseFlower", "label": "plant.daylily", "texture": "things/plant/daylily/DaylilyB" },
|
||||||
|
{ "defName": "RoseA", "parent": "BaseFlower", "label": "plant.rose", "sizeCells": 1.1, "texture": "things/plant/rose/RoseA" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Трава: быстрый почвопокров, основной скаттер лугов и леса.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.5,
|
||||||
|
"harvestProduct": "ProductGrass",
|
||||||
|
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 19, "temperatureTolerance": 8,
|
||||||
|
"coldHardiness": 9, "heatHardiness": 6,
|
||||||
|
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
|
||||||
|
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
|
||||||
|
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
||||||
|
"harvestAmount": 2, "leafHue": 0.36 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "sizeCells": 1.5, "label": "plant.stage.mature" }
|
||||||
|
] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Грибы: подлесок леса, растут в тени крон.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "GlowstoolA", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/glowstool/GlowstoolA" },
|
||||||
|
{ "defName": "GlowstoolB", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/glowstool/GlowstoolB" },
|
||||||
|
{ "defName": "TimbershroomA", "parent": "BaseMushroom", "sizeCells": 1.1, "texture": "things/plant/timbershroom/TimbershroomA" },
|
||||||
|
{ "defName": "TimbershroomB", "parent": "BaseMushroom", "sizeCells": 1.1, "texture": "things/plant/timbershroom/TimbershroomB" },
|
||||||
|
{ "defName": "NutrifungusA", "parent": "BaseMushroom", "sizeCells": 0.9, "texture": "things/plant/nutrifungus/NutrifungusA" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Деревья: умеренные (BaseTree), тёплые (BaseWarmTree) и холодные (BaseColdTree) ниши.
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
|
||||||
|
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
|
||||||
|
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
|
||||||
|
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
|
||||||
|
{ "defName": "TreeMapleA", "parent": "BaseTree", "label": "plant.maple", "texture": "things/plant/treemaple/TreeMapleA" },
|
||||||
|
{ "defName": "TreeMapleB", "parent": "BaseTree", "label": "plant.maple", "texture": "things/plant/treemaple/TreeMapleB" },
|
||||||
|
{ "defName": "TreePoplarA", "parent": "BaseTree", "label": "plant.poplar", "texture": "things/plant/treepoplar/TreePoplarA" },
|
||||||
|
{ "defName": "TreeTeakA", "parent": "BaseWarmTree", "label": "plant.teak", "texture": "things/plant/treeteak/TreeTeakA" },
|
||||||
|
{ "defName": "TreeTeakB", "parent": "BaseWarmTree", "label": "plant.teak", "texture": "things/plant/treeteak/TreeTeakB" },
|
||||||
|
{ "defName": "TreeCypressA", "parent": "BaseColdTree", "label": "plant.cypress", "texture": "things/plant/treecypress/TreeCypressA" },
|
||||||
|
{ "defName": "TreeCypressB", "parent": "BaseColdTree", "label": "plant.cypress", "texture": "things/plant/treecypress/TreeCypressB" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"type": "Plant",
|
||||||
|
// Общие (абстрактные) растения: базовые геномы по типам ниш. Конкретные виды наследуют их
|
||||||
|
// через "parent" и лежат рядом в файлах по семействам (Trees, Bushes, Cacti, …).
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 10,
|
||||||
|
"coldHardiness": 14, "heatHardiness": 12,
|
||||||
|
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
|
||||||
|
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Деревья тёплой и холодной температурных ниш (showcase температурных генов).
|
||||||
|
{ "defName": "BaseWarmTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.65, "lightTolerance": 0.5, "optimalTemperature": 23, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 8, "heatHardiness": 16,
|
||||||
|
"optimalFertility": 1.5, "fertilityTolerance": 0.9, "vigor": 1.1,
|
||||||
|
"lifespan": 150, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.22 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
{ "defName": "BaseColdTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.26,
|
||||||
|
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
||||||
|
"genome": { "optimalLight": 0.55, "lightTolerance": 0.5, "optimalTemperature": 8, "temperatureTolerance": 12,
|
||||||
|
"coldHardiness": 18, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.2, "fertilityTolerance": 1.0, "vigor": 0.9,
|
||||||
|
"lifespan": 220, "dispersalRange": 2, "reproduceInterval": 16, "selfPollination": 0.2,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.12, "spread": 0.08,
|
||||||
|
"fruitYield": 4, "fruitSeason": 2, "harvestAmount": 12, "leafHue": 0.45 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
||||||
|
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
||||||
|
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
|
||||||
|
"fruitProduct": "ProductBerry",
|
||||||
|
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 10, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
|
||||||
|
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
||||||
|
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
|
||||||
|
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Кактусы: жаролюбивы, засухоустойчивы (низкий оптимум почвы), морозо-нестойки → гибнут в морозы.
|
||||||
|
{ "defName": "BaseCactus", "abstract": true, "label": "plant.cactus", "sizeCells": 1.2, "trunkRadiusCells": 0,
|
||||||
|
"harvestProduct": "ProductFiber",
|
||||||
|
"genome": { "optimalLight": 0.95, "lightTolerance": 0.35, "optimalTemperature": 26, "temperatureTolerance": 8,
|
||||||
|
"coldHardiness": 20, "heatHardiness": 18,
|
||||||
|
"optimalFertility": 0.4, "fertilityTolerance": 0.5, "vigor": 0.5,
|
||||||
|
"lifespan": 300, "dispersalRange": 2, "reproduceInterval": 25, "selfPollination": 0.6,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
||||||
|
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 5, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Грибы: подлесок леса. Низкий оптимум света (0.15) → растут в тени крон, где другим темно
|
||||||
|
// (showcase световой пригодности). Богатая почва, короткая жизнь, быстрое спороношение.
|
||||||
|
{ "defName": "BaseMushroom", "abstract": true, "label": "plant.mushroom", "sizeCells": 0.9, "trunkRadiusCells": 0,
|
||||||
|
"harvestProduct": "ProductMushroom",
|
||||||
|
"genome": { "optimalLight": 0.15, "lightTolerance": 0.22, "optimalTemperature": 14, "temperatureTolerance": 11,
|
||||||
|
"coldHardiness": 12, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.6, "fertilityTolerance": 1.1, "vigor": 1.2,
|
||||||
|
"lifespan": 25, "dispersalRange": 3, "reproduceInterval": 4, "selfPollination": 0.9,
|
||||||
|
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
||||||
|
"fruitYield": 0, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.5 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
// Луг: цветы (декоративные, быстрое расселение) и ягодный почвопокров (еда).
|
||||||
|
{ "defName": "BaseFlower", "abstract": true, "label": "plant.flower", "sizeCells": 1.0, "trunkRadiusCells": 0,
|
||||||
|
"genome": { "optimalLight": 0.9, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 8, "heatHardiness": 8,
|
||||||
|
"optimalFertility": 1.0, "fertilityTolerance": 1.1, "vigor": 1.4,
|
||||||
|
"lifespan": 18, "dispersalRange": 4, "reproduceInterval": 3, "selfPollination": 0.85,
|
||||||
|
"mutationRate": 0.08, "variantChance": 0.3, "spread": 0.1,
|
||||||
|
"fruitYield": 0, "fruitSeason": 0, "harvestAmount": 0, "leafHue": 0.2 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.4, "growDays": 2, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ "defName": "BaseBerryBush", "abstract": true, "label": "plant.berrybush", "sizeCells": 1.2, "trunkRadiusCells": 0,
|
||||||
|
"fruitProduct": "ProductBerry",
|
||||||
|
"genome": { "optimalLight": 0.8, "lightTolerance": 0.4, "optimalTemperature": 18, "temperatureTolerance": 9,
|
||||||
|
"coldHardiness": 10, "heatHardiness": 7,
|
||||||
|
"optimalFertility": 1.2, "fertilityTolerance": 1.0, "vigor": 1.1,
|
||||||
|
"lifespan": 40, "dispersalRange": 2, "reproduceInterval": 6, "selfPollination": 0.6,
|
||||||
|
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
||||||
|
"fruitYield": 4, "fruitSeason": 2, "harvestAmount": 0, "leafHue": 0.33 },
|
||||||
|
"stages": [
|
||||||
|
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 3, "label": "plant.stage.sprout" },
|
||||||
|
{ "label": "plant.stage.mature" }
|
||||||
|
] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@
|
|||||||
"defs": [
|
"defs": [
|
||||||
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
|
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
|
||||||
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
|
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
|
||||||
|
{ "defName": "ProductFiber", "label": "product.fiber", "kind": "material" },
|
||||||
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
||||||
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }
|
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" },
|
||||||
|
{ "defName": "ProductMushroom", "label": "product.mushroom", "kind": "food" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"type": "Terrain",
|
||||||
|
"defs": [
|
||||||
|
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
|
||||||
|
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
|
||||||
|
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.46, "color": [237, 201, 120],
|
||||||
|
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4,
|
||||||
|
"scatter": [
|
||||||
|
{ "chance": 0.05, "options": ["SaguaroCactusA", "SaguaroCactusB"] },
|
||||||
|
{ "chance": 0.10, "options": ["PebbleCactusA", "PebbleCactusB", "PincushionCactusA"] },
|
||||||
|
{ "chance": 0.04, "options": ["AgaveA", "AgaveB"] }
|
||||||
|
] },
|
||||||
|
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.72, "color": [90, 160, 70],
|
||||||
|
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
|
||||||
|
"scatter": [
|
||||||
|
{ "chance": 1.0, "options": ["GrassA"] },
|
||||||
|
{ "chance": 0.5, "options": ["GrassA"] },
|
||||||
|
{ "chance": 0.22, "options": ["BushA", "BushB", "BushC"] },
|
||||||
|
{ "chance": 0.20, "options": ["Strawberry", "Raspberry"] },
|
||||||
|
{ "chance": 0.14, "options": ["DandelionA", "DandelionB", "DandelionC", "DaylilyA", "DaylilyB", "RoseA"] }
|
||||||
|
] },
|
||||||
|
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
||||||
|
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
||||||
|
"scatter": [
|
||||||
|
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA",
|
||||||
|
"TreeMapleA", "TreeMapleB", "TreePoplarA", "TreeCypressA", "TreeCypressB", "TreeTeakA", "TreeTeakB"] },
|
||||||
|
{ "chance": 0.6, "options": ["GrassA"] },
|
||||||
|
{ "chance": 0.16, "options": ["BushA", "BushB", "BushC"] },
|
||||||
|
{ "chance": 0.14, "options": ["Strawberry", "Raspberry"] },
|
||||||
|
{ "chance": 0.16, "options": ["GlowstoolA", "GlowstoolB", "TimbershroomA", "TimbershroomB", "NutrifungusA"] }
|
||||||
|
] },
|
||||||
|
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
|
||||||
|
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Gene",
|
|
||||||
// Организм-агностичные гены: каждый ген задаёт, как генерируются/мутируют аллели и как ген
|
|
||||||
// вкладывается в признаки (effects: имя признака → формула; value — выраженное значение гена).
|
|
||||||
// Этот набор покрывает измерения генома растения (его используют системы роста/жизненного цикла).
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "BaseNumericGene", "abstract": true, "kind": "Numeric",
|
|
||||||
"spread": 0.08, "mutationChance": 0.05, "mutationMagnitude": 0.12 },
|
|
||||||
|
|
||||||
{ "defName": "GeneOptimalLight", "parent": "BaseNumericGene", "label": "gene.optimalLight",
|
|
||||||
"default": 0.6, "min": 0.0, "max": 1.0, "tags": ["environment", "light"],
|
|
||||||
"effects": { "optimalLight": "value" } },
|
|
||||||
{ "defName": "GeneLightTolerance", "parent": "BaseNumericGene", "label": "gene.lightTolerance",
|
|
||||||
"default": 0.5, "min": 0.05, "max": 1.0, "tags": ["environment", "light"],
|
|
||||||
"effects": { "lightTolerance": "value" } },
|
|
||||||
{ "defName": "GeneOptimalTemperature", "parent": "BaseNumericGene", "label": "gene.optimalTemperature",
|
|
||||||
"default": 14, "min": -20, "max": 45, "tags": ["environment", "temperature"],
|
|
||||||
"effects": { "optimalTemperature": "value" } },
|
|
||||||
{ "defName": "GeneTemperatureTolerance", "parent": "BaseNumericGene", "label": "gene.temperatureTolerance",
|
|
||||||
"default": 16, "min": 2, "max": 40, "tags": ["environment", "temperature"],
|
|
||||||
"effects": { "temperatureTolerance": "value" } },
|
|
||||||
{ "defName": "GeneOptimalFertility", "parent": "BaseNumericGene", "label": "gene.optimalFertility",
|
|
||||||
"default": 1.4, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
|
||||||
"effects": { "optimalFertility": "value" } },
|
|
||||||
{ "defName": "GeneFertilityTolerance", "parent": "BaseNumericGene", "label": "gene.fertilityTolerance",
|
|
||||||
"default": 0.9, "min": 0.1, "max": 3.0, "tags": ["environment", "soil"],
|
|
||||||
"effects": { "fertilityTolerance": "value" } },
|
|
||||||
|
|
||||||
{ "defName": "GeneVigor", "parent": "BaseNumericGene", "label": "gene.vigor",
|
|
||||||
"default": 1.0, "min": 0.1, "max": 3.0, "tags": ["growth"],
|
|
||||||
"effects": { "vigor": "value" } },
|
|
||||||
{ "defName": "GeneLifespan", "parent": "BaseNumericGene", "label": "gene.lifespan",
|
|
||||||
"default": 160, "min": 5, "max": 500, "tags": ["lifecycle"],
|
|
||||||
"effects": { "lifespan": "value" } },
|
|
||||||
{ "defName": "GeneDispersalRange", "parent": "BaseNumericGene", "label": "gene.dispersalRange",
|
|
||||||
"default": 2, "min": 1, "max": 8, "tags": ["lifecycle", "reproduction"],
|
|
||||||
"effects": { "dispersalRange": "value" } },
|
|
||||||
{ "defName": "GeneReproduceInterval", "parent": "BaseNumericGene", "label": "gene.reproduceInterval",
|
|
||||||
"default": 14, "min": 1, "max": 60, "tags": ["lifecycle", "reproduction"],
|
|
||||||
"effects": { "reproduceInterval": "value" } },
|
|
||||||
{ "defName": "GeneSelfPollination", "parent": "BaseNumericGene", "label": "gene.selfPollination",
|
|
||||||
"default": 0.2, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
|
||||||
"effects": { "selfPollination": "value" } },
|
|
||||||
{ "defName": "GeneMutationRate", "parent": "BaseNumericGene", "label": "gene.mutationRate",
|
|
||||||
"default": 0.05, "min": 0.0, "max": 1.0, "tags": ["reproduction"],
|
|
||||||
"effects": { "mutationRate": "value" } },
|
|
||||||
|
|
||||||
// --- Контент (фаза G4): плодоношение, добыча, цвет ---
|
|
||||||
{ "defName": "GeneFruitYield", "parent": "BaseNumericGene", "label": "gene.fruitYield",
|
|
||||||
"default": 0, "min": 0, "max": 12, "tags": ["fruiting"],
|
|
||||||
"effects": { "fruitYield": "value" } },
|
|
||||||
{ "defName": "GeneFruitSeason", "parent": "BaseNumericGene", "label": "gene.fruitSeason",
|
|
||||||
"default": 1, "min": 0, "max": 3, "spread": 0, "mutationChance": 0, "tags": ["fruiting"],
|
|
||||||
"effects": { "fruitSeason": "value" } },
|
|
||||||
{ "defName": "GeneHarvestAmount", "parent": "BaseNumericGene", "label": "gene.harvestAmount",
|
|
||||||
"default": 1, "min": 0, "max": 50, "tags": ["harvest"],
|
|
||||||
"effects": { "harvestAmount": "value" } },
|
|
||||||
{ "defName": "GeneLeafHue", "parent": "BaseNumericGene", "label": "gene.leafHue",
|
|
||||||
"default": 0.33, "min": 0, "max": 1, "spread": 0.04, "tags": ["morphology", "color"],
|
|
||||||
"effects": { "leafHue": "value" } },
|
|
||||||
|
|
||||||
// Производный ген: признак собирается группировкой по регэкспу — сумма всех генов-толерантностей
|
|
||||||
// (демонстрация gsom-функций фазы G5). Собственное значение гена не используется.
|
|
||||||
{ "defName": "GeneHardiness", "parent": "BaseNumericGene", "label": "gene.hardiness",
|
|
||||||
"default": 0, "min": 0, "max": 1, "spread": 0, "mutationChance": 0, "tags": ["derived"],
|
|
||||||
"effects": { "hardiness": "gsum('Gene.*Tolerance')" } },
|
|
||||||
|
|
||||||
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
|
||||||
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
|
||||||
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
|
||||||
"effects": { "variant": "value" } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Plant",
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
|
|
||||||
"harvestProduct": "ProductWood", "fruitProduct": "ProductAcorn",
|
|
||||||
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 14, "temperatureTolerance": 16,
|
|
||||||
"optimalFertility": 1.4, "fertilityTolerance": 0.9, "vigor": 1.0,
|
|
||||||
"lifespan": 160, "dispersalRange": 2, "reproduceInterval": 14, "selfPollination": 0.2,
|
|
||||||
"mutationRate": 0.05, "variantChance": 0.15, "spread": 0.08,
|
|
||||||
"fruitYield": 5, "fruitSeason": 2, "harvestAmount": 10, "leafHue": 0.30 },
|
|
||||||
"stages": [
|
|
||||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
|
|
||||||
{ "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
|
|
||||||
{ "sizeCells": 2.0, "label": "plant.stage.mature" }
|
|
||||||
] },
|
|
||||||
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
|
|
||||||
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
|
|
||||||
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
|
|
||||||
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
|
|
||||||
|
|
||||||
{ "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
|
|
||||||
"harvestProduct": "ProductGrass",
|
|
||||||
"genome": { "optimalLight": 0.85, "lightTolerance": 0.35, "optimalTemperature": 20, "temperatureTolerance": 12,
|
|
||||||
"optimalFertility": 1.0, "fertilityTolerance": 1.0, "vigor": 1.3,
|
|
||||||
"lifespan": 22, "dispersalRange": 3, "reproduceInterval": 3, "selfPollination": 0.8,
|
|
||||||
"mutationRate": 0.06, "variantChance": 0.2, "spread": 0.1,
|
|
||||||
"harvestAmount": 2, "leafHue": 0.36 },
|
|
||||||
"stages": [
|
|
||||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
|
|
||||||
{ "sizeCells": 1.2, "label": "plant.stage.mature" }
|
|
||||||
] },
|
|
||||||
|
|
||||||
{ "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
|
|
||||||
"fruitProduct": "ProductBerry",
|
|
||||||
"genome": { "optimalLight": 0.6, "lightTolerance": 0.5, "optimalTemperature": 17, "temperatureTolerance": 13,
|
|
||||||
"optimalFertility": 1.1, "fertilityTolerance": 1.0, "vigor": 1.0,
|
|
||||||
"lifespan": 60, "dispersalRange": 2, "reproduceInterval": 7, "selfPollination": 0.5,
|
|
||||||
"mutationRate": 0.05, "variantChance": 0.18, "spread": 0.1,
|
|
||||||
"fruitYield": 3, "fruitSeason": 1, "harvestAmount": 3, "leafHue": 0.32 },
|
|
||||||
"stages": [
|
|
||||||
{ "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
|
|
||||||
{ "sizeCells": 1.4, "label": "plant.stage.mature" }
|
|
||||||
] },
|
|
||||||
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
|
|
||||||
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
|
|
||||||
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "Terrain",
|
|
||||||
"defs": [
|
|
||||||
{ "defName": "DeepWater", "label": "terrain.deepwater", "maxHeight": 0.30, "color": [23, 63, 95] },
|
|
||||||
{ "defName": "Water", "label": "terrain.water", "maxHeight": 0.42, "color": [32, 99, 155] },
|
|
||||||
{ "defName": "Sand", "label": "terrain.sand", "maxHeight": 0.48, "color": [237, 201, 120],
|
|
||||||
"isLand": true, "surface": "terrain/surfaces/sand", "fertility": 0.4 },
|
|
||||||
{ "defName": "Grass", "label": "terrain.grass", "maxHeight": 0.68, "color": [90, 160, 70],
|
|
||||||
"isLand": true, "surface": "terrain/surfaces/mossy", "fertility": 1.0,
|
|
||||||
"scatter": [
|
|
||||||
{ "chance": 0.35, "options": ["GrassA"] },
|
|
||||||
{ "chance": 0.07, "options": ["BushA", "BushB", "BushC"] }
|
|
||||||
] },
|
|
||||||
{ "defName": "Forest", "label": "terrain.forest", "maxHeight": 0.85, "color": [44, 110, 50],
|
|
||||||
"isLand": true, "surface": "terrain/surfaces/soil", "fertility": 1.6,
|
|
||||||
"scatter": [
|
|
||||||
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
|
|
||||||
] },
|
|
||||||
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
|
|
||||||
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,32 @@
|
|||||||
"season.summer": "summer",
|
"season.summer": "summer",
|
||||||
"season.autumn": "autumn",
|
"season.autumn": "autumn",
|
||||||
"season.winter": "winter",
|
"season.winter": "winter",
|
||||||
"hud.controls": "WASD — camera, wheel — zoom, ` — console, F1 — inspector",
|
"hud.controls": "WASD — camera, wheel — zoom, LMB — select, ` — console, F1 — inspector",
|
||||||
|
"inspect.tab.overview": "Overview",
|
||||||
|
"inspect.tab.genes": "Genes",
|
||||||
|
"inspect.tab.products": "Products",
|
||||||
|
"inspect.stage": "Stage: {0} ({1}/{2})",
|
||||||
|
"inspect.growing": "Growth: {0:0}% to next stage",
|
||||||
|
"inspect.mature": "Fully grown",
|
||||||
|
"inspect.age": "Age: {0:0.0} d · lives to {1:0}",
|
||||||
|
"inspect.state": "State: {0}",
|
||||||
|
"inspect.state.growing": "growing",
|
||||||
|
"inspect.state.mature": "mature",
|
||||||
|
"inspect.state.dormantcold": "dormant — too cold",
|
||||||
|
"inspect.state.dormanthot": "dormant — too hot",
|
||||||
|
"inspect.stress": "Stress: {0:0.0} d to death",
|
||||||
|
"inspect.temp": "Temperature: grows {0:0}…{1:0}°C, optimal {2:0}…{3:0}°C",
|
||||||
|
"inspect.genes": "— Genes —",
|
||||||
|
"inspect.gene.vigor": "Vigor {0:0.00} · lifespan {1:0} d",
|
||||||
|
"inspect.gene.env": "Opt. light {0:0.00} · opt. soil {1:0.00}",
|
||||||
|
"inspect.gene.repro": "Dispersal {0:0.0} · interval {1:0.0} d",
|
||||||
|
"inspect.gene.repro2": "Self-pollin. {0:0.00} · mutation {1:0.00}",
|
||||||
|
"inspect.gene.hardy": "Cold/heat {0:0}/{1:0} · leaf hue {2:0.00}",
|
||||||
|
"inspect.gene.variant": "Morph: recessive variant",
|
||||||
|
"inspect.harvest": "Harvest: {0} ×{1:0.#}",
|
||||||
|
"inspect.fruit": "Fruit: {0} ×{1:0.#} ({2}) · ripe {3:0.#}",
|
||||||
|
"inspect.barren": "Bears no fruit",
|
||||||
|
"inspect.hint": "LMB — select · RMB/Esc — clear",
|
||||||
"hud.paused": "PAUSED",
|
"hud.paused": "PAUSED",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
"menu.subtitle": "a god-game: minimal graphics, deep simulation",
|
"menu.subtitle": "a god-game: minimal graphics, deep simulation",
|
||||||
@@ -35,6 +60,7 @@
|
|||||||
"settings.vsync": "Vertical sync",
|
"settings.vsync": "Vertical sync",
|
||||||
"settings.resolution": "Resolution",
|
"settings.resolution": "Resolution",
|
||||||
"settings.volume": "Volume",
|
"settings.volume": "Volume",
|
||||||
|
"settings.uiscale": "UI scale",
|
||||||
"settings.apply": "Apply",
|
"settings.apply": "Apply",
|
||||||
"settings.back": "Back",
|
"settings.back": "Back",
|
||||||
"pause.title": "Paused",
|
"pause.title": "Paused",
|
||||||
@@ -63,14 +89,31 @@
|
|||||||
"plant.pine": "pine",
|
"plant.pine": "pine",
|
||||||
"plant.grass": "grass tuft",
|
"plant.grass": "grass tuft",
|
||||||
"plant.bush": "bush",
|
"plant.bush": "bush",
|
||||||
|
"plant.maple": "maple",
|
||||||
|
"plant.poplar": "poplar",
|
||||||
|
"plant.teak": "teak",
|
||||||
|
"plant.cypress": "cypress",
|
||||||
|
"plant.cactus": "cactus",
|
||||||
|
"plant.saguaro": "saguaro",
|
||||||
|
"plant.agave": "agave",
|
||||||
|
"plant.mushroom": "mushroom",
|
||||||
|
"plant.flower": "flower",
|
||||||
|
"plant.dandelion": "dandelion",
|
||||||
|
"plant.daylily": "daylily",
|
||||||
|
"plant.rose": "rose",
|
||||||
|
"plant.berrybush": "berry bush",
|
||||||
|
"plant.strawberry": "strawberry",
|
||||||
|
"plant.raspberry": "raspberry",
|
||||||
"plant.stage.seedling": "seedling",
|
"plant.stage.seedling": "seedling",
|
||||||
"plant.stage.sprout": "sprout",
|
"plant.stage.sprout": "sprout",
|
||||||
"plant.stage.sapling": "sapling",
|
"plant.stage.sapling": "sapling",
|
||||||
"plant.stage.mature": "mature",
|
"plant.stage.mature": "mature",
|
||||||
"product.wood": "wood",
|
"product.wood": "wood",
|
||||||
"product.grass": "grass",
|
"product.grass": "grass",
|
||||||
|
"product.fiber": "fiber",
|
||||||
"product.berry": "berries",
|
"product.berry": "berries",
|
||||||
"product.acorn": "acorn",
|
"product.acorn": "acorn",
|
||||||
|
"product.mushroom": "mushrooms",
|
||||||
"pawn.being": "being",
|
"pawn.being": "being",
|
||||||
"pawn.bear": "bear",
|
"pawn.bear": "bear",
|
||||||
"pawn.deer": "deer",
|
"pawn.deer": "deer",
|
||||||
@@ -84,5 +127,6 @@
|
|||||||
"net.connecting": "connecting…",
|
"net.connecting": "connecting…",
|
||||||
"net.connected": "connected",
|
"net.connected": "connected",
|
||||||
"net.failed": "connection failed",
|
"net.failed": "connection failed",
|
||||||
"net.lost": "connection lost"
|
"net.lost": "connection lost",
|
||||||
|
"net.reconnecting": "reconnecting…"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,32 @@
|
|||||||
"season.summer": "лето",
|
"season.summer": "лето",
|
||||||
"season.autumn": "осень",
|
"season.autumn": "осень",
|
||||||
"season.winter": "зима",
|
"season.winter": "зима",
|
||||||
"hud.controls": "WASD — камера, колесо — зум, ` — консоль, F1 — инспектор",
|
"hud.controls": "WASD — камера, колесо — зум, ЛКМ — выбрать, ` — консоль, F1 — инспектор",
|
||||||
|
"inspect.tab.overview": "Обзор",
|
||||||
|
"inspect.tab.genes": "Гены",
|
||||||
|
"inspect.tab.products": "Продукты",
|
||||||
|
"inspect.stage": "Стадия: {0} ({1}/{2})",
|
||||||
|
"inspect.growing": "Рост: {0:0}% до следующей стадии",
|
||||||
|
"inspect.mature": "Полностью выросло",
|
||||||
|
"inspect.age": "Возраст: {0:0.0} дн · живёт до {1:0}",
|
||||||
|
"inspect.state": "Состояние: {0}",
|
||||||
|
"inspect.state.growing": "растёт",
|
||||||
|
"inspect.state.mature": "созрело",
|
||||||
|
"inspect.state.dormantcold": "покой — слишком холодно",
|
||||||
|
"inspect.state.dormanthot": "покой — слишком жарко",
|
||||||
|
"inspect.stress": "Стресс: {0:0.0} дн до гибели",
|
||||||
|
"inspect.temp": "Температура: рост {0:0}…{1:0}°C, оптимум {2:0}…{3:0}°C",
|
||||||
|
"inspect.genes": "— Гены —",
|
||||||
|
"inspect.gene.vigor": "Бодрость {0:0.00} · жизнь {1:0} дн",
|
||||||
|
"inspect.gene.env": "Опт. свет {0:0.00} · опт. почва {1:0.00}",
|
||||||
|
"inspect.gene.repro": "Расселение {0:0.0} кл · период {1:0.0} дн",
|
||||||
|
"inspect.gene.repro2": "Самоопыление {0:0.00} · мутации {1:0.00}",
|
||||||
|
"inspect.gene.hardy": "Морозо/жаро {0:0}/{1:0} · оттенок {2:0.00}",
|
||||||
|
"inspect.gene.variant": "Морфа: рецессивный вариант",
|
||||||
|
"inspect.harvest": "Сбор: {0} ×{1:0.#}",
|
||||||
|
"inspect.fruit": "Плоды: {0} ×{1:0.#} ({2}) · зрелых {3:0.#}",
|
||||||
|
"inspect.barren": "Не плодоносит",
|
||||||
|
"inspect.hint": "ЛКМ — выбрать · ПКМ/Esc — снять",
|
||||||
"hud.paused": "ПАУЗА",
|
"hud.paused": "ПАУЗА",
|
||||||
"menu.title": "LittleSim",
|
"menu.title": "LittleSim",
|
||||||
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
|
"menu.subtitle": "бог-игра: минимум графики, максимум симуляции",
|
||||||
@@ -35,6 +60,7 @@
|
|||||||
"settings.vsync": "Вертикальная синхронизация",
|
"settings.vsync": "Вертикальная синхронизация",
|
||||||
"settings.resolution": "Разрешение",
|
"settings.resolution": "Разрешение",
|
||||||
"settings.volume": "Громкость",
|
"settings.volume": "Громкость",
|
||||||
|
"settings.uiscale": "Масштаб UI",
|
||||||
"settings.apply": "Применить",
|
"settings.apply": "Применить",
|
||||||
"settings.back": "Назад",
|
"settings.back": "Назад",
|
||||||
"pause.title": "Пауза",
|
"pause.title": "Пауза",
|
||||||
@@ -63,14 +89,31 @@
|
|||||||
"plant.pine": "сосна",
|
"plant.pine": "сосна",
|
||||||
"plant.grass": "пучок травы",
|
"plant.grass": "пучок травы",
|
||||||
"plant.bush": "куст",
|
"plant.bush": "куст",
|
||||||
|
"plant.maple": "клён",
|
||||||
|
"plant.poplar": "тополь",
|
||||||
|
"plant.teak": "тик",
|
||||||
|
"plant.cypress": "кипарис",
|
||||||
|
"plant.cactus": "кактус",
|
||||||
|
"plant.saguaro": "сагуаро",
|
||||||
|
"plant.agave": "агава",
|
||||||
|
"plant.mushroom": "гриб",
|
||||||
|
"plant.flower": "цветок",
|
||||||
|
"plant.dandelion": "одуванчик",
|
||||||
|
"plant.daylily": "лилейник",
|
||||||
|
"plant.rose": "роза",
|
||||||
|
"plant.berrybush": "ягодный куст",
|
||||||
|
"plant.strawberry": "земляника",
|
||||||
|
"plant.raspberry": "малина",
|
||||||
"plant.stage.seedling": "росток",
|
"plant.stage.seedling": "росток",
|
||||||
"plant.stage.sprout": "всходы",
|
"plant.stage.sprout": "всходы",
|
||||||
"plant.stage.sapling": "саженец",
|
"plant.stage.sapling": "саженец",
|
||||||
"plant.stage.mature": "взрослое",
|
"plant.stage.mature": "взрослое",
|
||||||
"product.wood": "древесина",
|
"product.wood": "древесина",
|
||||||
"product.grass": "трава",
|
"product.grass": "трава",
|
||||||
|
"product.fiber": "волокно",
|
||||||
"product.berry": "ягоды",
|
"product.berry": "ягоды",
|
||||||
"product.acorn": "жёлудь",
|
"product.acorn": "жёлудь",
|
||||||
|
"product.mushroom": "грибы",
|
||||||
"pawn.being": "житель",
|
"pawn.being": "житель",
|
||||||
"pawn.bear": "медведь",
|
"pawn.bear": "медведь",
|
||||||
"pawn.deer": "олень",
|
"pawn.deer": "олень",
|
||||||
@@ -84,5 +127,6 @@
|
|||||||
"net.connecting": "подключение…",
|
"net.connecting": "подключение…",
|
||||||
"net.connected": "подключено",
|
"net.connected": "подключено",
|
||||||
"net.failed": "не удалось подключиться",
|
"net.failed": "не удалось подключиться",
|
||||||
"net.lost": "соединение потеряно"
|
"net.lost": "соединение потеряно",
|
||||||
|
"net.reconnecting": "переподключение…"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ Mods/<Имя>/
|
|||||||
вид — `being`/`animal`). CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип
|
вид — `being`/`animal`). CLR-классы — `src/LittleSim/Content/GameDefs.cs`; новый тип
|
||||||
дефа = новый класс + `RegisterType` в `GameContent.Load`.
|
дефа = новый класс + `RegisterType` в `GameContent.Load`.
|
||||||
|
|
||||||
|
`Defs/` сканируется рекурсивно (`Defs/**/*.json`), и имя файла ни на что не влияет —
|
||||||
|
важны только `type` и `defName`. Поэтому контент Core разложен по подпапкам для удобства
|
||||||
|
поиска: одна папка на тип дефа, внутри — общие (абстрактные) дефы в `_Bases.json` и
|
||||||
|
конкретные, разбитые по семействам:
|
||||||
|
|
||||||
|
```
|
||||||
|
Defs/
|
||||||
|
Genes/ _Bases.json, Environment.json, Growth.json, Content.json
|
||||||
|
Plants/ _Bases.json, Trees.json, Bushes.json, Grass.json, Cacti.json,
|
||||||
|
Mushrooms.json, Flowers.json, Berries.json
|
||||||
|
Pawns/ _Bases.json, Beings.json, Animals.json
|
||||||
|
Terrain/ Terrain.json
|
||||||
|
Products/ Products.json
|
||||||
|
Patches/ Patches.json
|
||||||
|
WorldPresets/ WorldPresets.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`parent` разрешается после слияния всех файлов, так что ссылка на родителя из другого
|
||||||
|
файла/папки (`TreeOakA` → `BaseTree`) работает независимо от расположения.
|
||||||
|
|
||||||
## Локализация
|
## Локализация
|
||||||
|
|
||||||
`Languages/<код>/*.json` — плоские словари «ключ → строка»:
|
`Languages/<код>/*.json` — плоские словари «ключ → строка»:
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
# Roadmap LittleSim
|
|
||||||
|
|
||||||
Этап закрыт, когда механика работает, наблюдаема (HUD/консоль/визуал)
|
|
||||||
и управляется командой консоли.
|
|
||||||
|
|
||||||
## Этап 0 — Каркас ✅
|
|
||||||
- [x] Репозиторий с движком-сабмодулем, общий solution
|
|
||||||
- [x] Генерация рельефа по сиду (вода/песок/трава/лес/горы)
|
|
||||||
- [x] Жители-заглушки (блуждание), камера бога, HUD, консоль (`regen`)
|
|
||||||
|
|
||||||
## Этап 1 — Жители как агенты
|
|
||||||
- [~] Потребности: голод, усталость (тикают со временем) — усталость/энергия готова (`PawnNeeds`), голод впереди
|
|
||||||
- [~] Utility-выбор действия: искать еду / отдыхать / бродить — каркас на движке (`MrGameEng.AI`), выбор «отдыхать/бродить» работает; «искать еду» впереди
|
|
||||||
- [ ] Еда на траве/в лесу: растёт, собирается, истощается
|
|
||||||
- [ ] Смерть от голода; визуальное состояние жителя (цвет/прозрачность)
|
|
||||||
- [ ] Консоль: `spawn <n>`, `feed`, `starve` для тестов
|
|
||||||
|
|
||||||
## Этап 2 — Общество
|
|
||||||
- [ ] Дома и поселения (житель несёт еду домой, спит дома)
|
|
||||||
- [ ] Размножение при сытости; рост населения
|
|
||||||
- [ ] Простые профессии: собиратель, строитель
|
|
||||||
|
|
||||||
## Этап 3 — Бог
|
|
||||||
- [ ] Вера как ресурс (генерируется жителями)
|
|
||||||
- [ ] Первые силы: дождь (ускоряет рост еды), молния (поджигает лес)
|
|
||||||
- [ ] Терраформинг клеток
|
|
||||||
|
|
||||||
## Этап 4 — Экология и время
|
|
||||||
- [ ] День/ночь, сезоны
|
|
||||||
- [ ] Восстановление/деградация ресурсов, пожары
|
|
||||||
- [ ] Миграции при нехватке ресурсов
|
|
||||||
|
|
||||||
## Бэклог
|
|
||||||
- Сохранение/загрузка мира (сериализация Friflo)
|
|
||||||
- Фиксированный тик симуляции, независимый от рендера
|
|
||||||
- Исторические графики (население, еда) в дев-оверлее
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# Растения как экосистема
|
|
||||||
|
|
||||||
Дизайн системы растений: рост зависит от среды, среда описана факторами, реакция растения на
|
|
||||||
факторы задаётся **генами**, а размножение передаёт гены потомкам с мутацией — появляется
|
|
||||||
естественный отбор. Документ — единый ориентир для фаз A–D; детали каждой фазы уточняются её
|
|
||||||
отдельным планом.
|
|
||||||
|
|
||||||
## Модель роста
|
|
||||||
|
|
||||||
Скорость роста растения в данный момент:
|
|
||||||
|
|
||||||
```
|
|
||||||
скорость = база × пригодность(свет) × пригодность(температура) × пригодность(плодородность)
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждая «пригодность» — число в [0..1] из кривой отклика (движковый `ResponseCurve`, модуль
|
|
||||||
`MrGameEng.AI`) с **оптимумом** и **шириной толерантности**, которые берутся из генов растения.
|
|
||||||
Растение вне своих оптимумов растёт медленно или стоит; в своих — быстро. Так все факторы среды
|
|
||||||
сводятся к одному механизму, а различие видов/особей — целиком в генах.
|
|
||||||
|
|
||||||
Фактор «возраст/стадия» остаётся как сейчас (`PlantGrowthStage` в дефах): пригодности умножают
|
|
||||||
прирост `AgeDays`, стадии переключают спрайт/размер по накопленному возрасту.
|
|
||||||
|
|
||||||
## Факторы среды
|
|
||||||
|
|
||||||
- **Свет** — амбиент день/ночь (`MrGameEng.Lighting.DayNight`), сэмплируемый в точке
|
|
||||||
(`SampleAt(world)`); глобальный в фазе A, локальный (с тенями) в фазе D.
|
|
||||||
- **Температура** — непрерывная, из климата (`MrGameEng.Core.Climate`): сезонный синус по дню года
|
|
||||||
+ суточные колебания (холоднее ночью).
|
|
||||||
- **Плодородность** — пока константа типа террейна (`TerrainDef.Fertility`); позже возможна
|
|
||||||
по-клеточная почва с истощением/восстановлением.
|
|
||||||
|
|
||||||
## Гены (диплоидный геном, гибридная экспрессия)
|
|
||||||
|
|
||||||
Каждое растение несёт **геном**: по паре аллелей на ген. Экспрессия гибридная:
|
|
||||||
|
|
||||||
- **Числовые гены** (оптимум света, толерантность к свету, оптимум температуры, толерантность,
|
|
||||||
потребность в плодородности, макс. размер, базовая скорость, продолжительность жизни, дальность
|
|
||||||
расселения, плодовитость, темп мутаций, способность к самоопылению) — фенотип = **смешение**
|
|
||||||
(среднее двух аллелей). Реализм отбора: промежуточные значения, плавный дрейф.
|
|
||||||
- **Дискретные гены** (например, цвет/морфа, наличие признака) — **доминант/рецессив**: рецессивный
|
|
||||||
фенотип виден только в гомозиготе (aa).
|
|
||||||
|
|
||||||
Наследование (мейоз): каждый родитель отдаёт по одному случайному аллелю на ген; потомок получает
|
|
||||||
по одному от каждого. Затем — мутация (с вероятностью из гена темпа мутаций сдвигаем числовой
|
|
||||||
аллель/переключаем дискретный). Всё детерминировано сидом мира.
|
|
||||||
|
|
||||||
Стартовая популяция: геномы генерируются из сида (вид задаёт базовый геном и разброс аллелей в
|
|
||||||
дефе; особи отклоняются).
|
|
||||||
|
|
||||||
## Размножение (полуреалистичное)
|
|
||||||
|
|
||||||
- **Перекрёстное опыление (приоритет):** зрелое растение ищет зрелого соседа того же вида в радиусе
|
|
||||||
(ген дальности; запрос через spatial-hash движка). Есть партнёр → семя с геномом от двух
|
|
||||||
родителей (Мендель + мутация).
|
|
||||||
- **Самоопыление (запас):** нет партнёра и ген «способность к самоопылению» позволяет → семя из
|
|
||||||
одного родителя (само-скрещивание аллелей + мутация).
|
|
||||||
- **Расселение:** семя падает в клетку в радиусе расселения; прорастает, если клетка подходит
|
|
||||||
(суша, не занято сверх лимита, пригодность не нулевая).
|
|
||||||
- **Лимит плотности:** не больше N растений на клетку/площадь — иначе взрыв численности.
|
|
||||||
- **Старение/смерть:** возраст превышает ген жизни → смерть; или длительный стресс (пригодность
|
|
||||||
около нуля) → гибель. Смерть освобождает место — баланс с размножением и основа отбора.
|
|
||||||
|
|
||||||
## Производительность и сейв
|
|
||||||
|
|
||||||
- Представление — **по сущности на растение** (видно в рендере и ECS-инспекторе, индивидуальные
|
|
||||||
гены). Защита от взрыва: лимит плотности, смерть, разумные радиусы, проверки размножения по
|
|
||||||
таймеру/сезону (не каждый кадр на каждое растение), spatial-hash для соседей.
|
|
||||||
- **Персист:** геномы, позиции, возраст и стадия растений сохраняются в `WorldSave` (мир больше не
|
|
||||||
воспроизводится только из сида — эволюция переживает загрузку).
|
|
||||||
- **Детерминизм:** мутации/расселение/опыление — на сидированном `Random`, как в остальной симуляции.
|
|
||||||
|
|
||||||
## Фазы реализации
|
|
||||||
|
|
||||||
- **A — климат и день/ночь (движок).** `Climate` (температура/сезон поверх `Calendar`),
|
|
||||||
`DayNight` (амбиент, `Renderer2D.AmbientLight` на World-слоях, сэмплируемый `SampleAt`),
|
|
||||||
затемнение мира ночью, HUD: сезон/температура/время. Фундамент для влияния среды на рост.
|
|
||||||
- **B — гены и рост по пригодностям (игра).** Компонент `PlantGenome`, расчёт пригодностей
|
|
||||||
(свет из `DayNight.SampleAt`, температура из `Climate`, плодородность из террейна), замена
|
|
||||||
плоского `GrowthRate`. Геном виден/правится в инспекторе.
|
|
||||||
- **C — размножение и смерть (игра).** Перекрёстное/самоопыление, расселение, лимит плотности,
|
|
||||||
старение/стресс-смерть, персист популяции в сейв.
|
|
||||||
- **D — полноценное 2D-освещение (движок).** Точечные источники + тени (окклюзия от препятствий);
|
|
||||||
`DayNight.SampleAt` начинает возвращать локальную освещённость, и затенённые растения растут хуже.
|
|
||||||
+1
-1
Submodule engine updated: d044cafad9...96e19c7c61
+3
-1
@@ -9,7 +9,9 @@ $Project = Join-Path $Root "src\LittleSim\LittleSim.csproj"
|
|||||||
|
|
||||||
Push-Location $Root
|
Push-Location $Root
|
||||||
try {
|
try {
|
||||||
dotnet build LittleSim.sln -c $Configuration
|
# Только игровой проект (тянет за собой нужные движковые библиотеки) — не вся солюшн с
|
||||||
|
# тестами, сервером и WASM-клиентом (его emscripten-сборка занимает минуты).
|
||||||
|
dotnet build $Project -c $Configuration
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -18,5 +18,7 @@ done
|
|||||||
|
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
dotnet build LittleSim.sln -c "$CONFIGURATION"
|
# Только игровой проект (тянет за собой нужные движковые библиотеки) — не вся солюшн с
|
||||||
|
# тестами, сервером и WASM-клиентом (его emscripten-сборка занимает минуты).
|
||||||
|
dotnet build src/LittleSim/LittleSim.csproj -c "$CONFIGURATION"
|
||||||
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
|
dotnet run --project src/LittleSim/LittleSim.csproj -c "$CONFIGURATION" --no-build "$@"
|
||||||
|
|||||||
@@ -16,24 +16,33 @@ public class LittleSimWebGame : Game
|
|||||||
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
|
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
|
||||||
public EngineContext Context { get; } = new EngineContext();
|
public EngineContext Context { get; } = new EngineContext();
|
||||||
|
|
||||||
private readonly Uri _server;
|
|
||||||
private GraphicsDeviceManager _graphics;
|
private GraphicsDeviceManager _graphics;
|
||||||
private SpriteBatch _spriteBatch = null!;
|
private SpriteBatch _spriteBatch = null!;
|
||||||
private Texture2D _pixel = null!;
|
private Texture2D _pixel = null!;
|
||||||
|
private WorldViewScene _scene = null!;
|
||||||
|
|
||||||
/// <summary>Игра, подключающаяся к серверу <paramref name="server"/>.</summary>
|
/// <summary>Игра-наблюдатель: мира не создаёт, ждёт <see cref="Connect"/> из UI.</summary>
|
||||||
public LittleSimWebGame(Uri server)
|
public LittleSimWebGame()
|
||||||
{
|
{
|
||||||
_server = server;
|
|
||||||
_graphics = new GraphicsDeviceManager(this);
|
_graphics = new GraphicsDeviceManager(this);
|
||||||
Content.RootDirectory = "Content";
|
Content.RootDirectory = "Content";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Состояние соединения со сценой-наблюдателем — читает оверлей подключения.</summary>
|
||||||
|
public ConnectionStatus Status => _scene?.Status ?? ConnectionStatus.Idle;
|
||||||
|
|
||||||
|
/// <summary>Число реплицированных жителей — для статуса в оверлее.</summary>
|
||||||
|
public int EntityCount => _scene?.EntityCount ?? 0;
|
||||||
|
|
||||||
|
/// <summary>Подключиться к серверу <paramref name="server"/> (вызывается из Blazor-оверлея).</summary>
|
||||||
|
public void Connect(Uri server) => _scene?.Connect(server);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Initialize()
|
protected override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
Context.Scenes.Switch(new WorldViewScene(_server, () => _spriteBatch, () => _pixel));
|
_scene = new WorldViewScene(() => _spriteBatch, () => _pixel);
|
||||||
|
Context.Scenes.Switch(_scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@page "/index.html"
|
@page "/index.html"
|
||||||
@inject IJSRuntime JsRuntime
|
@inject IJSRuntime JsRuntime
|
||||||
@using nkast.Wasm.Canvas
|
@using nkast.Wasm.Canvas
|
||||||
@@ -18,3 +18,12 @@
|
|||||||
">
|
">
|
||||||
<canvas id="theCanvas" style="touch-action:none;"></canvas>
|
<canvas id="theCanvas" style="touch-action:none;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="connect-bar">
|
||||||
|
<span class="title">LittleSim</span>
|
||||||
|
<input class="server" placeholder="ws://host:9050"
|
||||||
|
@bind="_serverAddress" @bind:event="oninput"
|
||||||
|
@onkeydown="OnAddressKey" />
|
||||||
|
<button class="connect" @onclick="Connect">Подключиться</button>
|
||||||
|
<span class="status @StatusClass">@StatusText</span>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
|
|
||||||
namespace LittleSim.Web.Pages
|
namespace LittleSim.Web.Pages
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,12 @@ namespace LittleSim.Web.Pages
|
|||||||
[Inject]
|
[Inject]
|
||||||
private NavigationManager Navigation { get; set; } = null!;
|
private NavigationManager Navigation { get; set; } = null!;
|
||||||
|
|
||||||
private Game? _game;
|
private LittleSimWebGame? _game;
|
||||||
|
private string _serverAddress = "";
|
||||||
|
private Uri? _pendingServer; // запрошен Connect до создания игры — подключим в первом тике
|
||||||
|
private ConnectionStatus _lastStatus = ConnectionStatus.Idle;
|
||||||
|
private int _lastCount = -1;
|
||||||
|
private int _frame;
|
||||||
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
protected override void OnAfterRender(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -18,25 +23,92 @@ namespace LittleSim.Web.Pages
|
|||||||
|
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
|
_serverAddress = DefaultServerAddress();
|
||||||
|
StateHasChanged();
|
||||||
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Гонится из requestAnimationFrame (index.html). Игру создаём лениво в первом тике —
|
||||||
|
// мира она не создаёт, лишь ждёт адрес сервера из формы.
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public void TickDotNet()
|
public void TickDotNet()
|
||||||
{
|
{
|
||||||
if (_game == null)
|
if (_game == null)
|
||||||
{
|
{
|
||||||
_game = new LittleSimWebGame(ResolveServerUri());
|
_game = new LittleSimWebGame();
|
||||||
_game.Run();
|
_game.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
_game.Tick();
|
_game.Tick();
|
||||||
|
|
||||||
|
if (_pendingServer is not null)
|
||||||
|
{
|
||||||
|
_game.Connect(_pendingServer);
|
||||||
|
_pendingServer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Адрес сервера: ?server=ws://host:port в URL страницы; по умолчанию —
|
// Перерисовываем оверлей только при смене статуса (или изредка — чтобы освежить счётчик),
|
||||||
// хост самой страницы на порту LittleSim.Server.
|
// а не каждый кадр rAF.
|
||||||
private Uri ResolveServerUri()
|
var status = _game.Status;
|
||||||
|
var count = _game.EntityCount;
|
||||||
|
if (
|
||||||
|
status != _lastStatus
|
||||||
|
|| (status == ConnectionStatus.Connected && count != _lastCount && _frame % 15 == 0)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_lastStatus = status;
|
||||||
|
_lastCount = count;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
_frame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAddressKey(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == "Enter")
|
||||||
|
{
|
||||||
|
Connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connect()
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(_serverAddress?.Trim(), UriKind.Absolute, out var uri))
|
||||||
|
{
|
||||||
|
return; // адрес не похож на ws://… — игнорируем
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_game is not null)
|
||||||
|
{
|
||||||
|
_game.Connect(uri);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pendingServer = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string StatusText =>
|
||||||
|
_lastStatus switch
|
||||||
|
{
|
||||||
|
ConnectionStatus.Connecting => "подключение…",
|
||||||
|
ConnectionStatus.Connected => $"подключено · {_lastCount} жит.",
|
||||||
|
ConnectionStatus.Reconnecting => "переподключение…",
|
||||||
|
_ => "не подключено",
|
||||||
|
};
|
||||||
|
|
||||||
|
private string StatusClass =>
|
||||||
|
_lastStatus switch
|
||||||
|
{
|
||||||
|
ConnectionStatus.Connected => "ok",
|
||||||
|
ConnectionStatus.Connecting or ConnectionStatus.Reconnecting => "warn",
|
||||||
|
_ => "idle",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Адрес по умолчанию: ?server=ws://host:port из URL страницы, иначе хост страницы на порту сервера.
|
||||||
|
private string DefaultServerAddress()
|
||||||
{
|
{
|
||||||
var page = new Uri(Navigation.Uri);
|
var page = new Uri(Navigation.Uri);
|
||||||
var query = page.Query.TrimStart('?');
|
var query = page.Query.TrimStart('?');
|
||||||
@@ -45,7 +117,7 @@ namespace LittleSim.Web.Pages
|
|||||||
var separator = pair.IndexOf('=');
|
var separator = pair.IndexOf('=');
|
||||||
if (separator > 0 && pair[..separator] == "server")
|
if (separator > 0 && pair[..separator] == "server")
|
||||||
{
|
{
|
||||||
return new Uri(Uri.UnescapeDataString(pair[(separator + 1)..]));
|
return Uri.UnescapeDataString(pair[(separator + 1)..]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +126,7 @@ namespace LittleSim.Web.Pages
|
|||||||
Scheme = "ws",
|
Scheme = "ws",
|
||||||
Host = page.Host,
|
Host = page.Host,
|
||||||
Port = WebNetSchema.DefaultPort,
|
Port = WebNetSchema.DefaultPort,
|
||||||
}.Uri;
|
}.Uri.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
.connect-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(12, 16, 24, 0.82);
|
||||||
|
border-bottom: 1px solid rgba(108, 198, 255, 0.25);
|
||||||
|
color: #d8e0ea;
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6cc6ff;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .server {
|
||||||
|
flex: 0 1 320px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 5px 9px;
|
||||||
|
background: #0c1018;
|
||||||
|
border: 1px solid #2a3850;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e6edf6;
|
||||||
|
font-family: ui-monospace, 'Cascadia Code', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .server:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #6cc6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .connect {
|
||||||
|
padding: 5px 14px;
|
||||||
|
background: #1c2c44;
|
||||||
|
border: 1px solid #3a567f;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d8e0ea;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .connect:hover {
|
||||||
|
background: #25395a;
|
||||||
|
border-color: #6cc6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status {
|
||||||
|
margin-left: auto;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status.ok {
|
||||||
|
color: #7fdca0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status.warn {
|
||||||
|
color: #f0c060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-bar .status.idle {
|
||||||
|
color: #8a96a6;
|
||||||
|
}
|
||||||
@@ -9,31 +9,74 @@ using MrGameEng.Net;
|
|||||||
|
|
||||||
namespace LittleSim.Web;
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
/// <summary>Состояние соединения веб-клиента — его читает оверлей подключения (Blazor).</summary>
|
||||||
|
public enum ConnectionStatus
|
||||||
|
{
|
||||||
|
/// <summary>Сервер ещё не выбран — ждём, пока пользователь нажмёт «Подключиться».</summary>
|
||||||
|
Idle,
|
||||||
|
|
||||||
|
/// <summary>Идёт первая попытка подключения.</summary>
|
||||||
|
Connecting,
|
||||||
|
|
||||||
|
/// <summary>Соединение установлено, снапшоты приходят.</summary>
|
||||||
|
Connected,
|
||||||
|
|
||||||
|
/// <summary>Соединение оборвалось/не удалось — ждём следующей попытки (бэкофф).</summary>
|
||||||
|
Reconnecting,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Браузерный клиент мира LittleSim: подключается к дедикейтед-серверу
|
/// Браузерный клиент мира LittleSim: по команде <see cref="Connect"/> подключается к
|
||||||
/// (LittleSim.Server --listen), применяет дельта-снапшоты в свой EntityStore и рисует
|
/// дедикейтед-серверу (LittleSim.Server --listen), применяет дельта-снапшоты в свой
|
||||||
/// жителей через KNI SpriteBatch (WebGL). Симуляция целиком на сервере — сюда приезжают
|
/// EntityStore и рисует жителей через KNI SpriteBatch (WebGL). Мира не создаёт — только
|
||||||
/// только компоненты схемы (позиция + потребности); позиции сглаживаются до частоты
|
/// наблюдает: симуляция целиком на сервере, сюда приезжают лишь компоненты схемы (позиция +
|
||||||
/// кадра, усталость затемняет квадратик жителя.
|
/// потребности). Позиции сглаживаются до частоты кадра, усталость затемняет квадратик. При
|
||||||
|
/// обрыве переподключается сам с экспоненциальным бэкоффом, очищая устаревшие сущности.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WorldViewScene : Scene
|
public sealed class WorldViewScene : Scene
|
||||||
{
|
{
|
||||||
private readonly Uri _server;
|
// Бэкофф переподключения: задержка удваивается с каждой неудачей до потолка.
|
||||||
|
private const float MaxBackoffSeconds = 8f;
|
||||||
|
|
||||||
private readonly Func<SpriteBatch> _spriteBatch;
|
private readonly Func<SpriteBatch> _spriteBatch;
|
||||||
private readonly Func<Texture2D> _pixel;
|
private readonly Func<Texture2D> _pixel;
|
||||||
|
|
||||||
private ReplicationClient _replication = null!;
|
private ReplicationClient _replication = null!;
|
||||||
private Task<WebSocketClient>? _connecting;
|
private Task<WebSocketClient>? _connecting;
|
||||||
private WebSocketClient? _connection;
|
private WebSocketClient? _connection;
|
||||||
|
private Uri? _server; // целевой сервер; null — пользователь ещё не подключался
|
||||||
|
private double _nextAttemptAt;
|
||||||
|
private int _attempt;
|
||||||
|
|
||||||
/// <summary>Сцена, подключающаяся к <paramref name="server"/>.</summary>
|
/// <summary>Текущее состояние соединения — для оверлея подключения.</summary>
|
||||||
public WorldViewScene(Uri server, Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
public ConnectionStatus Status { get; private set; } = ConnectionStatus.Idle;
|
||||||
|
|
||||||
|
/// <summary>Сколько реплицированных жителей сейчас в мире клиента.</summary>
|
||||||
|
public int EntityCount => _replication?.EntityCount ?? 0;
|
||||||
|
|
||||||
|
/// <summary>Сцена-наблюдатель; сервер задаётся позже через <see cref="Connect"/>.</summary>
|
||||||
|
public WorldViewScene(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||||
{
|
{
|
||||||
_server = server;
|
|
||||||
_spriteBatch = spriteBatch;
|
_spriteBatch = spriteBatch;
|
||||||
_pixel = pixel;
|
_pixel = pixel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подключиться к <paramref name="server"/> (ws:// или wss://). Сбрасывает прежнее
|
||||||
|
/// соединение и счётчик попыток — подключение начнётся в ближайшем тике. Дальше клиент сам
|
||||||
|
/// переподключается к этому адресу при обрывах.
|
||||||
|
/// </summary>
|
||||||
|
public void Connect(Uri server)
|
||||||
|
{
|
||||||
|
_connection?.Close();
|
||||||
|
_connection = null;
|
||||||
|
_replication?.Clear();
|
||||||
|
_server = server;
|
||||||
|
_attempt = 0;
|
||||||
|
_nextAttemptAt = 0f;
|
||||||
|
Status = ConnectionStatus.Connecting;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
|
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
|
||||||
private struct NetLerp : IComponent
|
private struct NetLerp : IComponent
|
||||||
{
|
{
|
||||||
@@ -51,35 +94,68 @@ public sealed class WorldViewScene : Scene
|
|||||||
UpdateSystems.Add(new NetSmoothingSystem());
|
UpdateSystems.Add(new NetSmoothingSystem());
|
||||||
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
|
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
|
||||||
|
|
||||||
Log.Info($"LittleSim.Web: connecting to {_server}…");
|
Log.Info("LittleSim.Web ready — awaiting connect");
|
||||||
_connecting = WebSocketClient.ConnectAsync(_server);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUnload() => _connection?.Close();
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
private void Pump()
|
private void Pump()
|
||||||
{
|
{
|
||||||
|
if (_server is null)
|
||||||
|
{
|
||||||
|
return; // адрес ещё не задан — ждём команды Connect из оверлея
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Context.Clock.UnscaledTotalTime;
|
||||||
|
|
||||||
|
// Завершилась попытка подключения: успех — берём соединение; провал — планируем повтор.
|
||||||
if (_connecting is { IsCompleted: true } finished)
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
{
|
{
|
||||||
_connecting = null;
|
_connecting = null;
|
||||||
if (finished.IsFaulted)
|
if (finished.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
_replication.Clear(); // сбрасываем устаревшие сущности перед свежим полным снапшотом
|
||||||
|
_attempt = 0;
|
||||||
|
Status = ConnectionStatus.Connected;
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Log.Error(
|
Log.Error(
|
||||||
$"Connect to {_server} failed: "
|
$"Connect to {_server} failed: "
|
||||||
+ finished.Exception?.GetBaseException().Message
|
+ finished.Exception?.GetBaseException().Message
|
||||||
);
|
);
|
||||||
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
// Обрыв установленного соединения: чистим и уходим в переподключение.
|
||||||
|
if (_connection is { IsOpen: false })
|
||||||
{
|
{
|
||||||
_connection = finished.Result;
|
_connection = null;
|
||||||
Log.Info($"Connected to {_server}");
|
_replication.Clear();
|
||||||
}
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_connection is not null)
|
if (_connection is not null)
|
||||||
{
|
{
|
||||||
_replication.Pump(_connection);
|
_replication.Pump(_connection);
|
||||||
}
|
}
|
||||||
|
else if (_connecting is null && now >= _nextAttemptAt)
|
||||||
|
{
|
||||||
|
Status = _attempt == 0 ? ConnectionStatus.Connecting : ConnectionStatus.Reconnecting;
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспоненциальный бэкофф: 1, 2, 4, 8, 8, … секунд между попытками.
|
||||||
|
private void ScheduleReconnect(double now)
|
||||||
|
{
|
||||||
|
var delay = MathF.Min(MaxBackoffSeconds, 1f * (1 << Math.Min(_attempt, 3)));
|
||||||
|
_attempt++;
|
||||||
|
_nextAttemptAt = now + delay;
|
||||||
|
Status = ConnectionStatus.Reconnecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
|
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ public sealed class GameSettings
|
|||||||
|
|
||||||
/// <summary>Общая громкость 0..1.</summary>
|
/// <summary>Общая громкость 0..1.</summary>
|
||||||
public float Volume { get; set; } = 1f;
|
public float Volume { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>Масштаб интерфейса (1 — обычный); применяется к Myra-десктопу всех сцен.</summary>
|
||||||
|
public float UiScale { get; set; } = 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
/// <summary>Чтение/запись <see cref="GameSettings"/> и применение их к движку.</summary>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public sealed class ScatterEntry
|
|||||||
public List<string> Options { get; init; } = [];
|
public List<string> Options { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Тип клетки рельефа (Defs/terrain.json): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
/// <summary>Тип клетки рельефа (Defs/Terrain/): порог высоты, цвет, поверхность, скаттер растений.</summary>
|
||||||
public sealed class TerrainDef : Def
|
public sealed class TerrainDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
/// <summary>Верхняя граница высоты (клетка относится к первому дефу, чей порог выше её высоты).</summary>
|
||||||
@@ -67,7 +67,7 @@ public sealed class PlantGrowthStage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
|
/// Продукт (Defs/Products/): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||||
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||||
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -93,9 +93,15 @@ public sealed class GenomeDef
|
|||||||
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
/// <summary>Оптимальная температура (°C) и её толерантность.</summary>
|
||||||
public float OptimalTemperature { get; init; } = 18f;
|
public float OptimalTemperature { get; init; } = 18f;
|
||||||
|
|
||||||
/// <summary>Ширина толерантности по температуре (°C).</summary>
|
/// <summary>Ширина толерантности по температуре (°C) — половина плато полного роста вокруг оптимума.</summary>
|
||||||
public float TemperatureTolerance { get; init; } = 12f;
|
public float TemperatureTolerance { get; init; } = 12f;
|
||||||
|
|
||||||
|
/// <summary>Морозостойкость (°C): на сколько ниже плато рост спадает до нуля (жёсткая нижняя граница).</summary>
|
||||||
|
public float ColdHardiness { get; init; } = 8f;
|
||||||
|
|
||||||
|
/// <summary>Жаростойкость (°C): на сколько выше плато рост спадает до нуля (жёсткая верхняя граница).</summary>
|
||||||
|
public float HeatHardiness { get; init; } = 10f;
|
||||||
|
|
||||||
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
/// <summary>Оптимальная плодородность почвы и её толерантность.</summary>
|
||||||
public float OptimalFertility { get; init; } = 1f;
|
public float OptimalFertility { get; init; } = 1f;
|
||||||
|
|
||||||
@@ -139,7 +145,7 @@ public sealed class GenomeDef
|
|||||||
public float LeafHue { get; init; } = 0.33f;
|
public float LeafHue { get; init; } = 0.33f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
/// <summary>Растение (Defs/Plants/): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||||
public sealed class PlantDef : Def
|
public sealed class PlantDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
/// <summary>Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.</summary>
|
||||||
@@ -165,7 +171,7 @@ public sealed class PlantDef : Def
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Пресет размера мира (Defs/worldpresets.json): габариты сетки и население. Меню «Новый
|
/// Пресет размера мира (Defs/WorldPresets/): габариты сетки и население. Меню «Новый
|
||||||
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
/// мир» строит список размеров из этих дефов — новая механика приходит данными мода, а не
|
||||||
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
/// хардкодом. <see cref="Def.Label"/> — ключ локализации (preset.*).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -184,7 +190,7 @@ public sealed class WorldPresetDef : Def
|
|||||||
public int Order { get; init; }
|
public int Order { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Существо (Defs/pawns.json): текстура, размер и вид (житель/животное).</summary>
|
/// <summary>Существо (Defs/Pawns/): текстура, размер и вид (житель/животное).</summary>
|
||||||
public sealed class PawnDef : Def
|
public sealed class PawnDef : Def
|
||||||
{
|
{
|
||||||
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
/// <summary>Вид существа: жители ("being") живут в мировой сцене, животные ("animal") — в террейне.</summary>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public sealed class PlantSet
|
|||||||
/// <summary>Индекс вида по дефу.</summary>
|
/// <summary>Индекс вида по дефу.</summary>
|
||||||
public int IndexOf(PlantDef def) => _index[def];
|
public int IndexOf(PlantDef def) => _index[def];
|
||||||
|
|
||||||
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр
|
// Переводит базовый геном вида (числа из Defs/Plants/) в набор генов: каждое число — центр
|
||||||
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
|
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
|
||||||
private static GenomeTemplate BuildTemplate(
|
private static GenomeTemplate BuildTemplate(
|
||||||
GenomeDef g,
|
GenomeDef g,
|
||||||
@@ -92,7 +92,7 @@ public sealed class PlantSet
|
|||||||
genes.TryGetValue(id, out var def)
|
genes.TryGetValue(id, out var def)
|
||||||
? def
|
? def
|
||||||
: throw new KeyNotFoundException(
|
: throw new KeyNotFoundException(
|
||||||
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?"
|
$"Gene def '{id}' not found — is Mods/Core/Defs/Genes/ loaded?"
|
||||||
);
|
);
|
||||||
|
|
||||||
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
|
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
|
||||||
@@ -103,6 +103,8 @@ public sealed class PlantSet
|
|||||||
Numeric("GeneLightTolerance", g.LightTolerance),
|
Numeric("GeneLightTolerance", g.LightTolerance),
|
||||||
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
|
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
|
||||||
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
|
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
|
||||||
|
Numeric("GeneColdHardiness", g.ColdHardiness),
|
||||||
|
Numeric("GeneHeatHardiness", g.HeatHardiness),
|
||||||
Numeric("GeneOptimalFertility", g.OptimalFertility),
|
Numeric("GeneOptimalFertility", g.OptimalFertility),
|
||||||
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
|
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
|
||||||
Numeric("GeneVigor", g.Vigor),
|
Numeric("GeneVigor", g.Vigor),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public sealed class BootScene : Scene
|
|||||||
host.Graphics.ApplyChanges();
|
host.Graphics.ApplyChanges();
|
||||||
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
|
Context.Services.Get<AudioManager>().MasterVolume = _settings.Volume;
|
||||||
|
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
_label = new Label
|
_label = new Label
|
||||||
{
|
{
|
||||||
TextColor = Ui.Accent,
|
TextColor = Ui.Accent,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class CreditsScene : Scene
|
|||||||
{
|
{
|
||||||
var lang = Context.Services.Get<GameContent>().Languages;
|
var lang = Context.Services.Get<GameContent>().Languages;
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var column = Ui.Column(12);
|
var column = Ui.Column(12);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("credits.title")));
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public static class GameLayers
|
|||||||
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
|
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
|
||||||
public static LayerId Beings { get; private set; }
|
public static LayerId Beings { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Слой-оверлей поверх мира: рамка выделения (рисуется после существ, без Y-сорта).</summary>
|
||||||
|
public static LayerId Selection { get; private set; }
|
||||||
|
|
||||||
/// <summary>Регистрирует слои при первом вызове.</summary>
|
/// <summary>Регистрирует слои при первом вызове.</summary>
|
||||||
public static void EnsureRegistered(Renderer2D renderer)
|
public static void EnsureRegistered(Renderer2D renderer)
|
||||||
{
|
{
|
||||||
@@ -22,6 +25,7 @@ public static class GameLayers
|
|||||||
}
|
}
|
||||||
|
|
||||||
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
|
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
|
||||||
|
Selection = renderer.Layers.Register("Selection", LayerSpace.World, LayerSortMode.Depth);
|
||||||
_registered = true;
|
_registered = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class LoadGameScene : Scene
|
|||||||
{
|
{
|
||||||
_content = Context.Services.Get<GameContent>();
|
_content = Context.Services.Get<GameContent>();
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
_desktop = this.UseUI();
|
_desktop = this.UseScaledUI();
|
||||||
Rebuild();
|
Rebuild();
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class MainMenuScene : Scene
|
|||||||
{
|
{
|
||||||
var content = Context.Services.Get<GameContent>();
|
var content = Context.Services.Get<GameContent>();
|
||||||
var lang = content.Languages;
|
var lang = content.Languages;
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var column = Ui.Column(12);
|
var column = Ui.Column(12);
|
||||||
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
column.Widgets.Add(Ui.Title(lang.Get("menu.title")));
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public sealed class MultiplayerScene : Scene
|
|||||||
{
|
{
|
||||||
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
|
private static readonly RectF Bounds = new(0f, 0f, 1280f, 720f);
|
||||||
|
|
||||||
|
// Бэкофф переподключения: задержка удваивается с каждой неудачей до потолка.
|
||||||
|
private const float MaxBackoffSeconds = 8f;
|
||||||
|
|
||||||
private readonly Uri _server;
|
private readonly Uri _server;
|
||||||
private Task<WebSocketClient>? _connecting;
|
private Task<WebSocketClient>? _connecting;
|
||||||
private WebSocketClient? _connection;
|
private WebSocketClient? _connection;
|
||||||
@@ -38,6 +41,8 @@ public sealed class MultiplayerScene : Scene
|
|||||||
private GameContent _content = null!;
|
private GameContent _content = null!;
|
||||||
private Label _hud = null!;
|
private Label _hud = null!;
|
||||||
private string _statusKey = "net.connecting";
|
private string _statusKey = "net.connecting";
|
||||||
|
private double _nextAttemptAt; // время (unscaled) следующей попытки подключения
|
||||||
|
private int _attempt; // 0 — первая попытка; растёт при обрывах, задаёт бэкофф
|
||||||
|
|
||||||
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
|
/// <summary>Сцена, подключающаяся к серверу <paramref name="server"/> (ws:// или wss://).</summary>
|
||||||
public MultiplayerScene(Uri server) => _server = server;
|
public MultiplayerScene(Uri server) => _server = server;
|
||||||
@@ -64,7 +69,7 @@ public sealed class MultiplayerScene : Scene
|
|||||||
entity.AddComponent(new NetLerp());
|
entity.AddComponent(new NetLerp());
|
||||||
};
|
};
|
||||||
|
|
||||||
var desktop = this.UseUI();
|
var desktop = this.UseScaledUI();
|
||||||
_hud = new Label { Left = 10, Top = 8 };
|
_hud = new Label { Left = 10, Top = 8 };
|
||||||
desktop.Root = Ui.Screen(_hud);
|
desktop.Root = Ui.Screen(_hud);
|
||||||
|
|
||||||
@@ -86,39 +91,53 @@ public sealed class MultiplayerScene : Scene
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
_connecting = WebSocketClient.ConnectAsync(_server);
|
// Первую попытку запускает сам Pump (единый путь с переподключением): _nextAttemptAt = 0.
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUnload() => _connection?.Close();
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
private void Pump()
|
private void Pump()
|
||||||
{
|
{
|
||||||
|
var now = Context.Clock.UnscaledTotalTime;
|
||||||
|
|
||||||
|
// Завершилась попытка подключения: успех — берём соединение; провал — планируем повтор.
|
||||||
if (_connecting is { IsCompleted: true } finished)
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
{
|
{
|
||||||
_connecting = null;
|
_connecting = null;
|
||||||
if (finished.IsFaulted)
|
if (finished.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
_replication.Clear(); // сбрасываем устаревшие сущности перед свежим полным снапшотом
|
||||||
|
_attempt = 0;
|
||||||
|
_statusKey = "net.connected";
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_statusKey = "net.failed";
|
|
||||||
Log.Error(
|
Log.Error(
|
||||||
$"Connect to {_server} failed: "
|
$"Connect to {_server} failed: "
|
||||||
+ finished.Exception?.GetBaseException().Message
|
+ finished.Exception?.GetBaseException().Message
|
||||||
);
|
);
|
||||||
|
ScheduleReconnect(now);
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
// Обрыв уже установленного соединения: чистим и уходим в переподключение.
|
||||||
|
if (_connection is { IsOpen: false })
|
||||||
{
|
{
|
||||||
_connection = finished.Result;
|
_connection = null;
|
||||||
_statusKey = "net.connected";
|
_replication.Clear();
|
||||||
Log.Info($"Connected to {_server}");
|
ScheduleReconnect(now);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_connection is not null)
|
if (_connection is not null)
|
||||||
{
|
{
|
||||||
_replication.Pump(_connection);
|
_replication.Pump(_connection);
|
||||||
if (!_connection.IsOpen)
|
|
||||||
{
|
|
||||||
_statusKey = "net.lost";
|
|
||||||
}
|
}
|
||||||
|
else if (_connecting is null && now >= _nextAttemptAt)
|
||||||
|
{
|
||||||
|
_statusKey = _attempt == 0 ? "net.connecting" : "net.reconnecting";
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
}
|
}
|
||||||
|
|
||||||
_hud.Text = _content.Languages.Format(
|
_hud.Text = _content.Languages.Format(
|
||||||
@@ -133,4 +152,13 @@ public sealed class MultiplayerScene : Scene
|
|||||||
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Экспоненциальный бэкофф: 1, 2, 4, 8, 8, … секунд между попытками.
|
||||||
|
private void ScheduleReconnect(double now)
|
||||||
|
{
|
||||||
|
var delay = MathF.Min(MaxBackoffSeconds, 1f * (1 << Math.Min(_attempt, 3)));
|
||||||
|
_attempt++;
|
||||||
|
_nextAttemptAt = now + delay;
|
||||||
|
_statusKey = "net.reconnecting";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public sealed class NewWorldScene : Scene
|
|||||||
var content = Context.Services.Get<GameContent>();
|
var content = Context.Services.Get<GameContent>();
|
||||||
var lang = content.Languages;
|
var lang = content.Languages;
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
var desktop = this.UseUI(); // ставит MyraEnvironment.Game до создания виджетов
|
var desktop = this.UseScaledUI(); // ставит MyraEnvironment.Game до создания виджетов
|
||||||
|
|
||||||
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
var presets = content.Defs.All<WorldPresetDef>().OrderBy(p => p.Order).ToList();
|
||||||
_preset = presets[0];
|
_preset = presets[0];
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ internal sealed class PauseMenu
|
|||||||
private readonly GameSpeed _speed;
|
private readonly GameSpeed _speed;
|
||||||
private readonly GameHost _host;
|
private readonly GameHost _host;
|
||||||
private readonly AudioManager _audio;
|
private readonly AudioManager _audio;
|
||||||
|
private readonly Desktop _desktop;
|
||||||
private readonly Func<string> _onSave;
|
private readonly Func<string> _onSave;
|
||||||
private readonly Action _onMainMenu;
|
private readonly Action _onMainMenu;
|
||||||
private readonly Action _onQuit;
|
private readonly Action _onQuit;
|
||||||
@@ -36,6 +37,7 @@ internal sealed class PauseMenu
|
|||||||
EngineContext context,
|
EngineContext context,
|
||||||
GameContent content,
|
GameContent content,
|
||||||
GameSpeed speed,
|
GameSpeed speed,
|
||||||
|
Desktop desktop,
|
||||||
Func<string> onSave,
|
Func<string> onSave,
|
||||||
Action onMainMenu,
|
Action onMainMenu,
|
||||||
Action onQuit
|
Action onQuit
|
||||||
@@ -44,6 +46,7 @@ internal sealed class PauseMenu
|
|||||||
_context = context;
|
_context = context;
|
||||||
_content = content;
|
_content = content;
|
||||||
_speed = speed;
|
_speed = speed;
|
||||||
|
_desktop = desktop;
|
||||||
_host = (GameHost)context.Services.Get<Game>();
|
_host = (GameHost)context.Services.Get<Game>();
|
||||||
_audio = context.Services.Get<AudioManager>();
|
_audio = context.Services.Get<AudioManager>();
|
||||||
_onSave = onSave;
|
_onSave = onSave;
|
||||||
@@ -144,6 +147,7 @@ internal sealed class PauseMenu
|
|||||||
{
|
{
|
||||||
GameSettingsStore.Save(_settings);
|
GameSettingsStore.Save(_settings);
|
||||||
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||||
|
UiScaling.Apply(_context, _desktop, _settings.UiScale); // масштаб UI — сразу
|
||||||
ShowSettings(); // язык мог смениться
|
ShowSettings(); // язык мог смениться
|
||||||
},
|
},
|
||||||
onBack: () =>
|
onBack: () =>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public sealed class SettingsScene : Scene
|
|||||||
_host = (GameHost)Context.Services.Get<Game>();
|
_host = (GameHost)Context.Services.Get<Game>();
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
|
|
||||||
_desktop = this.UseUI();
|
_desktop = this.UseScaledUI();
|
||||||
Rebuild();
|
Rebuild();
|
||||||
|
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
@@ -53,6 +53,7 @@ public sealed class SettingsScene : Scene
|
|||||||
{
|
{
|
||||||
GameSettingsStore.Save(_settings);
|
GameSettingsStore.Save(_settings);
|
||||||
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
GameSettingsStore.Apply(_settings, _host.Graphics, _content.Languages, _audio);
|
||||||
|
UiScaling.Apply(Context, _desktop, _settings.UiScale); // масштаб UI — сразу
|
||||||
Rebuild(); // язык мог смениться — перестраиваем подписи
|
Rebuild(); // язык мог смениться — перестраиваем подписи
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,12 +45,23 @@ public sealed class WorldScene : Scene
|
|||||||
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
|
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
|
||||||
public const int DensityCap = 4;
|
public const int DensityCap = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// День года, с которого начинается мир — тёплая весна (~16 °C), а не холодный рубеж года.
|
||||||
|
/// Сдвигает сезонную фазу климата, давая длинный тёплый сезон роста до первой зимы.
|
||||||
|
/// </summary>
|
||||||
|
public const int StartDayOfYear = 10;
|
||||||
|
|
||||||
|
/// <summary>Час суток на старте мира — утро (07:00), а не полночь.</summary>
|
||||||
|
public const int StartHour = 7;
|
||||||
|
|
||||||
private readonly WorldConfig _config;
|
private readonly WorldConfig _config;
|
||||||
private readonly WorldSave? _save;
|
private readonly WorldSave? _save;
|
||||||
private readonly RectF _bounds;
|
private readonly RectF _bounds;
|
||||||
|
|
||||||
private GameSpeed _speed = null!;
|
private GameSpeed _speed = null!;
|
||||||
private PauseMenu _pause = null!;
|
private PauseMenu _pause = null!;
|
||||||
|
private Selection _selection = null!;
|
||||||
|
private InspectPanel _inspect = null!;
|
||||||
private readonly List<Action> _speedRefreshers = [];
|
private readonly List<Action> _speedRefreshers = [];
|
||||||
|
|
||||||
private PlantSet _plants = null!;
|
private PlantSet _plants = null!;
|
||||||
@@ -87,8 +98,13 @@ public sealed class WorldScene : Scene
|
|||||||
GameLayers.EnsureRegistered(renderer);
|
GameLayers.EnsureRegistered(renderer);
|
||||||
this.UseTilemaps();
|
this.UseTilemaps();
|
||||||
|
|
||||||
var calendar = Context.UseCalendar(SecondsPerDay);
|
var calendar = Context.UseCalendar(SecondsPerDay, StartHour / 24.0);
|
||||||
var climate = Context.UseClimate(ClimateSettings.Default);
|
var climate = Context.UseClimate(
|
||||||
|
ClimateSettings.Default with
|
||||||
|
{
|
||||||
|
StartDayOfYear = StartDayOfYear,
|
||||||
|
}
|
||||||
|
);
|
||||||
var dayNight = new DayNight(calendar, DayNightSettings.Default);
|
var dayNight = new DayNight(calendar, DayNightSettings.Default);
|
||||||
|
|
||||||
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
||||||
@@ -121,18 +137,26 @@ public sealed class WorldScene : Scene
|
|||||||
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
||||||
|
|
||||||
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
||||||
var desktop = this.UseUI();
|
var desktop = this.UseScaledUI();
|
||||||
var hudLabel = new Label { Left = 10, Top = 8 };
|
var hudLabel = new Label { Left = 10, Top = 8 };
|
||||||
var speedBar = BuildSpeedBar(content);
|
var speedBar = BuildSpeedBar(content);
|
||||||
_pause = new PauseMenu(
|
_pause = new PauseMenu(
|
||||||
Context,
|
Context,
|
||||||
content,
|
content,
|
||||||
_speed,
|
_speed,
|
||||||
|
desktop,
|
||||||
onSave: SaveWorld,
|
onSave: SaveWorld,
|
||||||
onMainMenu: () => Switch(new MainMenuScene()),
|
onMainMenu: () => Switch(new MainMenuScene()),
|
||||||
onQuit: () => Context.Services.Get<Game>().Exit()
|
onQuit: () => Context.Services.Get<Game>().Exit()
|
||||||
);
|
);
|
||||||
desktop.Root = Ui.Screen(hudLabel, speedBar, _pause.Root);
|
_selection = new Selection();
|
||||||
|
var brackets = new SelectionBrackets(
|
||||||
|
Store,
|
||||||
|
atlases.GetRegion(device, "ui/overlays/selectionbracket"),
|
||||||
|
GameLayers.Selection
|
||||||
|
);
|
||||||
|
_inspect = new InspectPanel(Store, _plants, content, climate, _selection, brackets);
|
||||||
|
desktop.Root = Ui.Screen(hudLabel, _inspect.Panel, speedBar, _pause.Root);
|
||||||
|
|
||||||
this.UseInspector(renderer);
|
this.UseInspector(renderer);
|
||||||
var console = this.UseDevConsole();
|
var console = this.UseDevConsole();
|
||||||
@@ -168,6 +192,7 @@ public sealed class WorldScene : Scene
|
|||||||
Store,
|
Store,
|
||||||
_plants,
|
_plants,
|
||||||
calendar,
|
calendar,
|
||||||
|
climate,
|
||||||
Context.Clock,
|
Context.Clock,
|
||||||
_config.Width,
|
_config.Width,
|
||||||
_config.Height,
|
_config.Height,
|
||||||
@@ -181,6 +206,17 @@ public sealed class WorldScene : Scene
|
|||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||||||
);
|
);
|
||||||
|
UpdateSystems.Add(
|
||||||
|
new SelectionSystem(
|
||||||
|
Store,
|
||||||
|
input,
|
||||||
|
renderer,
|
||||||
|
_selection,
|
||||||
|
() => _pause.IsOpen,
|
||||||
|
() => desktop.IsMouseOverGUI
|
||||||
|
)
|
||||||
|
);
|
||||||
|
UpdateSystems.Add(new CallbackSystem(() => _inspect.Refresh()));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
new HudSystem(
|
new HudSystem(
|
||||||
Context,
|
Context,
|
||||||
@@ -308,9 +344,17 @@ public sealed class WorldScene : Scene
|
|||||||
private void Hotkeys(InputManager input)
|
private void Hotkeys(InputManager input)
|
||||||
{
|
{
|
||||||
if (input.IsKeyPressed(Keys.Escape))
|
if (input.IsKeyPressed(Keys.Escape))
|
||||||
|
{
|
||||||
|
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
|
||||||
|
if (_selection.HasSelection)
|
||||||
|
{
|
||||||
|
_selection.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_pause.Toggle();
|
_pause.Toggle();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_pause.IsOpen)
|
if (_pause.IsOpen)
|
||||||
{
|
{
|
||||||
@@ -587,6 +631,11 @@ public sealed class WorldScene : Scene
|
|||||||
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
||||||
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
||||||
);
|
);
|
||||||
|
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||||
|
console.WriteLine(
|
||||||
|
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
|
||||||
|
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
|
||||||
|
);
|
||||||
|
|
||||||
if (def.HarvestProduct is { } harvest)
|
if (def.HarvestProduct is { } harvest)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ public static class PlantFactory
|
|||||||
CellFertility = cellFertility,
|
CellFertility = cellFertility,
|
||||||
},
|
},
|
||||||
new PlantOrganism { Genome = genome, Traits = traits },
|
new PlantOrganism { Genome = genome, Traits = traits },
|
||||||
new Fruiting()
|
new Fruiting(),
|
||||||
|
new TemperatureStress()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,12 @@ public sealed class PlantGrowthSystem(
|
|||||||
|
|
||||||
ref var traits = ref orgs[i].Traits;
|
ref var traits = ref orgs[i].Traits;
|
||||||
var light = lighting.SampleAt(t[i].Position);
|
var light = lighting.SampleAt(t[i].Position);
|
||||||
|
// Температура — трапеция RimWorld: плато полного роста, жёсткие края (дормантность вне).
|
||||||
|
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||||
var rate =
|
var rate =
|
||||||
traits.Vigor
|
traits.Vigor
|
||||||
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
|
* Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
|
||||||
* Suitability.Gaussian(
|
* Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax)
|
||||||
temperature,
|
|
||||||
traits.OptimalTemperature,
|
|
||||||
traits.TemperatureTolerance
|
|
||||||
)
|
|
||||||
* Suitability.Gaussian(
|
* Suitability.Gaussian(
|
||||||
grow.CellFertility,
|
grow.CellFertility,
|
||||||
traits.OptimalFertility,
|
traits.OptimalFertility,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Friflo.Engine.ECS;
|
|||||||
using Friflo.Engine.ECS.Systems;
|
using Friflo.Engine.ECS.Systems;
|
||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
using MrGameEng.Genetics;
|
using MrGameEng.Genetics;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
@@ -9,21 +10,30 @@ using MrGameEng.Graphics;
|
|||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Жизненный цикл растений: размножение (наследование по Менделю) и смерть от старости. Запускается
|
/// Жизненный цикл растений: размножение (наследование по Менделю), смерть от старости и от
|
||||||
/// не каждый кадр, а раз в <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку
|
/// температурного стресса (вымерзание/перегрев). Запускается не каждый кадр, а раз в
|
||||||
/// (счётчик плотности + «представитель» вида/генома), собирает умерших по возрасту, затем зрелые с
|
/// <see cref="IntervalDays"/> игрового дня. За тик: строит по-клеточную сетку (счётчик плотности +
|
||||||
/// вероятностью <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления
|
/// «представитель» вида/генома), копит/рассасывает температурный стресс по текущей температуре,
|
||||||
/// (зрелый сосед того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в
|
/// собирает умерших (старость или превышенный стресс), затем зрелые с вероятностью
|
||||||
/// случайную клетку радиуса, если это суша и не превышен лимит плотности. Структурные изменения
|
/// <c>elapsed/ReproduceInterval</c> сеют потомка — приоритет перекрёстного опыления (зрелый сосед
|
||||||
/// (создание/удаление сущностей) применяются после проходов по запросу — это безопасно.
|
/// того же вида в радиусе расселения), иначе самоопыление по гену. Семя падает в случайную клетку
|
||||||
|
/// радиуса, если это суша и не превышен лимит плотности. Структурные изменения (создание/удаление
|
||||||
|
/// сущностей) применяются после проходов по запросу — это безопасно.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlantLifecycleSystem : BaseSystem
|
public sealed class PlantLifecycleSystem : BaseSystem
|
||||||
{
|
{
|
||||||
private const float IntervalDays = 0.25f;
|
private const float IntervalDays = 0.25f;
|
||||||
|
|
||||||
|
/// <summary>Сколько игровых дней дормантности вне диапазона роста убивают растение.</summary>
|
||||||
|
private const float LethalStressDays = 6f;
|
||||||
|
|
||||||
|
/// <summary>Во сколько раз стресс рассасывается быстрее в пригодной температуре, чем копится вне.</summary>
|
||||||
|
private const float StressRecovery = 1.5f;
|
||||||
|
|
||||||
private readonly EntityStore _store;
|
private readonly EntityStore _store;
|
||||||
private readonly PlantSet _plants;
|
private readonly PlantSet _plants;
|
||||||
private readonly Calendar _calendar;
|
private readonly Calendar _calendar;
|
||||||
|
private readonly Climate _climate;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
private readonly int _width;
|
private readonly int _width;
|
||||||
private readonly int _height;
|
private readonly int _height;
|
||||||
@@ -32,7 +42,12 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
private readonly bool[] _cellLand;
|
private readonly bool[] _cellLand;
|
||||||
private readonly int _densityCap;
|
private readonly int _densityCap;
|
||||||
private readonly Random _rng;
|
private readonly Random _rng;
|
||||||
private readonly ArchetypeQuery<PlantGrowth, PlantOrganism, Transform2D> _query;
|
private readonly ArchetypeQuery<
|
||||||
|
PlantGrowth,
|
||||||
|
PlantOrganism,
|
||||||
|
Transform2D,
|
||||||
|
TemperatureStress
|
||||||
|
> _query;
|
||||||
|
|
||||||
private readonly int[] _count;
|
private readonly int[] _count;
|
||||||
private readonly CellRep[] _rep;
|
private readonly CellRep[] _rep;
|
||||||
@@ -45,6 +60,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
EntityStore store,
|
EntityStore store,
|
||||||
PlantSet plants,
|
PlantSet plants,
|
||||||
Calendar calendar,
|
Calendar calendar,
|
||||||
|
Climate climate,
|
||||||
GameClock clock,
|
GameClock clock,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -58,6 +74,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
_store = store;
|
_store = store;
|
||||||
_plants = plants;
|
_plants = plants;
|
||||||
_calendar = calendar;
|
_calendar = calendar;
|
||||||
|
_climate = climate;
|
||||||
_clock = clock;
|
_clock = clock;
|
||||||
_width = width;
|
_width = width;
|
||||||
_height = height;
|
_height = height;
|
||||||
@@ -66,7 +83,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
_cellLand = cellLand;
|
_cellLand = cellLand;
|
||||||
_densityCap = densityCap;
|
_densityCap = densityCap;
|
||||||
_rng = new Random(seed);
|
_rng = new Random(seed);
|
||||||
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D>();
|
_query = store.Query<PlantGrowth, PlantOrganism, Transform2D, TemperatureStress>();
|
||||||
_count = new int[width * height];
|
_count = new int[width * height];
|
||||||
_rep = new CellRep[width * height];
|
_rep = new CellRep[width * height];
|
||||||
}
|
}
|
||||||
@@ -82,13 +99,13 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
var elapsed = _accumulator;
|
var elapsed = _accumulator;
|
||||||
_accumulator = 0f;
|
_accumulator = 0f;
|
||||||
|
|
||||||
BuildGridAndCollect();
|
BuildGridAndCollect(elapsed);
|
||||||
Reproduce(elapsed);
|
Reproduce(elapsed);
|
||||||
Apply();
|
Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проход 1: сетка плотности + представители, сбор умерших и зрелых-кандидатов.
|
// Проход 1: сетка плотности + представители, температурный стресс, сбор умерших и зрелых-кандидатов.
|
||||||
private void BuildGridAndCollect()
|
private void BuildGridAndCollect(float elapsed)
|
||||||
{
|
{
|
||||||
Array.Clear(_count);
|
Array.Clear(_count);
|
||||||
for (var c = 0; c < _rep.Length; c++)
|
for (var c = 0; c < _rep.Length; c++)
|
||||||
@@ -100,11 +117,14 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
_candidates.Clear();
|
_candidates.Clear();
|
||||||
_births.Clear();
|
_births.Clear();
|
||||||
|
|
||||||
foreach (var (growths, organisms, transforms, entities) in _query.Chunks)
|
var temperature = _climate.Temperature; // глобальная — едина для всех растений в этот тик
|
||||||
|
|
||||||
|
foreach (var (growths, organisms, transforms, stresses, entities) in _query.Chunks)
|
||||||
{
|
{
|
||||||
var g = growths.Span;
|
var g = growths.Span;
|
||||||
var orgs = organisms.Span;
|
var orgs = organisms.Span;
|
||||||
var t = transforms.Span;
|
var t = transforms.Span;
|
||||||
|
var st = stresses.Span;
|
||||||
for (var i = 0; i < g.Length; i++)
|
for (var i = 0; i < g.Length; i++)
|
||||||
{
|
{
|
||||||
ref var grow = ref g[i];
|
ref var grow = ref g[i];
|
||||||
@@ -115,6 +135,23 @@ public sealed class PlantLifecycleSystem : BaseSystem
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Температурный стресс: вне диапазона роста (трапеция = 0) копится, внутри — рассасывается.
|
||||||
|
var (tMin, tLow, tHigh, tMax) = org.Traits.TemperatureBand();
|
||||||
|
ref var stress = ref st[i].Days;
|
||||||
|
if (Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax) <= 0f)
|
||||||
|
{
|
||||||
|
stress += elapsed;
|
||||||
|
if (stress >= LethalStressDays)
|
||||||
|
{
|
||||||
|
_deaths.Add(entities.EntityAt(i)); // вымерзло/перегрелось
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stress = MathF.Max(0f, stress - elapsed * StressRecovery);
|
||||||
|
}
|
||||||
|
|
||||||
var pos = t[i].Position;
|
var pos = t[i].Position;
|
||||||
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
var cx = Math.Clamp((int)(pos.X / _cellSize), 0, _width - 1);
|
||||||
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
var cy = Math.Clamp((int)(pos.Y / _cellSize), 0, _height - 1);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using MrGameEng.Genetics;
|
|||||||
namespace LittleSim.Sim;
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>genes.json</c>):
|
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>Defs/Genes/</c>):
|
||||||
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
|
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
|
||||||
/// Имена полей соответствуют именам признаков в эффектах генов.
|
/// Имена полей соответствуют именам признаков в эффектах генов.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -15,6 +15,8 @@ public struct PlantPhenotype
|
|||||||
public float LightTolerance;
|
public float LightTolerance;
|
||||||
public float OptimalTemperature;
|
public float OptimalTemperature;
|
||||||
public float TemperatureTolerance;
|
public float TemperatureTolerance;
|
||||||
|
public float ColdHardiness;
|
||||||
|
public float HeatHardiness;
|
||||||
public float OptimalFertility;
|
public float OptimalFertility;
|
||||||
public float FertilityTolerance;
|
public float FertilityTolerance;
|
||||||
public float Vigor;
|
public float Vigor;
|
||||||
@@ -33,6 +35,18 @@ public struct PlantPhenotype
|
|||||||
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
||||||
public bool IsVariant;
|
public bool IsVariant;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Четыре точки температурной трапеции роста (модель RimWorld): жёсткий минимум, нижний край плато,
|
||||||
|
/// верхний край плато, жёсткий максимум. Плато — оптимум ± толерантность; жёсткие края отстоят от
|
||||||
|
/// плато на морозо-/жаростойкость. Кормит <see cref="MrGameEng.AI.Suitability.Trapezoid"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly (float Min, float OptimalLow, float OptimalHigh, float Max) TemperatureBand()
|
||||||
|
{
|
||||||
|
var low = OptimalTemperature - TemperatureTolerance;
|
||||||
|
var high = OptimalTemperature + TemperatureTolerance;
|
||||||
|
return (low - ColdHardiness, low, high, high + HeatHardiness);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
|
||||||
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
|
||||||
{
|
{
|
||||||
@@ -43,6 +57,8 @@ public struct PlantPhenotype
|
|||||||
LightTolerance = T("lightTolerance"),
|
LightTolerance = T("lightTolerance"),
|
||||||
OptimalTemperature = T("optimalTemperature"),
|
OptimalTemperature = T("optimalTemperature"),
|
||||||
TemperatureTolerance = T("temperatureTolerance"),
|
TemperatureTolerance = T("temperatureTolerance"),
|
||||||
|
ColdHardiness = T("coldHardiness"),
|
||||||
|
HeatHardiness = T("heatHardiness"),
|
||||||
OptimalFertility = T("optimalFertility"),
|
OptimalFertility = T("optimalFertility"),
|
||||||
FertilityTolerance = T("fertilityTolerance"),
|
FertilityTolerance = T("fertilityTolerance"),
|
||||||
Vigor = T("vigor"),
|
Vigor = T("vigor"),
|
||||||
@@ -85,3 +101,15 @@ public struct Fruiting : IComponent
|
|||||||
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
||||||
public float RipeFruit;
|
public float RipeFruit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Накопленный температурный стресс растения в игровых днях. Пока температура вне диапазона роста
|
||||||
|
/// (трапеция <see cref="PlantPhenotype.TemperatureBand"/> даёт ноль — слишком холодно или жарко),
|
||||||
|
/// стресс копится; внутри диапазона — рассасывается. Превышение порога убивает растение
|
||||||
|
/// (вымерзание/перегрев). Управляется <c>PlantLifecycleSystem</c>.
|
||||||
|
/// </summary>
|
||||||
|
public struct TemperatureStress : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Накоплено дней дормантности вне диапазона роста; при превышении лимита — гибель.</summary>
|
||||||
|
public float Days;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Input;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Текущее выделение игрока (как в RimWorld): id выбранной сущности или -1. Общий источник истины
|
||||||
|
/// для системы пикинга (<see cref="SelectionSystem"/>) и панели осмотра (<c>InspectPanel</c>):
|
||||||
|
/// первая пишет, вторая читает и рисует рамку/инфо. Хранит id, а не <see cref="Entity"/>, чтобы
|
||||||
|
/// безопасно переживать удаление сущности (растение умерло) — валидность проверяется через
|
||||||
|
/// <c>EntityStore.TryGetEntityById</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Selection
|
||||||
|
{
|
||||||
|
/// <summary>Runtime-id выбранной сущности, или -1, если ничего не выбрано.</summary>
|
||||||
|
public int EntityId { get; private set; } = -1;
|
||||||
|
|
||||||
|
/// <summary>Есть ли активное выделение.</summary>
|
||||||
|
public bool HasSelection => EntityId >= 0;
|
||||||
|
|
||||||
|
/// <summary>Выбрать сущность по id.</summary>
|
||||||
|
public void Select(int entityId) => EntityId = entityId;
|
||||||
|
|
||||||
|
/// <summary>Снять выделение.</summary>
|
||||||
|
public void Clear() => EntityId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Пикинг мышью: ЛКМ выбирает верхнюю сущность под курсором (ближайшую по ограничивающей окружности
|
||||||
|
/// спрайта), клик по пустому месте или ПКМ снимает выделение. Молчит, пока ввод захвачен оверлеем
|
||||||
|
/// (консоль/инспектор) или активен <paramref name="blocked"/> (меню-пауза), и игнорирует клики над
|
||||||
|
/// UI-панелями (<paramref name="overUi"/>). Выбор работает и на паузе — мир можно осматривать стоя.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SelectionSystem(
|
||||||
|
EntityStore store,
|
||||||
|
InputManager input,
|
||||||
|
Renderer2D renderer,
|
||||||
|
Selection selection,
|
||||||
|
Func<bool> blocked,
|
||||||
|
Func<bool> overUi
|
||||||
|
) : BaseSystem
|
||||||
|
{
|
||||||
|
protected override void OnUpdateGroup()
|
||||||
|
{
|
||||||
|
if (blocked() || overUi())
|
||||||
|
{
|
||||||
|
return; // меню-пауза/консоль или клик пришёлся на виджет UI — это не пикинг мира
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.IsMousePressed(MouseButton.Right))
|
||||||
|
{
|
||||||
|
selection.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.IsMousePressed(MouseButton.Left))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouse = input.MousePosition;
|
||||||
|
|
||||||
|
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||||||
|
if (TryPick(world, out var id))
|
||||||
|
{
|
||||||
|
selection.Select(id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selection.Clear(); // клик по пустой земле снимает выделение
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ближайшее растение, чья ограничивающая окружность спрайта накрывает точку мира. Запрос
|
||||||
|
// ограничен PlantGrowth — так из пикинга выпадают спрайты рамки выделения и прочий не-контент.
|
||||||
|
private bool TryPick(Vector2 world, out int id)
|
||||||
|
{
|
||||||
|
id = -1;
|
||||||
|
var bestDistance = 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++)
|
||||||
|
{
|
||||||
|
if (s[i].Region is not { } region)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (center, radius) = CullingMath.SpriteBoundingCircle(
|
||||||
|
in t[i],
|
||||||
|
region,
|
||||||
|
s[i].Origin
|
||||||
|
);
|
||||||
|
var distance = Vector2.DistanceSquared(center, world);
|
||||||
|
if (distance <= radius * radius && distance < bestDistance)
|
||||||
|
{
|
||||||
|
bestDistance = distance;
|
||||||
|
id = entities.EntityAt(i).Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Sim;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Рамка выделения в стиле RimWorld: четыре угловые скобки вокруг выбранного объекта. Реализована
|
||||||
|
/// как четыре спрайт-сущности на слое-оверлее (<see cref="Scenes.GameLayers.Selection"/>) — их рисует
|
||||||
|
/// штатный спрайт-рендер, а одна и та же текстура уголка (<c>ui/overlays/selectionbracket</c>)
|
||||||
|
/// зеркалится во все четыре угла флагами <see cref="SpriteFlip"/> (батчер при флипе меняет только UV,
|
||||||
|
/// квадрат остаётся на месте). Скобки не имеют <c>PlantGrowth</c>, поэтому не попадают под пикинг и
|
||||||
|
/// прочие запросы растений. <see cref="Show"/> двигает их к объекту, <see cref="Hide"/> прячет.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SelectionBrackets
|
||||||
|
{
|
||||||
|
// По уголку на каждый угол: исходный арт лежит в левом-нижнем, флип разносит его по остальным.
|
||||||
|
private static readonly SpriteFlip[] Corners =
|
||||||
|
[
|
||||||
|
SpriteFlip.None,
|
||||||
|
SpriteFlip.X,
|
||||||
|
SpriteFlip.Y,
|
||||||
|
SpriteFlip.X | SpriteFlip.Y,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly Color Tint = new(150, 210, 255);
|
||||||
|
|
||||||
|
private readonly Entity[] _brackets = new Entity[4];
|
||||||
|
private readonly Texture2DRegion _region;
|
||||||
|
|
||||||
|
/// <summary>Создаёт четыре скрытые скобки на слое <paramref name="layer"/> из текстуры <paramref name="region"/>.</summary>
|
||||||
|
public SelectionBrackets(EntityStore store, Texture2DRegion region, LayerId layer)
|
||||||
|
{
|
||||||
|
_region = region;
|
||||||
|
var origin = new Vector2(region.Width / 2f, region.Height / 2f);
|
||||||
|
for (var i = 0; i < _brackets.Length; i++)
|
||||||
|
{
|
||||||
|
_brackets[i] = store.CreateEntity(
|
||||||
|
Transform2D.At(Vector2.Zero),
|
||||||
|
new Sprite
|
||||||
|
{
|
||||||
|
Region = null, // скрыта, пока ничего не выбрано
|
||||||
|
Color = Tint,
|
||||||
|
Origin = origin, // центр текстуры → квад центрируется на позиции
|
||||||
|
Layer = layer,
|
||||||
|
Flip = Corners[i],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ставит рамку вокруг точки <paramref name="center"/> с полуразмером <paramref name="radius"/>.</summary>
|
||||||
|
public void Show(Vector2 center, float radius)
|
||||||
|
{
|
||||||
|
// Квад чуть больше объекта — скобки садятся снаружи (как в RimWorld); арт-уголок ~⅓ ширины.
|
||||||
|
var scale = new Vector2(radius * 2.2f / _region.Width);
|
||||||
|
for (var i = 0; i < _brackets.Length; i++)
|
||||||
|
{
|
||||||
|
ref var transform = ref _brackets[i].GetComponent<Transform2D>();
|
||||||
|
ref var sprite = ref _brackets[i].GetComponent<Sprite>();
|
||||||
|
transform.Position = center;
|
||||||
|
transform.Scale = scale;
|
||||||
|
sprite.Region = _region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Прячет рамку (сбрасывает регион — рендер пропускает спрайты без текстуры).</summary>
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _brackets.Length; i++)
|
||||||
|
{
|
||||||
|
_brackets[i].GetComponent<Sprite>().Region = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -155,11 +155,15 @@ public sealed class GodCameraSystem(
|
|||||||
if (newZoom != oldZoom)
|
if (newZoom != oldZoom)
|
||||||
{
|
{
|
||||||
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
|
// Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
|
||||||
|
// Якорь и базовая позиция берутся из одного кадра камеры: WorldCenter — это позиция
|
||||||
|
// ПОСЛЕ клампа к границам (то, что реально нарисовано), а не сырой camera.Position —
|
||||||
|
// иначе у краёв карты/при широком обзоре зум «уезжает» от курсора.
|
||||||
var anchor = renderer.ScreenToWorld(
|
var anchor = renderer.ScreenToWorld(
|
||||||
new Vector2(input.MousePosition.X, input.MousePosition.Y)
|
new Vector2(input.MousePosition.X, input.MousePosition.Y)
|
||||||
);
|
);
|
||||||
|
var effective = renderer.Camera.WorldCenter;
|
||||||
camera.Zoom = newZoom;
|
camera.Zoom = newZoom;
|
||||||
camera.Position = anchor - (anchor - camera.Position) * (oldZoom / newZoom);
|
camera.Position = anchor - (anchor - effective) * (oldZoom / newZoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using LittleSim.Content;
|
||||||
|
using LittleSim.Sim;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Mods;
|
||||||
|
using Myra.Graphics2D;
|
||||||
|
using Myra.Graphics2D.Brushes;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Панель осмотра выбранной сущности (инфо-карточка в стиле RimWorld): угловая рамка вокруг объекта
|
||||||
|
/// (<see cref="SelectionBrackets"/>) и боковая панель в левом нижнем углу с вкладками — «Обзор»,
|
||||||
|
/// «Гены», «Продукты». Источник выделения — <see cref="Selection"/>; панель только читает компоненты
|
||||||
|
/// и обновляется каждый кадр (<see cref="Refresh"/>), оставаясь живой даже на паузе. Все строки идут
|
||||||
|
/// через <see cref="LanguageManager"/> (ключи <c>inspect.*</c>).
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class InspectPanel
|
||||||
|
{
|
||||||
|
private enum Tab
|
||||||
|
{
|
||||||
|
Overview,
|
||||||
|
Genes,
|
||||||
|
Products,
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly EntityStore _store;
|
||||||
|
private readonly PlantSet _plants;
|
||||||
|
private readonly GameContent _content;
|
||||||
|
private readonly Climate _climate;
|
||||||
|
private readonly Selection _selection;
|
||||||
|
private readonly SelectionBrackets _brackets;
|
||||||
|
|
||||||
|
private readonly VerticalStackPanel _panel;
|
||||||
|
private readonly Label _title;
|
||||||
|
private readonly Label _body;
|
||||||
|
private readonly List<(Tab Tab, TextButton Button)> _tabs = [];
|
||||||
|
private Tab _tab = Tab.Overview;
|
||||||
|
|
||||||
|
public InspectPanel(
|
||||||
|
EntityStore store,
|
||||||
|
PlantSet plants,
|
||||||
|
GameContent content,
|
||||||
|
Climate climate,
|
||||||
|
Selection selection,
|
||||||
|
SelectionBrackets brackets
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_store = store;
|
||||||
|
_plants = plants;
|
||||||
|
_content = content;
|
||||||
|
_climate = climate;
|
||||||
|
_selection = selection;
|
||||||
|
_brackets = brackets;
|
||||||
|
|
||||||
|
_title = new Label { TextColor = Ui.Accent, Wrap = true };
|
||||||
|
_body = new Label { TextColor = new Color(210, 216, 226), Wrap = true };
|
||||||
|
|
||||||
|
var tabBar = new HorizontalStackPanel { Spacing = 4 };
|
||||||
|
AddTab(tabBar, Tab.Overview, "inspect.tab.overview");
|
||||||
|
AddTab(tabBar, Tab.Genes, "inspect.tab.genes");
|
||||||
|
AddTab(tabBar, Tab.Products, "inspect.tab.products");
|
||||||
|
|
||||||
|
_panel = new VerticalStackPanel
|
||||||
|
{
|
||||||
|
Spacing = 6,
|
||||||
|
Padding = new Thickness(14),
|
||||||
|
Width = 380,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = VerticalAlignment.Bottom,
|
||||||
|
Margin = new Thickness(12, 0, 0, 14),
|
||||||
|
Background = new SolidBrush(new Color(10, 13, 18, 240)),
|
||||||
|
Border = new SolidBrush(new Color(96, 134, 168)),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
Visible = false,
|
||||||
|
};
|
||||||
|
_panel.Widgets.Add(_title);
|
||||||
|
_panel.Widgets.Add(tabBar);
|
||||||
|
_panel.Widgets.Add(new HorizontalSeparator());
|
||||||
|
_panel.Widgets.Add(_body);
|
||||||
|
_panel.Widgets.Add(
|
||||||
|
new Label
|
||||||
|
{
|
||||||
|
Text = _content.Languages.Get("inspect.hint"),
|
||||||
|
TextColor = Ui.Muted,
|
||||||
|
Wrap = true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Боковая инфо-панель — добавляется в корень экрана.</summary>
|
||||||
|
public VerticalStackPanel Panel => _panel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подтягивает выделение каждый кадр: проверяет, жива ли сущность (иначе снимает выбор), двигает
|
||||||
|
/// рамку к объекту и пересобирает текст активной вкладки. Не-растения панель не показывает.
|
||||||
|
/// </summary>
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!_selection.HasSelection
|
||||||
|
|| !_store.TryGetEntityById(_selection.EntityId, out var entity)
|
||||||
|
|| entity.IsNull
|
||||||
|
|| !entity.HasComponent<PlantGrowth>()
|
||||||
|
|| !entity.HasComponent<PlantOrganism>()
|
||||||
|
|| !entity.HasComponent<Transform2D>()
|
||||||
|
|| !entity.HasComponent<Sprite>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (_selection.HasSelection)
|
||||||
|
{
|
||||||
|
_selection.Clear(); // сущность исчезла (растение погибло)
|
||||||
|
}
|
||||||
|
|
||||||
|
_panel.Visible = false;
|
||||||
|
_brackets.Hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transform = entity.GetComponent<Transform2D>();
|
||||||
|
var sprite = entity.GetComponent<Sprite>();
|
||||||
|
if (sprite.Region is { } region)
|
||||||
|
{
|
||||||
|
var (center, radius) = CullingMath.SpriteBoundingCircle(
|
||||||
|
in transform,
|
||||||
|
region,
|
||||||
|
sprite.Origin
|
||||||
|
);
|
||||||
|
_brackets.Show(center, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateText(entity);
|
||||||
|
_panel.Visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTab(HorizontalStackPanel bar, Tab tab, string labelKey)
|
||||||
|
{
|
||||||
|
var button = new TextButton { Text = _content.Languages.Get(labelKey) };
|
||||||
|
button.Click += (_, _) => _tab = tab;
|
||||||
|
bar.Widgets.Add(button);
|
||||||
|
_tabs.Add((tab, button));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateText(Entity entity)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
ref readonly var grow = ref entity.GetComponent<PlantGrowth>();
|
||||||
|
ref readonly var org = ref entity.GetComponent<PlantOrganism>();
|
||||||
|
var species = _plants[grow.Species];
|
||||||
|
var def = species.Def;
|
||||||
|
var traits = org.Traits;
|
||||||
|
|
||||||
|
_title.Text = languages.Get(def.Label) + (traits.IsVariant ? " *" : "");
|
||||||
|
foreach (var (tab, button) in _tabs)
|
||||||
|
{
|
||||||
|
button.TextColor = tab == _tab ? Ui.Accent : Ui.Muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = new StringBuilder();
|
||||||
|
switch (_tab)
|
||||||
|
{
|
||||||
|
case Tab.Overview:
|
||||||
|
BuildOverview(text, entity, grow, traits, species);
|
||||||
|
break;
|
||||||
|
case Tab.Genes:
|
||||||
|
BuildGenes(text, traits);
|
||||||
|
break;
|
||||||
|
case Tab.Products:
|
||||||
|
BuildProducts(text, entity, def, traits);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_body.Text = text.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildOverview(
|
||||||
|
StringBuilder text,
|
||||||
|
Entity entity,
|
||||||
|
in PlantGrowth grow,
|
||||||
|
in PlantPhenotype traits,
|
||||||
|
PlantSet.Species species
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
var lastStage = species.Stages.Length - 1;
|
||||||
|
var mature = grow.Stage >= lastStage;
|
||||||
|
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.stage",
|
||||||
|
StageLabel(species.Def, grow.Stage),
|
||||||
|
grow.Stage + 1,
|
||||||
|
species.Stages.Length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (mature)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.mature"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var from = species.Stages[grow.Stage].EnterDay;
|
||||||
|
var to = species.Stages[grow.Stage + 1].EnterDay;
|
||||||
|
var pct = to > from ? (grow.AgeDays - from) / (to - from) * 100f : 0f;
|
||||||
|
text.AppendLine(languages.Format("inspect.growing", Math.Clamp(pct, 0f, 100f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine(languages.Format("inspect.age", grow.AgeDays, traits.Lifespan));
|
||||||
|
|
||||||
|
// Состояние по температуре (рост/покой) и накопленный стресс.
|
||||||
|
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||||||
|
var temperature = _climate.Temperature;
|
||||||
|
var suit = Suitability.Trapezoid(temperature, tMin, tLow, tHigh, tMax);
|
||||||
|
var state =
|
||||||
|
suit <= 0f
|
||||||
|
? languages.Get(
|
||||||
|
temperature <= tMin ? "inspect.state.dormantcold" : "inspect.state.dormanthot"
|
||||||
|
)
|
||||||
|
: languages.Get(mature ? "inspect.state.mature" : "inspect.state.growing");
|
||||||
|
text.AppendLine(languages.Format("inspect.state", state));
|
||||||
|
if (entity.TryGetComponent<TemperatureStress>(out var stress) && stress.Days > 0.05f)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Format("inspect.stress", stress.Days));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine(languages.Format("inspect.temp", tMin, tMax, tLow, tHigh));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildGenes(StringBuilder text, in PlantPhenotype traits)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
text.AppendLine(languages.Format("inspect.gene.vigor", traits.Vigor, traits.Lifespan));
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.env", traits.OptimalLight, traits.OptimalFertility)
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.repro", traits.DispersalRange, traits.ReproduceInterval)
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.gene.repro2", traits.SelfPollination, traits.MutationRate)
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.gene.hardy",
|
||||||
|
traits.ColdHardiness,
|
||||||
|
traits.HeatHardiness,
|
||||||
|
traits.LeafHue
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (traits.IsVariant)
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.gene.variant"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildProducts(
|
||||||
|
StringBuilder text,
|
||||||
|
Entity entity,
|
||||||
|
PlantDef def,
|
||||||
|
in PlantPhenotype traits
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var languages = _content.Languages;
|
||||||
|
if (def.HarvestProduct is { } harvest)
|
||||||
|
{
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format("inspect.harvest", ProductLabel(harvest), traits.HarvestAmount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||||||
|
{
|
||||||
|
var ripe = entity.TryGetComponent<Fruiting>(out var fruiting) ? fruiting.RipeFruit : 0f;
|
||||||
|
var season = languages.Get(
|
||||||
|
"season." + ((Season)traits.FruitSeason).ToString().ToLowerInvariant()
|
||||||
|
);
|
||||||
|
text.AppendLine(
|
||||||
|
languages.Format(
|
||||||
|
"inspect.fruit",
|
||||||
|
ProductLabel(fruit),
|
||||||
|
traits.FruitYield,
|
||||||
|
season,
|
||||||
|
ripe
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text.AppendLine(languages.Get("inspect.barren"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string StageLabel(PlantDef def, int stage) =>
|
||||||
|
stage < def.Stages.Count && def.Stages[stage].Label is { } key
|
||||||
|
? _content.Languages.Get(key)
|
||||||
|
: "—";
|
||||||
|
|
||||||
|
private string ProductLabel(string productDefName) =>
|
||||||
|
_content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||||
|
? _content.Languages.Get(product.Label)
|
||||||
|
: productDefName;
|
||||||
|
}
|
||||||
@@ -82,6 +82,19 @@ internal static class SettingsPanel
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Масштаб интерфейса — сегменты пресетов.
|
||||||
|
var scales = new List<float> { 0.75f, 1f, 1.25f, 1.5f };
|
||||||
|
column.Widgets.Add(
|
||||||
|
Segment(
|
||||||
|
lang.Get("settings.uiscale"),
|
||||||
|
scales,
|
||||||
|
s => $"{(int)MathF.Round(s * 100)}%",
|
||||||
|
() => settings.UiScale,
|
||||||
|
s => settings.UiScale = s,
|
||||||
|
refreshers
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Громкость — кнопки −/+ с подписью процента.
|
// Громкость — кнопки −/+ с подписью процента.
|
||||||
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
|
column.Widgets.Add(Volume(lang.Get("settings.volume"), settings, refreshers));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using LittleSim.App;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.UI;
|
||||||
|
using Myra.Graphics2D.UI;
|
||||||
|
|
||||||
|
namespace LittleSim.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Масштаб интерфейса для Myra-десктопа. Scale увеличивает всю отрисовку, а <c>BoundsFetcher</c>
|
||||||
|
/// отдаёт логический размер «экран ÷ масштаб», поэтому раскладка перетекает (а не обрезается):
|
||||||
|
/// при масштабе 1.25 виджеты крупнее, но углы/центры остаются прижаты к краям экрана. Ввод Myra
|
||||||
|
/// трансформируется тем же преобразованием, так что попадания/<c>IsMouseOverGUI</c> остаются верными.
|
||||||
|
/// </summary>
|
||||||
|
internal static class UiScaling
|
||||||
|
{
|
||||||
|
/// <summary>Применяет масштаб <paramref name="scale"/> (клампится в разумные пределы) к десктопу.</summary>
|
||||||
|
public static void Apply(EngineContext context, Desktop desktop, float scale)
|
||||||
|
{
|
||||||
|
scale = System.Math.Clamp(scale, 0.75f, 2f);
|
||||||
|
desktop.TransformOrigin = Vector2.Zero; // масштабируем от левого-верхнего угла — края на месте
|
||||||
|
desktop.Scale = new Vector2(scale);
|
||||||
|
desktop.BoundsFetcher = () =>
|
||||||
|
{
|
||||||
|
var viewport = context.GetGraphicsDevice().Viewport;
|
||||||
|
return new Rectangle(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
(int)MathF.Round(viewport.Width / scale),
|
||||||
|
(int)MathF.Round(viewport.Height / scale)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Расширение сцены: создаёт Myra-десктоп и сразу применяет масштаб UI из настроек.</summary>
|
||||||
|
internal static class ScaledUiExtensions
|
||||||
|
{
|
||||||
|
/// <summary>Как <c>UseUI</c>, но десктоп уже масштабирован по <see cref="GameSettings.UiScale"/>.</summary>
|
||||||
|
public static Desktop UseScaledUI(this Scene scene)
|
||||||
|
{
|
||||||
|
var desktop = scene.UseUI();
|
||||||
|
UiScaling.Apply(scene.Context, desktop, GameSettingsStore.Load().UiScale);
|
||||||
|
return desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user