diff --git a/engine/Age.Engine.Tests/AnimChannelTests.cs b/engine/Age.Engine.Tests/AnimChannelTests.cs
new file mode 100644
index 0000000..6e342c6
--- /dev/null
+++ b/engine/Age.Engine.Tests/AnimChannelTests.cs
@@ -0,0 +1,48 @@
+using Age.Engine.Model;
+using Xunit;
+
+public class AnimChannelTests
+{
+ private static GfxState VisibleObj(long handle)
+ {
+ var g = new GfxState();
+ g.SetSurface(1, resId: 5, colorKey: -1);
+ g.BindDraw(handle, 1, 0, 0, 256, 64, 100, 100); // 256x64 sheet at base pos (100,100)
+ return g;
+ }
+
+ [Fact]
+ public void SetSrcRect_StoresGridCellPeriod()
+ {
+ var g = VisibleObj(0x100);
+ g.SetSrcRect(0x100, gridW: 4, gridH: 1, cell: 2, period: 800);
+ var o = g.TryGet(0x100)!;
+ Assert.Equal(4, o.SrcGridW);
+ Assert.Equal(2, o.SrcCell);
+ Assert.Equal(800, o.SrcPeriod);
+ Assert.True(o.SrcAnim);
+ Assert.Equal(-1, o.SrcStart); // uninitialized until first interpolated frame
+ }
+
+ [Fact]
+ public void SetSrcRect_ClampsGridToAtLeastOne()
+ {
+ var g = VisibleObj(0x100);
+ g.SetSrcRect(0x100, gridW: 0, gridH: 0, cell: 0, period: 0);
+ var o = g.TryGet(0x100)!;
+ Assert.Equal(1, o.SrcGridW);
+ Assert.Equal(1, o.SrcGridH);
+ }
+
+ [Fact]
+ public void SetColorAnim_StoresPeriodAndTarget()
+ {
+ var g = VisibleObj(0x100);
+ long target = GfxState.PackColor(0x80, 0xFF0000); // half-alpha red glow
+ g.SetColorAnim(0x100, period: 1000, target: target);
+ var o = g.TryGet(0x100)!;
+ Assert.Equal(1000, o.ColorPeriod);
+ Assert.Equal(target, o.ColorTarget);
+ Assert.True(o.ColorAnim);
+ }
+}
diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs
index 318870a..c7ca1a2 100644
--- a/engine/Age.Engine/Model/GfxState.cs
+++ b/engine/Age.Engine/Model/GfxState.cs
@@ -32,6 +32,15 @@ public sealed class GfxState
public long Field64, Field68, Field6c;
public long Color;
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
+
+ // ---- src-rect / spritesheet-cell channel (ops 0x239 static cell, 0x231 animate). Interpolator
+ // SRC-RECT SCROLL channel: period obj+0x230, start obj+0x21c, grid obj+0x238/0x23c. ----
+ public long SrcGridW = 1, SrcGridH = 1, SrcCell, SrcPeriod, SrcStart = -1;
+ public bool SrcAnim;
+ // ---- animated color/glow channel (op 0x232). Interpolator COLOR channel: period obj+0x220,
+ // start obj+0x20c, target obj+0x240 — PING-PONG (distinct from static 0x202/0x203). ----
+ public long ColorPeriod, ColorStart = -1, ColorTarget;
+ public bool ColorAnim;
// draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag.
public int SourceSlot = -1;
public (int X, int Y, int W, int H) SrcRect;
@@ -137,6 +146,29 @@ public sealed class GfxState
{
lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = true; }
}
+
+ /// Ops 0x239 (static cell, period=0) / 0x231 (animate, period>0): set the spritesheet grid +
+ /// the visible cell. When animated, the interpolator ping-pongs the cell across the grid over the period.
+ public void SetSrcRect(long handle, long gridW, long gridH, long cell, long period)
+ {
+ lock (_lock)
+ {
+ var o = GetOrCreate(handle);
+ o.SrcGridW = gridW < 1 ? 1 : gridW; o.SrcGridH = gridH < 1 ? 1 : gridH;
+ o.SrcCell = cell; o.SrcPeriod = period; o.SrcStart = -1; o.SrcAnim = true;
+ }
+ }
+
+ /// Op 0x232 (anim-color): ping-pong the object's color/alpha toward
+ /// (packed 0xAARRGGBB) over ms — the pulsing glow. Distinct from static 0x202/0x203.
+ public void SetColorAnim(long handle, long period, long target)
+ {
+ lock (_lock)
+ {
+ var o = GetOrCreate(handle);
+ o.ColorPeriod = period; o.ColorTarget = target; o.ColorStart = -1; o.ColorAnim = true;
+ }
+ }
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank)
/// draw-texture bind (gfx_object_bind_draw): object draws surface