- AnimalNeedsSystem (per-frame, every animal): drop the string-keyed genome lookup and Mass.OfAnimal call; Kleiber factor is now (size/refBody)^2.25 using a precomputed AnimalSet.Species.RefBodySize. No dict probe per animal per frame. - AnimalActionSystem: fetch the Skills component ONCE per acting animal instead of twice per action (SkillFactor + GrantXp took in Skills now). - RecomputeCapacities: apply the senescence scale inside CapacityCalc.Compute instead of a second pass that allocated a List<string> of keys every recompute (per animal per health tick, and per injury in combat). - AnimalFactory.SkillSeed: iterate genome.Alleles (IReadOnlyDictionary, no copy) instead of ToDictionary() which allocated a dict per spawn. Behaviour-preserving (same numbers). Build + --check-content clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
304 lines
13 KiB
C#
304 lines
13 KiB
C#
using Friflo.Engine.ECS;
|
||
using LittleSim.Content;
|
||
using LittleSim.Scenes;
|
||
using Microsoft.Xna.Framework;
|
||
using MrGameEng.Genetics;
|
||
using MrGameEng.Graphics;
|
||
|
||
namespace LittleSim.Sim;
|
||
|
||
/// <summary>
|
||
/// Единая точка создания сущности-животного: спрайт вида, признаки из генома формулами генов, тинт по
|
||
/// оттенку меха и компонент-организм. Переиспользуется начальным спавном, размножением и загрузкой —
|
||
/// чтобы все животные собирались одинаково. Зеркало <see cref="PlantFactory"/>.
|
||
/// </summary>
|
||
public static class AnimalFactory
|
||
{
|
||
// Якоря оттенка меха: тёплый рыжевато-песочный → холодный серо-голубой (по гену furColor).
|
||
private static readonly Color FurWarm = new(255, 226, 188);
|
||
private static readonly Color FurCool = new(206, 222, 235);
|
||
|
||
/// <summary>Создаёт животное вида <paramref name="species"/> заданного возраста, генома и поколения.</summary>
|
||
public static Entity Create(
|
||
EntityStore store,
|
||
AnimalSet animals,
|
||
NeedSet needs,
|
||
SkillSet skills,
|
||
int species,
|
||
Vector2 position,
|
||
float ageDays,
|
||
Genome genome,
|
||
int generation,
|
||
int cellSize
|
||
)
|
||
{
|
||
var sp = animals[species];
|
||
var traits = AnimalPhenotype.FromTraits(Phenotype.Compute(genome, animals.GeneRegistry));
|
||
var isMale = IsMaleGenome(genome);
|
||
var stage = StageAt(ageDays, traits.MaturityAge, traits.Lifespan, sp.Stages);
|
||
var region = RegionFor(sp, stage, isMale);
|
||
|
||
var sprite = new Sprite(region, GameLayers.Beings);
|
||
sprite.CenterOrigin();
|
||
sprite.Color = FurTint(traits);
|
||
|
||
var entity = store.CreateEntity(
|
||
new Transform2D(
|
||
position,
|
||
scale: new Vector2(Scale(sp, traits, stage, region, cellSize))
|
||
),
|
||
sprite,
|
||
new AnimalOrganism
|
||
{
|
||
Genome = genome,
|
||
Traits = traits,
|
||
Species = species,
|
||
IsMale = isMale,
|
||
Generation = generation,
|
||
},
|
||
new AnimalGrowth { Stage = stage, AgeDays = ageDays },
|
||
// Нужды — динамический массив по NeedSet (deplete полны, drive пусты); «мозг» решает сразу.
|
||
new AnimalNeeds { Values = needs.NewValues() },
|
||
new AnimalBrain
|
||
{
|
||
Action = AnimalActions.Wander,
|
||
TargetPlant = -1,
|
||
TargetMate = -1,
|
||
},
|
||
new Health { State = HealthState.Create(sp.Body, traits.BodySize) },
|
||
// Настроение (фаза A8): нейтральное на старте, активность по тиру мозга считает MoodSystem.
|
||
new Mood
|
||
{
|
||
Value = 0.5f,
|
||
Active = false,
|
||
Thoughts = [],
|
||
}
|
||
);
|
||
|
||
// Навыки: стартовые уровни + страсть (детерминированно по геному — воспроизводимо без RNG-параметра).
|
||
entity.AddComponent(
|
||
new Skills
|
||
{
|
||
Levels = skills.NewLevels(),
|
||
Passions = skills.RollPassions(new Random(SkillSeed(genome) ^ species)),
|
||
}
|
||
);
|
||
return entity;
|
||
}
|
||
|
||
// Стабильный сид из генома (сумма аллелей — порядконезависима) для детерминированной страсти к навыкам.
|
||
private static int SkillSeed(Genome genome)
|
||
{
|
||
var acc = 0;
|
||
foreach (var (_, allele) in genome.Alleles) // IReadOnlyDictionary без копии — без аллокации
|
||
{
|
||
acc += (int)(allele.A * 7919f) + (int)(allele.B * 104729f);
|
||
}
|
||
|
||
return acc;
|
||
}
|
||
|
||
// Тинт свежего трупа (стадии гниения/скелета задаёт CorpseSystem).
|
||
private static readonly Color CorpseFreshTint = new(176, 148, 128);
|
||
|
||
/// <summary>Создаёт сущность-труп после смерти животного: спрайт-падаль (масштаб ∝ размер тела) и
|
||
/// запас мяса <paramref name="meatKg"/> (∝ масса тела, считается вызывающим через <see cref="Mass"/>).</summary>
|
||
public static Entity CreateCorpse(
|
||
EntityStore store,
|
||
AnimalSet animals,
|
||
int species,
|
||
Vector2 position,
|
||
float bodySize,
|
||
float meatKg,
|
||
int cellSize
|
||
)
|
||
{
|
||
var sp = animals[species];
|
||
var region = sp.Corpse;
|
||
var sprite = new Sprite(region, GameLayers.Beings);
|
||
sprite.CenterOrigin();
|
||
sprite.Color = CorpseFreshTint;
|
||
var sizeCells = bodySize > 0f ? bodySize : sp.Def.SizeCells;
|
||
return store.CreateEntity(
|
||
new Transform2D(position, scale: new Vector2(cellSize * sizeCells / region.Width)),
|
||
sprite,
|
||
new Corpse
|
||
{
|
||
Species = species,
|
||
RotDays = 0f,
|
||
Stage = 0,
|
||
Meat = meatKg,
|
||
BodySize = bodySize,
|
||
}
|
||
);
|
||
}
|
||
|
||
// Тинт яйца (бледно-кремовый) и доля размера клетки, до которой масштабируется спрайт яйца.
|
||
private static readonly Color EggTint = new(238, 230, 208);
|
||
private const float EggSizeFraction = 0.45f;
|
||
|
||
/// <summary>
|
||
/// Создаёт сущность-яйцо (тип рождения «яйцекладка»): спрайт-яйцо и компонент <see cref="Egg"/> с уже
|
||
/// скрещённым геномом будущего детёныша. Инкубируется и вылупляется в <see cref="EggSystem"/>.
|
||
/// </summary>
|
||
public static Entity CreateEgg(
|
||
EntityStore store,
|
||
AnimalSet animals,
|
||
int species,
|
||
Vector2 position,
|
||
Genome genome,
|
||
int generation,
|
||
float incubateDays,
|
||
int cellSize
|
||
)
|
||
{
|
||
var sp = animals[species];
|
||
var region = sp.Egg;
|
||
var sprite = new Sprite(region, GameLayers.Beings);
|
||
sprite.CenterOrigin();
|
||
sprite.Color = EggTint;
|
||
return store.CreateEntity(
|
||
new Transform2D(
|
||
position,
|
||
scale: new Vector2(cellSize * EggSizeFraction / region.Width)
|
||
),
|
||
sprite,
|
||
new Egg
|
||
{
|
||
Species = species,
|
||
Genome = genome,
|
||
Generation = generation,
|
||
IncubateDays = incubateDays,
|
||
}
|
||
);
|
||
}
|
||
|
||
/// <summary>Порог выраженности признака диеты, с которого вид реально ест данный корм.</summary>
|
||
public const float DietThreshold = 0.5f;
|
||
|
||
/// <summary>
|
||
/// Рацион особи из ГЕНОВ: ест растения, если max(herbivory, omnivory) ≥ порога; ест мясо
|
||
/// (охота/падаль), если max(carnivory, omnivory) ≥ порога — всеядность (omnivory) включает оба
|
||
/// источника, специалисты задаются высоким herbivory ЛИБО carnivory. Если у вида нет генов диеты
|
||
/// (все три ≈ 0) — откат на список <see cref="AnimalDef.Diet"/> (легаси/модерский fallback).
|
||
/// </summary>
|
||
public static (bool EatsPlants, bool EatsMeat) Diet(in AnimalPhenotype traits, AnimalDef def)
|
||
{
|
||
if (traits.Herbivory <= 0f && traits.Carnivory <= 0f && traits.Omnivory <= 0f)
|
||
{
|
||
return (DietContains(def, "plant"), DietContains(def, "meat"));
|
||
}
|
||
|
||
var eatsPlants = MathF.Max(traits.Herbivory, traits.Omnivory) >= DietThreshold;
|
||
var eatsMeat = MathF.Max(traits.Carnivory, traits.Omnivory) >= DietThreshold;
|
||
return (eatsPlants, eatsMeat);
|
||
}
|
||
|
||
private static bool DietContains(AnimalDef def, string food)
|
||
{
|
||
foreach (var f in def.Diet)
|
||
{
|
||
if (string.Equals(f, food, StringComparison.Ordinal))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>Гибель особи: оставляет труп (мясо ∝ масса тела) и удаляет сущность. Общий путь смерти.</summary>
|
||
public static void Die(EntityStore store, AnimalSet animals, Entity entity, int cellSize)
|
||
{
|
||
if (entity.IsNull || !entity.HasComponent<AnimalOrganism>())
|
||
{
|
||
return;
|
||
}
|
||
|
||
ref readonly var org = ref entity.GetComponent<AnimalOrganism>();
|
||
var position = entity.GetComponent<Transform2D>().Position;
|
||
var sp = animals[org.Species];
|
||
var stageScale = entity.TryGetComponent<AnimalGrowth>(out var grow)
|
||
? sp.Stages[Math.Clamp(grow.Stage, 0, sp.Stages.Length - 1)].Scale
|
||
: 1f;
|
||
var meat = Mass.OfAnimal(sp.Def, org.Traits, stageScale) * Mass.MeatFraction;
|
||
CreateCorpse(store, animals, org.Species, position, org.Traits.BodySize, meat, cellSize);
|
||
entity.DeleteEntity();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Эффективный интеллект особи (фаза A7): размер мозга (ген) × сознание (capacity из здоровья).
|
||
/// Повреждение мозга/болезнь/боль/кровопотеря снижают Consciousness → динамически роняют интеллект,
|
||
/// что гейтит набор доступных нужд/действий (см. <see cref="AnimalContext"/>). Вид без частей тела
|
||
/// (сознание не вычисляется) считается полностью сознательным — чтобы не отключить ему всё.
|
||
/// </summary>
|
||
public static float Intelligence(in AnimalPhenotype traits, HealthState? health)
|
||
{
|
||
var consciousness = health is { Parts.Length: > 0 }
|
||
? health.Capacity(AnimalCapacities.Consciousness)
|
||
: 1f;
|
||
return traits.BrainSize * consciousness;
|
||
}
|
||
|
||
/// <summary>Пол из локуса GeneSex: наличие аллеля Y (значение ≈1) → самец; иначе самка (XX).</summary>
|
||
public static bool IsMaleGenome(Genome genome) =>
|
||
genome.Has("GeneSex") && (genome["GeneSex"].A > 0.5f || genome["GeneSex"].B > 0.5f);
|
||
|
||
/// <summary>Индекс стадии по возрасту: последняя стадия, чей возраст входа (EnterAt × якорь) ≤ возраст.</summary>
|
||
public static int StageAt(
|
||
float ageDays,
|
||
float maturity,
|
||
float lifespan,
|
||
AnimalStageDef[] stages
|
||
)
|
||
{
|
||
var stage = 0;
|
||
for (var i = 1; i < stages.Length; i++)
|
||
{
|
||
var anchor = string.Equals(stages[i].RelativeTo, "lifespan", StringComparison.Ordinal)
|
||
? lifespan
|
||
: maturity;
|
||
if (ageDays >= stages[i].EnterAt * anchor)
|
||
{
|
||
stage = i;
|
||
}
|
||
}
|
||
|
||
return stage;
|
||
}
|
||
|
||
/// <summary>Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка).</summary>
|
||
public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) =>
|
||
string.Equals(sp.Stages[stage].Texture, "baby", StringComparison.Ordinal) ? sp.Baby
|
||
: isMale ? sp.Male
|
||
: sp.Female;
|
||
|
||
/// <summary>Взрослая ли стадия (способна размножаться) — флаг из данных стадии.</summary>
|
||
public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult;
|
||
|
||
/// <summary>Масштаб Transform2D для спрайта стадии (размер тела × множитель стадии / ширина региона).</summary>
|
||
public static float Scale(
|
||
AnimalSet.Species sp,
|
||
in AnimalPhenotype traits,
|
||
int stage,
|
||
Texture2DRegion region,
|
||
int cellSize
|
||
)
|
||
{
|
||
var sizeCells =
|
||
(traits.BodySize > 0f ? traits.BodySize : sp.Def.SizeCells) * sp.Stages[stage].Scale;
|
||
return cellSize * sizeCells / region.Width;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Цвет животного из генов: лёгкий тёплый/холодный сдвиг меха (ген furHue) поверх текстуры —
|
||
/// субтильный, чтобы не перекрашивать уже цветной спрайт зверя (как leafHue у растений).
|
||
/// Публичен, чтобы <see cref="AnimalAppearanceSystem"/> домножал его на яркость по нуждам.
|
||
/// </summary>
|
||
public static Color FurTint(in AnimalPhenotype traits)
|
||
{
|
||
var fur = Color.Lerp(FurWarm, FurCool, Math.Clamp(traits.FurHue, 0f, 1f));
|
||
return Color.Lerp(Color.White, fur, 0.5f);
|
||
}
|
||
}
|