@@ -995,6 +995,10 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
995995
996996 // Build item rows with sprite previews rendered from source SPR
997997 var itemsPanel = new Avalonia . Controls . StackPanel { Spacing = 2 } ;
998+
999+ // Track animated entries for the dialog's animation timer
1000+ var animatedEntries = new List < ( Avalonia . Controls . Image img , DatThingType thing , int frames , int currentFrame ) > ( ) ;
1001+
9981002 foreach ( var entry in entries )
9991003 {
10001004 var rowGrid = new Avalonia . Controls . Grid
@@ -1032,15 +1036,21 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
10321036 ClipToBounds = true ,
10331037 } ;
10341038 var bmp = ComposeThingBitmapStatic ( entry . SourceThing , sourceSpr ) ;
1039+ Avalonia . Controls . Image ? spriteImg = null ;
10351040 if ( bmp != null )
10361041 {
1037- var img = new Avalonia . Controls . Image
1042+ spriteImg = new Avalonia . Controls . Image
10381043 {
10391044 Source = bmp , Width = 32 , Height = 32 ,
10401045 Stretch = Avalonia . Media . Stretch . Uniform ,
10411046 } ;
1042- Avalonia . Media . RenderOptions . SetBitmapInterpolationMode ( img , Avalonia . Media . Imaging . BitmapInterpolationMode . None ) ;
1043- spriteBorder . Child = img ;
1047+ Avalonia . Media . RenderOptions . SetBitmapInterpolationMode ( spriteImg , Avalonia . Media . Imaging . BitmapInterpolationMode . None ) ;
1048+ spriteBorder . Child = spriteImg ;
1049+
1050+ // Register for animation if thing has multiple frames
1051+ var fg0 = entry . SourceThing . FrameGroups . Length > 0 ? entry . SourceThing . FrameGroups [ 0 ] : null ;
1052+ if ( fg0 != null && fg0 . Frames > 1 )
1053+ animatedEntries . Add ( ( spriteImg , entry . SourceThing , fg0 . Frames , 0 ) ) ;
10441054 }
10451055 Avalonia . Controls . Grid . SetColumn ( spriteBorder , 1 ) ;
10461056 rowGrid . Children . Add ( spriteBorder ) ;
@@ -1189,6 +1199,37 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
11891199 buttonPanel . Children . Add ( cancelBtn ) ;
11901200 buttonPanel . Children . Add ( confirmBtn ) ;
11911201
1202+ // Animation timer for sprite previews in the merge dialog
1203+ DispatcherTimer ? mergeAnimTimer = null ;
1204+ if ( animatedEntries . Count > 0 )
1205+ {
1206+ var animState = animatedEntries . Select ( e => new { e . img , e . thing , e . frames , frame = new int [ ] { 0 } } ) . ToList ( ) ;
1207+ int tickCounter = 0 ;
1208+ mergeAnimTimer = new DispatcherTimer { Interval = TimeSpan . FromMilliseconds ( 100 ) } ;
1209+ mergeAnimTimer . Tick += ( _ , _ ) =>
1210+ {
1211+ tickCounter ++ ;
1212+ foreach ( var s in animState )
1213+ {
1214+ int divisor = s . thing . Category switch
1215+ {
1216+ ThingCategory . Effect => 1 ,
1217+ ThingCategory . Missile => 1 ,
1218+ ThingCategory . Outfit => 3 ,
1219+ _ => 5 ,
1220+ } ;
1221+ if ( tickCounter % divisor != 0 ) continue ;
1222+ s . frame [ 0 ] = ( s . frame [ 0 ] + 1 ) % s . frames ;
1223+ var newBmp = ComposeThingBitmapStatic ( s . thing , sourceSpr , s . frame [ 0 ] ) ;
1224+ if ( newBmp != null )
1225+ s . img . Source = newBmp ;
1226+ }
1227+ } ;
1228+ mergeAnimTimer . Start ( ) ;
1229+ }
1230+
1231+ dialog . Closed += ( _ , _ ) => mergeAnimTimer ? . Stop ( ) ;
1232+
11921233 await dialog . ShowDialog ( window ) ;
11931234 }
11941235
@@ -1251,6 +1292,10 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
12511292
12521293 // Build the item list with sprite previews
12531294 var itemsPanel = new Avalonia . Controls . StackPanel { Spacing = 2 } ;
1295+
1296+ // Track animated entries for the dialog's animation timer
1297+ var batchAnimEntries = new List < ( Avalonia . Controls . Image img , DatThingType thing , int frames , int currentFrame ) > ( ) ;
1298+
12541299 foreach ( var entry in entries )
12551300 {
12561301 var rowGrid = new Avalonia . Controls . Grid
@@ -1286,7 +1331,7 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
12861331 VerticalAlignment = Avalonia . Layout . VerticalAlignment . Center ,
12871332 ClipToBounds = true ,
12881333 } ;
1289- var bmp = ComposeThingBitmap ( entry . SourceThing ) ;
1334+ var bmp = ComposeThingBitmapStatic ( entry . SourceThing , sourceSpr ) ;
12901335 if ( bmp != null )
12911336 {
12921337 var img = new Avalonia . Controls . Image
@@ -1296,6 +1341,10 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
12961341 } ;
12971342 Avalonia . Media . RenderOptions . SetBitmapInterpolationMode ( img , Avalonia . Media . Imaging . BitmapInterpolationMode . None ) ;
12981343 spriteBorder . Child = img ;
1344+
1345+ var fg0 = entry . SourceThing . FrameGroups . Length > 0 ? entry . SourceThing . FrameGroups [ 0 ] : null ;
1346+ if ( fg0 != null && fg0 . Frames > 1 )
1347+ batchAnimEntries . Add ( ( img , entry . SourceThing , fg0 . Frames , 0 ) ) ;
12991348 }
13001349 Avalonia . Controls . Grid . SetColumn ( spriteBorder , 1 ) ;
13011350 rowGrid . Children . Add ( spriteBorder ) ;
@@ -1457,6 +1506,37 @@ is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetim
14571506 buttonPanel . Children . Add ( cancelBtn ) ;
14581507 buttonPanel . Children . Add ( confirmBtn ) ;
14591508
1509+ // Animation timer for sprite previews in the batch transplant dialog
1510+ DispatcherTimer ? batchAnimTimer = null ;
1511+ if ( batchAnimEntries . Count > 0 )
1512+ {
1513+ var animState = batchAnimEntries . Select ( e => new { e . img , e . thing , e . frames , frame = new int [ ] { 0 } } ) . ToList ( ) ;
1514+ int tickCounter = 0 ;
1515+ batchAnimTimer = new DispatcherTimer { Interval = TimeSpan . FromMilliseconds ( 100 ) } ;
1516+ batchAnimTimer . Tick += ( _ , _ ) =>
1517+ {
1518+ tickCounter ++ ;
1519+ foreach ( var s in animState )
1520+ {
1521+ int divisor = s . thing . Category switch
1522+ {
1523+ ThingCategory . Effect => 1 ,
1524+ ThingCategory . Missile => 1 ,
1525+ ThingCategory . Outfit => 3 ,
1526+ _ => 5 ,
1527+ } ;
1528+ if ( tickCounter % divisor != 0 ) continue ;
1529+ s . frame [ 0 ] = ( s . frame [ 0 ] + 1 ) % s . frames ;
1530+ var newBmp = ComposeThingBitmapStatic ( s . thing , sourceSpr , s . frame [ 0 ] ) ;
1531+ if ( newBmp != null )
1532+ s . img . Source = newBmp ;
1533+ }
1534+ } ;
1535+ batchAnimTimer . Start ( ) ;
1536+ }
1537+
1538+ dialog . Closed += ( _ , _ ) => batchAnimTimer ? . Stop ( ) ;
1539+
14601540 await dialog . ShowDialog ( window ) ;
14611541 }
14621542
@@ -6293,7 +6373,7 @@ private void RefreshAfterSpriteEdit(uint spriteId)
62936373 /// Static version of ComposeThingBitmap that takes an explicit SprFile.
62946374 /// Used for composing sprites in a target session context (e.g. after transplant).
62956375 /// </summary>
6296- internal static WriteableBitmap ? ComposeThingBitmapStatic ( DatThingType thing , SprFile sprFile )
6376+ internal static WriteableBitmap ? ComposeThingBitmapStatic ( DatThingType thing , SprFile sprFile , int frame = 0 )
62976377 {
62986378 if ( thing . FrameGroups . Length == 0 ) return null ;
62996379
@@ -6302,13 +6382,15 @@ private void RefreshAfterSpriteEdit(uint spriteId)
63026382 int h = fg . Height ;
63036383 if ( w == 0 || h == 0 ) return null ;
63046384
6385+ int clampedFrame = Math . Clamp ( frame , 0 , Math . Max ( 0 , fg . Frames - 1 ) ) ;
6386+
63056387 // Single 1×1 item
63066388 if ( w == 1 && h == 1 && fg . Layers == 1 )
63076389 {
63086390 int px = 0 ;
63096391 if ( thing . Category == ThingCategory . Outfit && fg . PatternX > 2 )
63106392 px = 2 ;
6311- uint sprId = fg . GetSpriteId ( 0 , 0 , 0 , px , 0 , 0 , 0 ) ;
6393+ uint sprId = fg . GetSpriteId ( 0 , 0 , 0 , px , 0 , 0 , clampedFrame ) ;
63126394 var rgba = sprFile . GetSpriteRgba ( sprId ) ;
63136395 if ( rgba == null ) return null ;
63146396 try
@@ -6340,7 +6422,7 @@ private void RefreshAfterSpriteEdit(uint spriteId)
63406422 {
63416423 for ( int th = 0 ; th < h ; th ++ )
63426424 {
6343- uint sprId = fg . GetSpriteId ( tw , th , l , patX , 0 , 0 , 0 ) ;
6425+ uint sprId = fg . GetSpriteId ( tw , th , l , patX , 0 , 0 , clampedFrame ) ;
63446426 var rgba = sprFile . GetSpriteRgba ( sprId ) ;
63456427 if ( rgba == null ) continue ;
63466428
0 commit comments