diff --git a/AssetExtractor/Program.cs b/AssetExtractor/Program.cs index 28eb77c..a9b4462 100644 --- a/AssetExtractor/Program.cs +++ b/AssetExtractor/Program.cs @@ -55,6 +55,8 @@ public static class Program string? currentList = null; string itemName = ""; int tileW = 0, tileH = 0; + bool transparent = false; + string transparentColor = "clBlack"; StringBuilder? hex = null; byte[]? pictureData = null; int itemIndex = 0; @@ -64,7 +66,8 @@ public static class Program if (currentList == null || pictureData == null) return; string safeName = itemName.Length > 0 ? itemName : "unnamed"; string file = $"{currentList.ToLowerInvariant()}_{itemIndex:D2}_{safeName}.png"; - SaveGraphic(pictureData, Path.Combine(_outDir, "gfx", file)); + SaveGraphic(pictureData, Path.Combine(_outDir, "gfx", file), + transparent ? ParseDelphiColor(transparentColor) : null); manifest.TryAdd(currentList, []); // TileWidth/TileHeight of 0 means "whole image is one frame" manifest[currentList].Add(new ImageItem(itemIndex, itemName, file, tileW, tileH)); @@ -73,6 +76,8 @@ public static class Program pictureData = null; itemName = ""; tileW = tileH = 0; + transparent = false; + transparentColor = "clBlack"; } foreach (var raw in lines) @@ -116,6 +121,10 @@ public static class Program tileW = int.Parse(line["TileWidth = ".Length..]); else if (line.StartsWith("TileHeight = ")) tileH = int.Parse(line["TileHeight = ".Length..]); + else if (line.StartsWith("TransparentColor = ")) + transparentColor = line["TransparentColor = ".Length..]; + else if (line == "Transparent = True") + transparent = true; else if (line.StartsWith("Picture.Data = {")) hex = new StringBuilder(line["Picture.Data = {".Length..]); } @@ -126,8 +135,27 @@ public static class Program Console.WriteLine(" wrote gfx/manifest.json"); } + /// Delphi TColor: 0x00BBGGRR integer or a clXxx name. + static Color ParseDelphiColor(string value) + { + switch (value) + { + case "clBlack": return Color.FromArgb(0, 0, 0); + case "clWhite": return Color.FromArgb(255, 255, 255); + case "clRed": return Color.FromArgb(255, 0, 0); + case "clLime": return Color.FromArgb(0, 255, 0); + case "clBlue": return Color.FromArgb(0, 0, 255); + case "clNavy": return Color.FromArgb(0, 0, 128); + case "clFuchsia": return Color.FromArgb(255, 0, 255); + case "clYellow": return Color.FromArgb(255, 255, 0); + case "clAqua": return Color.FromArgb(0, 255, 255); + } + int v = int.Parse(value); + return Color.FromArgb(v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF); + } + /// Delphi TPicture blob: [1B class-name length][class name][payload]. - static void SaveGraphic(byte[] data, string outPath) + static void SaveGraphic(byte[] data, string outPath, Color? colorKey) { int nameLen = data[0]; string cls = Encoding.ASCII.GetString(data, 1, nameLen); @@ -135,14 +163,21 @@ public static class Program if (cls == "TPNGObject") { - File.WriteAllBytes(outPath, data[payloadStart..]); + // PNGs keep their native alpha (Omega honored it); key applies on top if set + byte[] png = data[payloadStart..]; + if (colorKey != null) + SaveWithColorKey(png, outPath, colorKey.Value); + else + File.WriteAllBytes(outPath, png); } else if (cls == "TBitmap") { - // payload: [4B length][BMP bytes] + // payload: [4B length][BMP bytes]; key only when the item was marked Transparent int len = BitConverter.ToInt32(data, payloadStart); - var bmpBytes = new ReadOnlySpan(data, payloadStart + 4, len); - SaveBmpAsKeyedPng(bmpBytes.ToArray(), outPath); + var bmpBytes = new ReadOnlySpan(data, payloadStart + 4, len).ToArray(); + using var ms = new MemoryStream(bmpBytes); + using var src = new Bitmap(ms); + SaveKeyed(src, outPath, colorKey); } else { @@ -150,6 +185,27 @@ public static class Program } } + /// Re-encode as PNG with the given color made fully transparent (Omega's Transparent=True). + static void SaveWithColorKey(byte[] imageBytes, string outPath, Color key) + { + using var ms = new MemoryStream(imageBytes); + using var src = new Bitmap(ms); + SaveKeyed(src, outPath, key); + } + + static void SaveKeyed(Bitmap src, string outPath, Color? key) + { + using var dst = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb); + for (int y = 0; y < src.Height; y++) + for (int x = 0; x < src.Width; x++) + { + var c = src.GetPixel(x, y); + bool keyed = key != null && c.A > 0 && c.R == key.Value.R && c.G == key.Value.G && c.B == key.Value.B; + dst.SetPixel(x, y, keyed ? Color.Transparent : c); + } + dst.Save(outPath, ImageFormat.Png); + } + /// Convert BMP to PNG, keying out the bottom-left pixel color (Delphi Transparent convention). static void SaveBmpAsKeyedPng(byte[] bmpBytes, string outPath) { diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_00_player.png b/FreedomFighter/Content/gfx/omegaimagelist1_00_player.png index e032c03..8c81ed2 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_00_player.png and b/FreedomFighter/Content/gfx/omegaimagelist1_00_player.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_04_water.png b/FreedomFighter/Content/gfx/omegaimagelist1_04_water.png index 73cc32f..124b38e 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_04_water.png and b/FreedomFighter/Content/gfx/omegaimagelist1_04_water.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_13_boss2.png b/FreedomFighter/Content/gfx/omegaimagelist1_13_boss2.png index f600019..50be868 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_13_boss2.png and b/FreedomFighter/Content/gfx/omegaimagelist1_13_boss2.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_17_mine.png b/FreedomFighter/Content/gfx/omegaimagelist1_17_mine.png index a5855ba..e95959e 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_17_mine.png and b/FreedomFighter/Content/gfx/omegaimagelist1_17_mine.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_18_missile_turret.png b/FreedomFighter/Content/gfx/omegaimagelist1_18_missile_turret.png index 9432026..917eefb 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_18_missile_turret.png and b/FreedomFighter/Content/gfx/omegaimagelist1_18_missile_turret.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_19_missile.png b/FreedomFighter/Content/gfx/omegaimagelist1_19_missile.png index efe7bf0..5fd57c5 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_19_missile.png and b/FreedomFighter/Content/gfx/omegaimagelist1_19_missile.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist1_20_scanner.png b/FreedomFighter/Content/gfx/omegaimagelist1_20_scanner.png index 467fb0c..42c608a 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist1_20_scanner.png and b/FreedomFighter/Content/gfx/omegaimagelist1_20_scanner.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_01_title.png b/FreedomFighter/Content/gfx/omegaimagelist2_01_title.png index 7048027..5b87744 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_01_title.png and b/FreedomFighter/Content/gfx/omegaimagelist2_01_title.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_03_title2.png b/FreedomFighter/Content/gfx/omegaimagelist2_03_title2.png index 484dd2c..e6f7df2 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_03_title2.png and b/FreedomFighter/Content/gfx/omegaimagelist2_03_title2.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_04_newgame.png b/FreedomFighter/Content/gfx/omegaimagelist2_04_newgame.png index aba6a52..875eac5 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_04_newgame.png and b/FreedomFighter/Content/gfx/omegaimagelist2_04_newgame.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_05_options.png b/FreedomFighter/Content/gfx/omegaimagelist2_05_options.png index d64aab3..d7e99ec 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_05_options.png and b/FreedomFighter/Content/gfx/omegaimagelist2_05_options.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_06_highscore.png b/FreedomFighter/Content/gfx/omegaimagelist2_06_highscore.png index 113bf7e..1a4f920 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_06_highscore.png and b/FreedomFighter/Content/gfx/omegaimagelist2_06_highscore.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_07_credits.png b/FreedomFighter/Content/gfx/omegaimagelist2_07_credits.png index 78530b7..63658f2 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_07_credits.png and b/FreedomFighter/Content/gfx/omegaimagelist2_07_credits.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist2_08_quit.png b/FreedomFighter/Content/gfx/omegaimagelist2_08_quit.png index 8a5c6ea..34224f7 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist2_08_quit.png and b/FreedomFighter/Content/gfx/omegaimagelist2_08_quit.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist3_00_bar.png b/FreedomFighter/Content/gfx/omegaimagelist3_00_bar.png index 6ea0f6b..6e68421 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist3_00_bar.png and b/FreedomFighter/Content/gfx/omegaimagelist3_00_bar.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist3_01_health.png b/FreedomFighter/Content/gfx/omegaimagelist3_01_health.png index 3dbd0c3..bd6421f 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist3_01_health.png and b/FreedomFighter/Content/gfx/omegaimagelist3_01_health.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist3_03_energy.png b/FreedomFighter/Content/gfx/omegaimagelist3_03_energy.png index 1547044..4b10faa 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist3_03_energy.png and b/FreedomFighter/Content/gfx/omegaimagelist3_03_energy.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist4_00_Tiles.png b/FreedomFighter/Content/gfx/omegaimagelist4_00_Tiles.png index ac976be..f2e4b53 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist4_00_Tiles.png and b/FreedomFighter/Content/gfx/omegaimagelist4_00_Tiles.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist4_01_collision.png b/FreedomFighter/Content/gfx/omegaimagelist4_01_collision.png index 16cc0f0..c50ff54 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist4_01_collision.png and b/FreedomFighter/Content/gfx/omegaimagelist4_01_collision.png differ diff --git a/FreedomFighter/Content/gfx/omegaimagelist4_02_trigger.png b/FreedomFighter/Content/gfx/omegaimagelist4_02_trigger.png index 04fdb1c..29aecd3 100644 Binary files a/FreedomFighter/Content/gfx/omegaimagelist4_02_trigger.png and b/FreedomFighter/Content/gfx/omegaimagelist4_02_trigger.png differ diff --git a/FreedomFighter/Game1.cs b/FreedomFighter/Game1.cs index 7780185..aa0fda2 100644 --- a/FreedomFighter/Game1.cs +++ b/FreedomFighter/Game1.cs @@ -17,6 +17,7 @@ public class Game1 : Game SpriteBatch _sb = null!; SpriteFont _font12 = null!; SpriteFont _font20 = null!; + RenderTarget2D _virtualScreen = null!; // fixed 800x600 scene, stretched to the window bool _paused; string _enteredName = ""; @@ -44,6 +45,18 @@ public class Game1 : Game IsFixedTimeStep = true; TargetElapsedTime = TimeSpan.FromSeconds(1 / 120.0); Window.Title = "Freedom Fighter"; + Window.AllowUserResizing = true; + Window.ClientSizeChanged += (_, _) => + { + int w = Window.ClientBounds.Width, h = Window.ClientBounds.Height; + if (w > 0 && h > 0 && + (w != _graphics.PreferredBackBufferWidth || h != _graphics.PreferredBackBufferHeight)) + { + _graphics.PreferredBackBufferWidth = w; + _graphics.PreferredBackBufferHeight = h; + _graphics.ApplyChanges(); + } + }; } protected override void LoadContent() @@ -51,6 +64,13 @@ public class Game1 : Game _sb = new SpriteBatch(GraphicsDevice); _font12 = Content.Load("Verdana12"); _font20 = Content.Load("Verdana20"); + _virtualScreen = new RenderTarget2D(GraphicsDevice, G.ScreenWidth, G.ScreenHeight); + + // the Delphi form's Width/Height were the OUTER window size (800x600, sizeable border); + // as a DPI-unaware app it was also stretched by Windows, so scale by the current DPI + if (System.Windows.Forms.Control.FromHandle(Window.Handle) is System.Windows.Forms.Form form) + form.Size = new System.Drawing.Size( + G.ScreenWidth * form.DeviceDpi / 96, G.ScreenHeight * form.DeviceDpi / 96); string contentDir = Path.Combine(AppContext.BaseDirectory, "Content"); @@ -109,36 +129,45 @@ public class Game1 : Game if (G.Input.PauseClicked) _paused = !_paused; - if (_paused) + + if (!_paused) { - base.Draw(gameTime); - return; + GraphicsDevice.SetRenderTarget(_virtualScreen); + GraphicsDevice.Clear(Color.Black); + _sb.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp); + + G.Sprites.Move(0); + + switch (G.State) + { + case GameState.Intro: DoIntro(); break; + case GameState.Idle: DrawMenu(); break; + case GameState.Intermission: DoIntermission(); break; + case GameState.Playing: GameLoop(); break; + case GameState.Stats: DoStats(); break; + case GameState.Shop: DoShop(); break; + case GameState.GameOver: DoGameOver(); break; + case GameState.Credits: DoCredits(); break; + case GameState.HighScore: DoHighScore(); break; + case GameState.EnterName: DoEnterName(); break; + } + + G.Sprites.Collision(); + G.Sprites.RemoveDead(); + G.Sprites.Draw(_sb); + + _sb.End(); + GraphicsDevice.SetRenderTarget(null); } + // stretch the 800x600 scene to the window, as DirectX presentation did GraphicsDevice.Clear(Color.Black); - _sb.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp); - - G.Sprites.Move(0); - - switch (G.State) - { - case GameState.Intro: DoIntro(); break; - case GameState.Idle: DrawMenu(); break; - case GameState.Intermission: DoIntermission(); break; - case GameState.Playing: GameLoop(); break; - case GameState.Stats: DoStats(); break; - case GameState.Shop: DoShop(); break; - case GameState.GameOver: DoGameOver(); break; - case GameState.Credits: DoCredits(); break; - case GameState.HighScore: DoHighScore(); break; - case GameState.EnterName: DoEnterName(); break; - } - - G.Sprites.Collision(); - G.Sprites.RemoveDead(); - G.Sprites.Draw(_sb); - + _sb.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearClamp); + _sb.Draw(_virtualScreen, new Rectangle(0, 0, + GraphicsDevice.PresentationParameters.BackBufferWidth, + GraphicsDevice.PresentationParameters.BackBufferHeight), Color.White); _sb.End(); + base.Draw(gameTime); if (ShotFile != null && --ShotFrames <= 0) @@ -151,14 +180,9 @@ public class Game1 : Game void SaveScreenshot(string file) { - int w = GraphicsDevice.PresentationParameters.BackBufferWidth; - int h = GraphicsDevice.PresentationParameters.BackBufferHeight; - var data = new Color[w * h]; - GraphicsDevice.GetBackBufferData(data); - using var tex = new Texture2D(GraphicsDevice, w, h); - tex.SetData(data); + // capture the virtual screen so debug shots are always 800x600 using var fs = File.Create(file); - tex.SaveAsPng(fs, w, h); + _virtualScreen.SaveAsPng(fs, _virtualScreen.Width, _virtualScreen.Height); } // ================================================================ helpers diff --git a/FreedomFighter/OmegaImage.cs b/FreedomFighter/OmegaImage.cs index 0c9009b..3ed3cd9 100644 --- a/FreedomFighter/OmegaImage.cs +++ b/FreedomFighter/OmegaImage.cs @@ -64,8 +64,10 @@ public class OmegaImage } /// - /// Full Omega draw. X,Y is the top-left of the pattern; rotation (degrees, clockwise) - /// pivots around (centerX, centerY) as a fraction of the pattern size. + /// Full Omega draw. X,Y is the top-left of the unscaled pattern; rotation and scale + /// are applied around the pivot (centerX, centerY), which stays at its unscaled + /// position — matching DirectX-era Draw semantics (a half-scaled 64px sprite at X + /// with center 0.5 still has its pivot at X+32). /// public void Draw(SpriteBatch sb, float x, float y, float rotation = 0, float centerX = 0, float centerY = 0, float scaleX = 1, float scaleY = 1, @@ -73,7 +75,7 @@ public class OmegaImage { var src = PatternRect(pattern); var origin = new Vector2(centerX * TileWidth, centerY * TileHeight); - var pos = new Vector2(x + origin.X * scaleX, y + origin.Y * scaleY); + var pos = new Vector2(x + origin.X, y + origin.Y); var color = new Color(red, green, blue, alpha); sb.Draw(Texture, pos, src, color, MathHelper.ToRadians(rotation), origin, new Vector2(scaleX, scaleY), SpriteEffects.None, 0); diff --git a/FreedomFighter/Sprites.cs b/FreedomFighter/Sprites.cs index cf81620..ed8cd74 100644 --- a/FreedomFighter/Sprites.cs +++ b/FreedomFighter/Sprites.cs @@ -112,8 +112,9 @@ public class SpriteManager public void Draw(SpriteBatch sb) { - int n = Items.Count; - for (int i = 0; i < n; i++) + // Omega drew newest sprites first, so earlier-created sprites appear on top + // (e.g. the missile turret gun is spawned before its base and must cover it) + for (int i = Items.Count - 1; i >= 0; i--) if (!Items[i].IsDead) Items[i].Draw(sb); }