G4: gene-driven content — fruiting, harvest products, leaf color
New plant content, all driven by genes through the trait layer (no engine change — the genetics machinery already supports it). - ProductDef + products.json (wood, grass, berries, acorn) — what plants yield; localized labels (ru/en). - New numeric genes (genes.json): GeneFruitYield, GeneFruitSeason, GeneHarvestAmount, GeneLeafHue, each declaring its trait via formula. GenomeDef carries the per-species bases (PlantSet maps them into the species template); PlantDef gains harvestProduct / fruitProduct. - PlantPhenotype gains FruitYield/FruitSeason/HarvestAmount/LeafHue. PlantFactory tints the sprite from the leaf-hue gene (combined with the morph variant). Plants carry a Fruiting component; PlantFruitingSystem ripens fruit on mature plants during their gene-chosen season and drops it off-season. - plants.json: trees give wood + acorns (autumn), bushes berries (summer), grass gives grass — amounts/season/hue from genes. - 'plant <species> [seed]' console command samples a species genome and prints its gene-driven traits and products. Build clean; def JSON validated; boot smoke loads the new content. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9c2c2d7fd0
commit
7fc4bcd301
@@ -45,6 +45,20 @@
|
||||
"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" } },
|
||||
|
||||
// Дискретный ген морфы: вариант 0 доминирует, 1 (рецессивный) виден только в гомозиготе.
|
||||
{ "defName": "GeneMorph", "kind": "Discrete", "label": "gene.morph",
|
||||
"variants": 2, "variantWeights": [0.82, 0.18], "mutationChance": 0.05, "tags": ["morphology"],
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"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 },
|
||||
"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" },
|
||||
@@ -17,20 +19,24 @@
|
||||
{ "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 },
|
||||
"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 },
|
||||
"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" }
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "Product",
|
||||
// Продукты сбора/плодоношения растений. На них ссылаются PlantDef.harvestProduct / fruitProduct;
|
||||
// количество задаётся генами (harvestAmount / fruitYield).
|
||||
"defs": [
|
||||
{ "defName": "ProductWood", "label": "product.wood", "kind": "material" },
|
||||
{ "defName": "ProductGrass", "label": "product.grass", "kind": "material" },
|
||||
{ "defName": "ProductBerry", "label": "product.berry", "kind": "food" },
|
||||
{ "defName": "ProductAcorn", "label": "product.acorn", "kind": "food" }
|
||||
]
|
||||
}
|
||||
@@ -78,6 +78,11 @@
|
||||
"plant.stage.sapling": "sapling",
|
||||
"plant.stage.mature": "mature",
|
||||
|
||||
"product.wood": "wood",
|
||||
"product.grass": "grass",
|
||||
"product.berry": "berries",
|
||||
"product.acorn": "acorn",
|
||||
|
||||
"pawn.being": "being",
|
||||
"pawn.bear": "bear",
|
||||
"pawn.deer": "deer",
|
||||
|
||||
@@ -78,6 +78,11 @@
|
||||
"plant.stage.sapling": "саженец",
|
||||
"plant.stage.mature": "взрослое",
|
||||
|
||||
"product.wood": "древесина",
|
||||
"product.grass": "трава",
|
||||
"product.berry": "ягоды",
|
||||
"product.acorn": "жёлудь",
|
||||
|
||||
"pawn.being": "житель",
|
||||
"pawn.bear": "медведь",
|
||||
"pawn.deer": "олень",
|
||||
|
||||
@@ -58,6 +58,7 @@ public sealed class GameContent
|
||||
var defs = new DefDatabase();
|
||||
defs.RegisterType<TerrainDef>("Terrain");
|
||||
defs.RegisterType<GeneDef>("Gene");
|
||||
defs.RegisterType<ProductDef>("Product");
|
||||
defs.RegisterType<PlantDef>("Plant");
|
||||
defs.RegisterType<PawnDef>("Pawn");
|
||||
defs.RegisterType<WorldPresetDef>("WorldPreset");
|
||||
|
||||
@@ -66,6 +66,17 @@ public sealed class PlantGrowthStage
|
||||
public string? Label { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Продукт (Defs/products.json): что растение даёт при сборе (древесина, трава) или плодоношении
|
||||
/// (ягоды, жёлудь). Источник истины — деф; растения ссылаются на него по имени, а количество
|
||||
/// определяют гены. <see cref="Def.Label"/> — ключ локализации (product.*).
|
||||
/// </summary>
|
||||
public sealed class ProductDef : Def
|
||||
{
|
||||
/// <summary>Категория продукта (например "material" или "food") — для будущей экономики/инвентаря.</summary>
|
||||
public string Kind { get; init; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Базовый геном вида (вложенный объект <see cref="PlantDef.Genome"/>): оптимумы и толерантности по
|
||||
/// факторам среды + базовая бодрость роста. Особь при спавне получает аллели рядом с этими базами
|
||||
@@ -114,6 +125,18 @@ public sealed class GenomeDef
|
||||
|
||||
/// <summary>Доля разброса аллелей вокруг базы при генерации особи.</summary>
|
||||
public float Spread { get; init; } = 0.08f;
|
||||
|
||||
/// <summary>Плодовитость: сколько плодов несёт зрелое растение в сезон (0 — не плодоносит).</summary>
|
||||
public float FruitYield { get; init; }
|
||||
|
||||
/// <summary>Сезон плодоношения (0 — весна, 1 — лето, 2 — осень, 3 — зима).</summary>
|
||||
public float FruitSeason { get; init; } = 1f;
|
||||
|
||||
/// <summary>Сколько продукта даёт сбор растения (масштаб <see cref="PlantDef.HarvestProduct"/>).</summary>
|
||||
public float HarvestAmount { get; init; } = 1f;
|
||||
|
||||
/// <summary>Оттенок листвы (0..1) — сдвиг тинта спрайта; ген цвета.</summary>
|
||||
public float LeafHue { get; init; } = 0.33f;
|
||||
}
|
||||
|
||||
/// <summary>Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста, геном.</summary>
|
||||
@@ -133,6 +156,12 @@ public sealed class PlantDef : Def
|
||||
|
||||
/// <summary>Базовый геном вида: оптимумы/толерантности по среде и бодрость роста.</summary>
|
||||
public GenomeDef Genome { get; init; } = new();
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого при сборе растения (дерево/трава); null — несборное.</summary>
|
||||
public string? HarvestProduct { get; init; }
|
||||
|
||||
/// <summary>Имя <see cref="ProductDef"/>, выдаваемого плодами (ягоды/жёлудь); null — не плодоносит.</summary>
|
||||
public string? FruitProduct { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -111,6 +111,11 @@ public sealed class PlantSet
|
||||
Numeric("GeneReproduceInterval", g.ReproduceInterval),
|
||||
Numeric("GeneSelfPollination", g.SelfPollination),
|
||||
Numeric("GeneMutationRate", g.MutationRate),
|
||||
// Контент (G4): плодоношение, добыча, цвет.
|
||||
Numeric("GeneFruitYield", g.FruitYield),
|
||||
new GenomeTemplate.Entry(Gene("GeneFruitSeason"), g.FruitSeason, 0f), // сезон фиксирован по виду
|
||||
Numeric("GeneHarvestAmount", g.HarvestAmount),
|
||||
new GenomeTemplate.Entry(Gene("GeneLeafHue"), g.LeafHue, 0.04f),
|
||||
new GenomeTemplate.Entry(
|
||||
Gene("GeneMorph"),
|
||||
0f,
|
||||
|
||||
@@ -162,6 +162,7 @@ public sealed class WorldScene : Scene
|
||||
);
|
||||
|
||||
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
|
||||
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
||||
UpdateSystems.Add(
|
||||
new PlantLifecycleSystem(
|
||||
Store,
|
||||
@@ -502,6 +503,11 @@ public sealed class WorldScene : Scene
|
||||
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
|
||||
(c, args) => RunGeneDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"plant",
|
||||
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
||||
(c, args) => RunPlantDemo(c, content, args)
|
||||
);
|
||||
console.Register(
|
||||
"menu",
|
||||
"menu — return to the main menu",
|
||||
@@ -550,4 +556,62 @@ public sealed class WorldScene : Scene
|
||||
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
|
||||
);
|
||||
}
|
||||
|
||||
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
|
||||
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
|
||||
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
|
||||
{
|
||||
var species = content.Defs.NamesOf("Plant");
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
|
||||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
|
||||
{
|
||||
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _plants.IndexOf(def);
|
||||
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||||
var genome = _plants[index].Template.Generate(new Random(seed));
|
||||
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
|
||||
|
||||
console.WriteLine(
|
||||
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
||||
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
||||
);
|
||||
|
||||
if (def.HarvestProduct is { } harvest)
|
||||
{
|
||||
console.WriteLine(
|
||||
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
|
||||
);
|
||||
}
|
||||
|
||||
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||||
{
|
||||
var season = content.Languages.Get(
|
||||
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
|
||||
);
|
||||
console.WriteLine(
|
||||
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.WriteLine(" barren (no fruit)");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProductLabel(GameContent content, string productDefName) =>
|
||||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||||
? content.Languages.Get(product.Label)
|
||||
: productDefName;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ public static class PlantFactory
|
||||
// Тинт рецессивной морфы (домножается на текстуру) — делает вариант визуально отличимым.
|
||||
private static readonly Color VariantTint = new(214, 150, 176);
|
||||
|
||||
// Якоря оттенка листвы: тёплый жёлто-зелёный → холодный сине-зелёный (по гену leafHue).
|
||||
private static readonly Color LeafWarm = new(170, 200, 90);
|
||||
private static readonly Color LeafCool = new(90, 170, 150);
|
||||
|
||||
/// <summary>Создаёт растение вида <paramref name="species"/> с заданным возрастом и геномом.</summary>
|
||||
public static Entity Create(
|
||||
EntityStore store,
|
||||
@@ -36,7 +40,7 @@ public static class PlantFactory
|
||||
|
||||
var sprite = new Sprite(resolved.Region, GameLayers.Beings);
|
||||
sprite.CenterOrigin();
|
||||
sprite.Color = traits.IsVariant ? VariantTint : Color.White;
|
||||
sprite.Color = Tint(traits);
|
||||
|
||||
return store.CreateEntity(
|
||||
new Transform2D(
|
||||
@@ -51,7 +55,17 @@ public static class PlantFactory
|
||||
AgeDays = ageDays,
|
||||
CellFertility = cellFertility,
|
||||
},
|
||||
new PlantOrganism { Genome = genome, Traits = traits }
|
||||
new PlantOrganism { Genome = genome, Traits = traits },
|
||||
new Fruiting()
|
||||
);
|
||||
}
|
||||
|
||||
// Цвет растения из генов: лёгкий оттенок листвы (ген leafHue) поверх текстуры; рецессивная морфа
|
||||
// дополнительно сдвигает тинт в свою сторону.
|
||||
private static Color Tint(in PlantPhenotype traits)
|
||||
{
|
||||
var leaf = Color.Lerp(LeafWarm, LeafCool, Math.Clamp(traits.LeafHue, 0f, 1f));
|
||||
var tint = Color.Lerp(Color.White, leaf, 0.35f);
|
||||
return traits.IsVariant ? Color.Lerp(tint, VariantTint, 0.6f) : tint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using LittleSim.Content;
|
||||
using MrGameEng.AI;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace LittleSim.Sim;
|
||||
|
||||
/// <summary>
|
||||
/// Плодоношение (фаза G4): у зрелого растения с признаком <see cref="PlantPhenotype.FruitYield"/> > 0
|
||||
/// в его генетический сезон (<see cref="PlantPhenotype.FruitSeason"/>) копятся зрелые плоды до потолка
|
||||
/// <see cref="PlantPhenotype.FruitYield"/>; вне сезона плоды опадают. Полностью управляется генами через
|
||||
/// признаки — система лишь применяет их к компоненту <see cref="Fruiting"/>. Незрелые/бесплодные
|
||||
/// растения остаются с нулём.
|
||||
/// </summary>
|
||||
public sealed class PlantFruitingSystem(PlantSet plants, Calendar calendar, Climate climate)
|
||||
: QuerySystem<PlantGrowth, PlantOrganism, Fruiting>
|
||||
{
|
||||
// За сколько игровых дней набирается/опадает полный урожай.
|
||||
private const float RipenDays = 10f;
|
||||
private const float DropDays = 20f;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
|
||||
if (deltaDays <= 0f)
|
||||
{
|
||||
return; // пауза
|
||||
}
|
||||
|
||||
var season = (int)climate.Season;
|
||||
|
||||
foreach (var (growths, organisms, fruits, _) in Query.Chunks)
|
||||
{
|
||||
var g = growths.Span;
|
||||
var orgs = organisms.Span;
|
||||
var f = fruits.Span;
|
||||
for (var i = 0; i < g.Length; i++)
|
||||
{
|
||||
ref var traits = ref orgs[i].Traits;
|
||||
var mature = g[i].Stage >= plants[g[i].Species].Stages.Length - 1;
|
||||
if (!mature || traits.FruitYield < 1f)
|
||||
{
|
||||
f[i].RipeFruit = 0f; // незрелое или бесплодное — плодов нет
|
||||
continue;
|
||||
}
|
||||
|
||||
ref var ripe = ref f[i].RipeFruit;
|
||||
if (season == traits.FruitSeason)
|
||||
{
|
||||
ripe = MathF.Min(
|
||||
traits.FruitYield,
|
||||
ripe + deltaDays * (traits.FruitYield / RipenDays)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
ripe = MathF.Max(0f, ripe - deltaDays * (traits.FruitYield / DropDays));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ public struct PlantPhenotype
|
||||
public float SelfPollination;
|
||||
public float MutationRate;
|
||||
|
||||
// --- Контент (G4): плодоношение, добыча, цвет ---
|
||||
public float FruitYield;
|
||||
public int FruitSeason;
|
||||
public float HarvestAmount;
|
||||
public float LeafHue;
|
||||
|
||||
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
|
||||
public bool IsVariant;
|
||||
|
||||
@@ -45,6 +51,10 @@ public struct PlantPhenotype
|
||||
ReproduceInterval = T("reproduceInterval"),
|
||||
SelfPollination = T("selfPollination"),
|
||||
MutationRate = T("mutationRate"),
|
||||
FruitYield = T("fruitYield"),
|
||||
FruitSeason = (int)MathF.Round(Math.Clamp(T("fruitSeason"), 0f, 3f)),
|
||||
HarvestAmount = T("harvestAmount"),
|
||||
LeafHue = T("leafHue"),
|
||||
IsVariant = T("variant") >= 0.5f,
|
||||
};
|
||||
}
|
||||
@@ -64,3 +74,14 @@ public struct PlantOrganism : IComponent
|
||||
/// <summary>Выраженные признаки — то, что читают системы роста/жизненного цикла.</summary>
|
||||
public PlantPhenotype Traits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Плодоношение растения: сколько зрелых плодов сейчас на нём. Копится у зрелого растения в его
|
||||
/// сезон плодоношения (признак <see cref="PlantPhenotype.FruitSeason"/>) до потолка
|
||||
/// <see cref="PlantPhenotype.FruitYield"/>, вне сезона опадает. Управляется <c>PlantFruitingSystem</c>.
|
||||
/// </summary>
|
||||
public struct Fruiting : IComponent
|
||||
{
|
||||
/// <summary>Накоплено зрелых плодов (0..FruitYield).</summary>
|
||||
public float RipeFruit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user