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) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.