diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 4412c46..6529dd8 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -323,6 +323,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 AnimalBobSystem(Context.Clock));
UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, CellSize));
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
UpdateSystems.Add(
diff --git a/src/LittleSim/Sim/AnimalSystems.cs b/src/LittleSim/Sim/AnimalSystems.cs
index 965f1ca..595d782 100644
--- a/src/LittleSim/Sim/AnimalSystems.cs
+++ b/src/LittleSim/Sim/AnimalSystems.cs
@@ -1688,6 +1688,52 @@ public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize)
}
}
+///
+/// «Раскачка» при ходьбе (презентация): идущий зверь покачивается из стороны в сторону (наклон спрайта
+/// через ), стоящий — плавно выпрямляется. Делает движение живее, не
+/// затрагивая симуляцию (она вращение не использует). Фаза покачивания идёт по игровому времени (стоп на
+/// паузе, быстрее на ускорении) и сдвинута по id особи — звери качаются вразнобой. Граница sim/presentation
+/// цела: система пишет только поворот трансформа.
+///
+public sealed class AnimalBobSystem(GameClock clock) : QuerySystem
+{
+ private const float SwaySpeed = 9f; // скорость набега фазы (рад/сек игрового времени)
+ private const float SwayAmplitude = 0.08f; // амплитуда наклона (рад ≈ 4.6°)
+ private const float MinMoveSq = 0.5f * 0.5f; // ближе к цели — считаем стоящим
+ private const float UprightDecay = 0.85f; // как быстро выпрямляется стоя (за кадр)
+
+ private float _phase;
+
+ protected override void OnUpdate()
+ {
+ var dt = clock.DeltaTime;
+ if (dt <= 0f)
+ {
+ return; // пауза — не качаем и не выпрямляем
+ }
+
+ _phase += dt * SwaySpeed;
+ foreach (var (brains, transforms, entities) in Query.Chunks)
+ {
+ var b = brains.Span;
+ var t = transforms.Span;
+ for (var i = 0; i < b.Length; i++)
+ {
+ ref var rot = ref t[i].Rotation;
+ if ((b[i].Target - t[i].Position).LengthSquared() > MinMoveSq)
+ {
+ var offset = entities.EntityAt(i).Id * 0.7f; // десинхрон между особями
+ rot = MathF.Sin(_phase + offset) * SwayAmplitude;
+ }
+ else
+ {
+ rot *= UprightDecay; // у цели/стоя — плавно к вертикали
+ }
+ }
+ }
+ }
+}
+
///
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.