diff --git a/src/LittleSim/Content/AnimalSet.cs b/src/LittleSim/Content/AnimalSet.cs index 50681be..87058b8 100644 --- a/src/LittleSim/Content/AnimalSet.cs +++ b/src/LittleSim/Content/AnimalSet.cs @@ -12,22 +12,30 @@ namespace LittleSim.Content; /// public sealed class AnimalSet { + /// Направленные спрайты одной «внешности» (пол/стадия): восток, север, юг. Запад = зеркало + /// востока (флип по X). Север/юг берутся по конвенции имени (_east→_north/_south), иначе = восток. + public readonly record struct Directional( + Texture2DRegion East, + Texture2DRegion North, + Texture2DRegion South + ); + /// Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи. public sealed class Species { /// Исходный деф животного. public required AnimalDef Def { get; init; } - /// Спрайт взрослой самки (он же общий/fallback). - public required Texture2DRegion Female { get; init; } + /// Направленные спрайты взрослой самки (она же общий/fallback). + public required Directional FemaleDirs { get; init; } - /// Спрайт взрослого самца (fallback — самка). - public required Texture2DRegion Male { get; init; } + /// Направленные спрайты взрослого самца (fallback — самка). + public required Directional MaleDirs { get; init; } - /// Спрайт детёныша/подростка (fallback — самка). - public required Texture2DRegion Baby { get; init; } + /// Направленные спрайты детёныша/подростка (fallback — самка). + public required Directional BabyDirs { get; init; } - /// Спрайт трупа (fallback — самка). + /// Спрайт трупа (fallback — самка, восток). public required Texture2DRegion Corpse { get; init; } /// Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка. @@ -45,6 +53,12 @@ public sealed class AnimalSet /// Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей /// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле. public required float RefBodySize { get; init; } + + /// Направленные спрайты для стадии/пола: детёныш → BabyDirs; иначе по полу (самец/самка). + public Directional LookDirs(int stage, bool isMale) => + string.Equals(Stages[stage].Texture, "baby", StringComparison.Ordinal) ? BabyDirs + : isMale ? MaleDirs + : FemaleDirs; } // Встроенный набор стадий по умолчанию (если вид не задал свой): как было до выноса в данные. @@ -105,22 +119,20 @@ public sealed class AnimalSet for (var i = 0; i < defs.Length; i++) { var def = defs[i]; - var female = atlases.GetRegion(device, def.Texture); + var femaleKey = def.Texture; + var maleKey = string.IsNullOrEmpty(def.MaleTexture) ? femaleKey : def.MaleTexture; + var babyKey = string.IsNullOrEmpty(def.BabyTexture) ? femaleKey : def.BabyTexture; _species[i] = new Species { Def = def, - Female = female, - Male = string.IsNullOrEmpty(def.MaleTexture) - ? female - : atlases.GetRegion(device, def.MaleTexture), - Baby = string.IsNullOrEmpty(def.BabyTexture) - ? female - : atlases.GetRegion(device, def.BabyTexture), + FemaleDirs = LoadDirs(atlases, device, femaleKey), + MaleDirs = LoadDirs(atlases, device, maleKey), + BabyDirs = LoadDirs(atlases, device, babyKey), Corpse = string.IsNullOrEmpty(def.CorpseTexture) - ? female + ? atlases.GetRegion(device, femaleKey) : atlases.GetRegion(device, def.CorpseTexture), Egg = string.IsNullOrEmpty(def.EggTexture) - ? female + ? atlases.GetRegion(device, femaleKey) : atlases.GetRegion(device, def.EggTexture), Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages, Body = content.Defs.TryGet(def.Body, out var body) ? body : null, @@ -134,6 +146,35 @@ public sealed class AnimalSet } } + // Грузит направленный набор по восточному ключу: север/юг — по конвенции имени (_east→_north/_south), + // если такой регион есть в атласе; иначе откат на восток (вид с одним спрайтом просто не разворачивается + // вверх/вниз, но влево/вправо — флипом востока). Запад всегда = зеркало востока. + private static Directional LoadDirs(ModAtlases atlases, GraphicsDevice device, string eastKey) + { + var east = atlases.GetRegion(device, eastKey); + return new Directional( + east, + TryDir(atlases, device, eastKey, "_north", east), + TryDir(atlases, device, eastKey, "_south", east) + ); + } + + private static Texture2DRegion TryDir( + ModAtlases atlases, + GraphicsDevice device, + string eastKey, + string suffix, + Texture2DRegion fallback + ) => + eastKey.Contains("_east", StringComparison.Ordinal) + && atlases.TryGetRegion( + device, + eastKey.Replace("_east", suffix, StringComparison.Ordinal), + out var region + ) + ? region + : fallback; + /// Число видов в таблице. public int Count => _species.Length; diff --git a/src/LittleSim/Content/ModAtlases.cs b/src/LittleSim/Content/ModAtlases.cs index e0d7946..790ad76 100644 --- a/src/LittleSim/Content/ModAtlases.cs +++ b/src/LittleSim/Content/ModAtlases.cs @@ -48,6 +48,15 @@ public sealed class ModAtlases : IDisposable return Get(device, name).GetRegion(textureKey); } + /// Регион по ключу, если он есть (для опциональных вариантов, напр. направленных спрайтов). + public bool TryGetRegion(GraphicsDevice device, string textureKey, out Texture2DRegion region) + { + var segments = textureKey.Split('/'); + var depth = Math.Min(GroupDepth, segments.Length - 1); + var name = depth == 0 ? "Atlas" : string.Join('.', segments[..depth]); + return Get(device, name).TryGetRegion(textureKey, out region); + } + /// public void Dispose() { diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 5d7211d..d0d694b 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -322,6 +322,7 @@ public sealed class WorldScene : Scene ); UpdateSystems.Add(new AnimalAppearanceSystem(_needs)); UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize)); + UpdateSystems.Add(new AnimalFacingSystem(_animals, CellSize)); UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, CellSize)); UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate)); UpdateSystems.Add( diff --git a/src/LittleSim/Sim/AnimalFactory.cs b/src/LittleSim/Sim/AnimalFactory.cs index 4ce20a1..1163751 100644 --- a/src/LittleSim/Sim/AnimalFactory.cs +++ b/src/LittleSim/Sim/AnimalFactory.cs @@ -269,9 +269,7 @@ public static class AnimalFactory /// Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка). 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; + sp.LookDirs(stage, isMale).East; // восток — базовое направление; разворот делает AnimalFacingSystem /// Взрослая ли стадия (способна размножаться) — флаг из данных стадии. public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult; diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs index 0e9735d..965f1ca 100644 --- a/src/LittleSim/Sim/AnimalSystems.cs +++ b/src/LittleSim/Sim/AnimalSystems.cs @@ -1627,6 +1627,67 @@ public sealed class AnimalAppearanceSystem(NeedSet needs) } } +/// +/// Разворот спрайта по направлению движения (как в RimWorld): зверь смотрит туда, куда идёт. Восток — +/// базовая текстура, запад — её зеркало (флип по X), север/юг — отдельные текстуры (если есть у вида). +/// Направление берётся из цели «мозга» () относительно позиции; стоящий +/// зверь сохраняет прежний разворот. При смене региона пересчитывает центр-origin и масштаб (текстуры +/// направлений могут отличаться шириной). Презентация (читает sim, пишет только Sprite/Transform-scale). +/// +public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize) + : QuerySystem +{ + private const float MinMove = 0.5f; // короче — считаем стоящим, разворот не трогаем + + protected override void OnUpdate() + { + foreach (var (brains, organisms, growths, transforms, sprites, _) in Query.Chunks) + { + var b = brains.Span; + var o = organisms.Span; + var g = growths.Span; + var t = transforms.Span; + var s = sprites.Span; + for (var i = 0; i < b.Length; i++) + { + var dir = b[i].Target - t[i].Position; + if (dir.LengthSquared() < MinMove * MinMove) + { + continue; // стоит/у цели — сохраняем текущий разворот + } + + var sp = animals[o[i].Species]; + var dirs = sp.LookDirs(g[i].Stage, o[i].IsMale); + Texture2DRegion region; + SpriteFlip flip; + if (MathF.Abs(dir.X) >= MathF.Abs(dir.Y)) + { + region = dirs.East; + flip = dir.X < 0f ? SpriteFlip.X : SpriteFlip.None; // запад = зеркало востока + } + else + { + region = dir.Y < 0f ? dirs.North : dirs.South; // экранный Y вниз: вверх = север + flip = SpriteFlip.None; + } + + ref var sprite = ref s[i]; + if (sprite.Region == region && sprite.Flip == flip) + { + continue; // разворот не изменился + } + + sprite.Region = region; + sprite.Origin = new Vector2(region.Width / 2f, region.Height / 2f); + sprite.Flip = flip; + t[i].Scale = new Vector2( + AnimalFactory.Scale(sp, o[i].Traits, g[i].Stage, region, cellSize) + ); + } + } + } +} + /// /// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→ /// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.