Minor bug fixes.
@@ -55,6 +55,8 @@ public static class Program
|
|||||||
string? currentList = null;
|
string? currentList = null;
|
||||||
string itemName = "";
|
string itemName = "";
|
||||||
int tileW = 0, tileH = 0;
|
int tileW = 0, tileH = 0;
|
||||||
|
bool transparent = false;
|
||||||
|
string transparentColor = "clBlack";
|
||||||
StringBuilder? hex = null;
|
StringBuilder? hex = null;
|
||||||
byte[]? pictureData = null;
|
byte[]? pictureData = null;
|
||||||
int itemIndex = 0;
|
int itemIndex = 0;
|
||||||
@@ -64,7 +66,8 @@ public static class Program
|
|||||||
if (currentList == null || pictureData == null) return;
|
if (currentList == null || pictureData == null) return;
|
||||||
string safeName = itemName.Length > 0 ? itemName : "unnamed";
|
string safeName = itemName.Length > 0 ? itemName : "unnamed";
|
||||||
string file = $"{currentList.ToLowerInvariant()}_{itemIndex:D2}_{safeName}.png";
|
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, []);
|
manifest.TryAdd(currentList, []);
|
||||||
// TileWidth/TileHeight of 0 means "whole image is one frame"
|
// TileWidth/TileHeight of 0 means "whole image is one frame"
|
||||||
manifest[currentList].Add(new ImageItem(itemIndex, itemName, file, tileW, tileH));
|
manifest[currentList].Add(new ImageItem(itemIndex, itemName, file, tileW, tileH));
|
||||||
@@ -73,6 +76,8 @@ public static class Program
|
|||||||
pictureData = null;
|
pictureData = null;
|
||||||
itemName = "";
|
itemName = "";
|
||||||
tileW = tileH = 0;
|
tileW = tileH = 0;
|
||||||
|
transparent = false;
|
||||||
|
transparentColor = "clBlack";
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var raw in lines)
|
foreach (var raw in lines)
|
||||||
@@ -116,6 +121,10 @@ public static class Program
|
|||||||
tileW = int.Parse(line["TileWidth = ".Length..]);
|
tileW = int.Parse(line["TileWidth = ".Length..]);
|
||||||
else if (line.StartsWith("TileHeight = "))
|
else if (line.StartsWith("TileHeight = "))
|
||||||
tileH = int.Parse(line["TileHeight = ".Length..]);
|
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 = {"))
|
else if (line.StartsWith("Picture.Data = {"))
|
||||||
hex = new StringBuilder(line["Picture.Data = {".Length..]);
|
hex = new StringBuilder(line["Picture.Data = {".Length..]);
|
||||||
}
|
}
|
||||||
@@ -126,8 +135,27 @@ public static class Program
|
|||||||
Console.WriteLine(" wrote gfx/manifest.json");
|
Console.WriteLine(" wrote gfx/manifest.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Delphi TColor: 0x00BBGGRR integer or a clXxx name.</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Delphi TPicture blob: [1B class-name length][class name][payload].</summary>
|
/// <summary>Delphi TPicture blob: [1B class-name length][class name][payload].</summary>
|
||||||
static void SaveGraphic(byte[] data, string outPath)
|
static void SaveGraphic(byte[] data, string outPath, Color? colorKey)
|
||||||
{
|
{
|
||||||
int nameLen = data[0];
|
int nameLen = data[0];
|
||||||
string cls = Encoding.ASCII.GetString(data, 1, nameLen);
|
string cls = Encoding.ASCII.GetString(data, 1, nameLen);
|
||||||
@@ -135,14 +163,21 @@ public static class Program
|
|||||||
|
|
||||||
if (cls == "TPNGObject")
|
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")
|
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);
|
int len = BitConverter.ToInt32(data, payloadStart);
|
||||||
var bmpBytes = new ReadOnlySpan<byte>(data, payloadStart + 4, len);
|
var bmpBytes = new ReadOnlySpan<byte>(data, payloadStart + 4, len).ToArray();
|
||||||
SaveBmpAsKeyedPng(bmpBytes.ToArray(), outPath);
|
using var ms = new MemoryStream(bmpBytes);
|
||||||
|
using var src = new Bitmap(ms);
|
||||||
|
SaveKeyed(src, outPath, colorKey);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -150,6 +185,27 @@ public static class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Re-encode as PNG with the given color made fully transparent (Omega's Transparent=True).</summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Convert BMP to PNG, keying out the bottom-left pixel color (Delphi Transparent convention).</summary>
|
/// <summary>Convert BMP to PNG, keying out the bottom-left pixel color (Delphi Transparent convention).</summary>
|
||||||
static void SaveBmpAsKeyedPng(byte[] bmpBytes, string outPath)
|
static void SaveBmpAsKeyedPng(byte[] bmpBytes, string outPath)
|
||||||
{
|
{
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 559 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 239 B |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 423 B |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 167 B After Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 126 B After Width: | Height: | Size: 126 B |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 270 B |
|
Before Width: | Height: | Size: 234 B After Width: | Height: | Size: 233 B |
@@ -17,6 +17,7 @@ public class Game1 : Game
|
|||||||
SpriteBatch _sb = null!;
|
SpriteBatch _sb = null!;
|
||||||
SpriteFont _font12 = null!;
|
SpriteFont _font12 = null!;
|
||||||
SpriteFont _font20 = null!;
|
SpriteFont _font20 = null!;
|
||||||
|
RenderTarget2D _virtualScreen = null!; // fixed 800x600 scene, stretched to the window
|
||||||
|
|
||||||
bool _paused;
|
bool _paused;
|
||||||
string _enteredName = "";
|
string _enteredName = "";
|
||||||
@@ -44,6 +45,18 @@ public class Game1 : Game
|
|||||||
IsFixedTimeStep = true;
|
IsFixedTimeStep = true;
|
||||||
TargetElapsedTime = TimeSpan.FromSeconds(1 / 120.0);
|
TargetElapsedTime = TimeSpan.FromSeconds(1 / 120.0);
|
||||||
Window.Title = "Freedom Fighter";
|
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()
|
protected override void LoadContent()
|
||||||
@@ -51,6 +64,13 @@ public class Game1 : Game
|
|||||||
_sb = new SpriteBatch(GraphicsDevice);
|
_sb = new SpriteBatch(GraphicsDevice);
|
||||||
_font12 = Content.Load<SpriteFont>("Verdana12");
|
_font12 = Content.Load<SpriteFont>("Verdana12");
|
||||||
_font20 = Content.Load<SpriteFont>("Verdana20");
|
_font20 = Content.Load<SpriteFont>("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");
|
string contentDir = Path.Combine(AppContext.BaseDirectory, "Content");
|
||||||
|
|
||||||
@@ -109,36 +129,45 @@ public class Game1 : Game
|
|||||||
|
|
||||||
if (G.Input.PauseClicked)
|
if (G.Input.PauseClicked)
|
||||||
_paused = !_paused;
|
_paused = !_paused;
|
||||||
if (_paused)
|
|
||||||
|
if (!_paused)
|
||||||
{
|
{
|
||||||
base.Draw(gameTime);
|
GraphicsDevice.SetRenderTarget(_virtualScreen);
|
||||||
return;
|
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);
|
GraphicsDevice.Clear(Color.Black);
|
||||||
_sb.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp);
|
_sb.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearClamp);
|
||||||
|
_sb.Draw(_virtualScreen, new Rectangle(0, 0,
|
||||||
G.Sprites.Move(0);
|
GraphicsDevice.PresentationParameters.BackBufferWidth,
|
||||||
|
GraphicsDevice.PresentationParameters.BackBufferHeight), Color.White);
|
||||||
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();
|
_sb.End();
|
||||||
|
|
||||||
base.Draw(gameTime);
|
base.Draw(gameTime);
|
||||||
|
|
||||||
if (ShotFile != null && --ShotFrames <= 0)
|
if (ShotFile != null && --ShotFrames <= 0)
|
||||||
@@ -151,14 +180,9 @@ public class Game1 : Game
|
|||||||
|
|
||||||
void SaveScreenshot(string file)
|
void SaveScreenshot(string file)
|
||||||
{
|
{
|
||||||
int w = GraphicsDevice.PresentationParameters.BackBufferWidth;
|
// capture the virtual screen so debug shots are always 800x600
|
||||||
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);
|
|
||||||
using var fs = File.Create(file);
|
using var fs = File.Create(file);
|
||||||
tex.SaveAsPng(fs, w, h);
|
_virtualScreen.SaveAsPng(fs, _virtualScreen.Width, _virtualScreen.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================ helpers
|
// ================================================================ helpers
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ public class OmegaImage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full Omega draw. X,Y is the top-left of the pattern; rotation (degrees, clockwise)
|
/// Full Omega draw. X,Y is the top-left of the unscaled pattern; rotation and scale
|
||||||
/// pivots around (centerX, centerY) as a fraction of the pattern size.
|
/// 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).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Draw(SpriteBatch sb, float x, float y, float rotation = 0,
|
public void Draw(SpriteBatch sb, float x, float y, float rotation = 0,
|
||||||
float centerX = 0, float centerY = 0, float scaleX = 1, float scaleY = 1,
|
float centerX = 0, float centerY = 0, float scaleX = 1, float scaleY = 1,
|
||||||
@@ -73,7 +75,7 @@ public class OmegaImage
|
|||||||
{
|
{
|
||||||
var src = PatternRect(pattern);
|
var src = PatternRect(pattern);
|
||||||
var origin = new Vector2(centerX * TileWidth, centerY * TileHeight);
|
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);
|
var color = new Color(red, green, blue, alpha);
|
||||||
sb.Draw(Texture, pos, src, color, MathHelper.ToRadians(rotation), origin,
|
sb.Draw(Texture, pos, src, color, MathHelper.ToRadians(rotation), origin,
|
||||||
new Vector2(scaleX, scaleY), SpriteEffects.None, 0);
|
new Vector2(scaleX, scaleY), SpriteEffects.None, 0);
|
||||||
|
|||||||
@@ -112,8 +112,9 @@ public class SpriteManager
|
|||||||
|
|
||||||
public void Draw(SpriteBatch sb)
|
public void Draw(SpriteBatch sb)
|
||||||
{
|
{
|
||||||
int n = Items.Count;
|
// Omega drew newest sprites first, so earlier-created sprites appear on top
|
||||||
for (int i = 0; i < n; i++)
|
// (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)
|
if (!Items[i].IsDead)
|
||||||
Items[i].Draw(sb);
|
Items[i].Draw(sb);
|
||||||
}
|
}
|
||||||
|
|||||||