Merge branch 'animal-facing' into main
Animals turn to face their direction of travel (east/north/south sprites + west flip), via AnimalFacingSystem and directional sprite sets in AnimalSet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,22 +12,30 @@ namespace LittleSim.Content;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AnimalSet
|
public sealed class AnimalSet
|
||||||
{
|
{
|
||||||
|
/// <summary>Направленные спрайты одной «внешности» (пол/стадия): восток, север, юг. Запад = зеркало
|
||||||
|
/// востока (флип по X). Север/юг берутся по конвенции имени (_east→_north/_south), иначе = восток.</summary>
|
||||||
|
public readonly record struct Directional(
|
||||||
|
Texture2DRegion East,
|
||||||
|
Texture2DRegion North,
|
||||||
|
Texture2DRegion South
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи.</summary>
|
/// <summary>Вид животного: его деф, спрайты по полу/стадии и набор генов для генерации особи.</summary>
|
||||||
public sealed class Species
|
public sealed class Species
|
||||||
{
|
{
|
||||||
/// <summary>Исходный деф животного.</summary>
|
/// <summary>Исходный деф животного.</summary>
|
||||||
public required AnimalDef Def { get; init; }
|
public required AnimalDef Def { get; init; }
|
||||||
|
|
||||||
/// <summary>Спрайт взрослой самки (он же общий/fallback).</summary>
|
/// <summary>Направленные спрайты взрослой самки (она же общий/fallback).</summary>
|
||||||
public required Texture2DRegion Female { get; init; }
|
public required Directional FemaleDirs { get; init; }
|
||||||
|
|
||||||
/// <summary>Спрайт взрослого самца (fallback — самка).</summary>
|
/// <summary>Направленные спрайты взрослого самца (fallback — самка).</summary>
|
||||||
public required Texture2DRegion Male { get; init; }
|
public required Directional MaleDirs { get; init; }
|
||||||
|
|
||||||
/// <summary>Спрайт детёныша/подростка (fallback — самка).</summary>
|
/// <summary>Направленные спрайты детёныша/подростка (fallback — самка).</summary>
|
||||||
public required Texture2DRegion Baby { get; init; }
|
public required Directional BabyDirs { get; init; }
|
||||||
|
|
||||||
/// <summary>Спрайт трупа (fallback — самка).</summary>
|
/// <summary>Спрайт трупа (fallback — самка, восток).</summary>
|
||||||
public required Texture2DRegion Corpse { get; init; }
|
public required Texture2DRegion Corpse { get; init; }
|
||||||
|
|
||||||
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
|
/// <summary>Спрайт яйца (тип рождения «яйцекладка»); по умолчанию — общий спрайт-точка.</summary>
|
||||||
@@ -45,6 +53,12 @@ public sealed class AnimalSet
|
|||||||
/// <summary>Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей
|
/// <summary>Эталонный размер тела вида (база GeneMaxBodySize) — предпосчитан для горячих путей
|
||||||
/// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле.</summary>
|
/// (масса/метаболизм Клайбера каждый кадр), чтобы не делать строковый словарный лукап в цикле.</summary>
|
||||||
public required float RefBodySize { get; init; }
|
public required float RefBodySize { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Направленные спрайты для стадии/пола: детёныш → BabyDirs; иначе по полу (самец/самка).</summary>
|
||||||
|
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++)
|
for (var i = 0; i < defs.Length; i++)
|
||||||
{
|
{
|
||||||
var def = defs[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
|
_species[i] = new Species
|
||||||
{
|
{
|
||||||
Def = def,
|
Def = def,
|
||||||
Female = female,
|
FemaleDirs = LoadDirs(atlases, device, femaleKey),
|
||||||
Male = string.IsNullOrEmpty(def.MaleTexture)
|
MaleDirs = LoadDirs(atlases, device, maleKey),
|
||||||
? female
|
BabyDirs = LoadDirs(atlases, device, babyKey),
|
||||||
: atlases.GetRegion(device, def.MaleTexture),
|
|
||||||
Baby = string.IsNullOrEmpty(def.BabyTexture)
|
|
||||||
? female
|
|
||||||
: atlases.GetRegion(device, def.BabyTexture),
|
|
||||||
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
|
Corpse = string.IsNullOrEmpty(def.CorpseTexture)
|
||||||
? female
|
? atlases.GetRegion(device, femaleKey)
|
||||||
: atlases.GetRegion(device, def.CorpseTexture),
|
: atlases.GetRegion(device, def.CorpseTexture),
|
||||||
Egg = string.IsNullOrEmpty(def.EggTexture)
|
Egg = string.IsNullOrEmpty(def.EggTexture)
|
||||||
? female
|
? atlases.GetRegion(device, femaleKey)
|
||||||
: atlases.GetRegion(device, def.EggTexture),
|
: atlases.GetRegion(device, def.EggTexture),
|
||||||
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
|
Stages = def.Stages.Length > 0 ? def.Stages : DefaultStages,
|
||||||
Body = content.Defs.TryGet<BodyDef>(def.Body, out var body) ? body : null,
|
Body = content.Defs.TryGet<BodyDef>(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;
|
||||||
|
|
||||||
/// <summary>Число видов в таблице.</summary>
|
/// <summary>Число видов в таблице.</summary>
|
||||||
public int Count => _species.Length;
|
public int Count => _species.Length;
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ public sealed class ModAtlases : IDisposable
|
|||||||
return Get(device, name).GetRegion(textureKey);
|
return Get(device, name).GetRegion(textureKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Регион по ключу, если он есть (для опциональных вариантов, напр. направленных спрайтов).</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ public sealed class WorldScene : Scene
|
|||||||
);
|
);
|
||||||
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
|
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
|
||||||
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
|
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 AnimalMortalitySystem(Store, _animals, CellSize));
|
||||||
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
|
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
|
||||||
UpdateSystems.Add(
|
UpdateSystems.Add(
|
||||||
|
|||||||
@@ -269,9 +269,7 @@ public static class AnimalFactory
|
|||||||
|
|
||||||
/// <summary>Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка).</summary>
|
/// <summary>Спрайт стадии: набор "baby" — текстура детёныша; иначе по полу (самец/самка).</summary>
|
||||||
public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) =>
|
public static Texture2DRegion RegionFor(AnimalSet.Species sp, int stage, bool isMale) =>
|
||||||
string.Equals(sp.Stages[stage].Texture, "baby", StringComparison.Ordinal) ? sp.Baby
|
sp.LookDirs(stage, isMale).East; // восток — базовое направление; разворот делает AnimalFacingSystem
|
||||||
: isMale ? sp.Male
|
|
||||||
: sp.Female;
|
|
||||||
|
|
||||||
/// <summary>Взрослая ли стадия (способна размножаться) — флаг из данных стадии.</summary>
|
/// <summary>Взрослая ли стадия (способна размножаться) — флаг из данных стадии.</summary>
|
||||||
public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult;
|
public static bool IsAdult(AnimalSet.Species sp, int stage) => sp.Stages[stage].Adult;
|
||||||
|
|||||||
@@ -1627,6 +1627,67 @@ public sealed class AnimalAppearanceSystem(NeedSet needs)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Разворот спрайта по направлению движения (как в RimWorld): зверь смотрит туда, куда идёт. Восток —
|
||||||
|
/// базовая текстура, запад — её зеркало (флип по X), север/юг — отдельные текстуры (если есть у вида).
|
||||||
|
/// Направление берётся из цели «мозга» (<see cref="AnimalBrain.Target"/>) относительно позиции; стоящий
|
||||||
|
/// зверь сохраняет прежний разворот. При смене региона пересчитывает центр-origin и масштаб (текстуры
|
||||||
|
/// направлений могут отличаться шириной). Презентация (читает sim, пишет только Sprite/Transform-scale).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize)
|
||||||
|
: QuerySystem<AnimalBrain, AnimalOrganism, AnimalGrowth, Transform2D, Sprite>
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
|
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
|
||||||
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.
|
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.
|
||||||
|
|||||||
Reference in New Issue
Block a user