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);
}