Add project files.

This commit is contained in:
2026-09-06 18:01:31 -04:00
parent 0142942887
commit ebcfd9cd39
93 changed files with 3332 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="8.0.*" />
</ItemGroup>
</Project>

234
AssetExtractor/Lzh.cs Normal file
View File

@@ -0,0 +1,234 @@
namespace AssetExtractor;
/// <summary>
/// Decoder for the LZHUF format (LZSS + adaptive Huffman, Okumura/Yoshizaki 1988),
/// as written by the Delphi TLZRW1 component in "Good" mode.
/// Stream layout: [4B magic "LZH!" = 0x4C5A4821 LE][4B LE original size][LZHUF bitstream].
/// </summary>
public static class Lzh
{
public const uint LzhMagic = (((((uint)'L' << 8) + 'Z') << 8) + 'H' << 8) + '!';
const int N = 4096; // ring buffer size
const int F = 60; // lookahead buffer size
const int Threshold = 2;
const int NChar = 256 - Threshold + F; // 314 character codes
const int T = NChar * 2 - 1; // 627, size of freq table
const int R = T - 1; // 626, root position
const int MaxFreq = 0x8000;
// Tables for decoding the upper 6 bits of the match position
static readonly byte[] DCode = BuildDCode();
static readonly byte[] DLen = BuildDLen();
static byte[] BuildDCode()
{
// Canonical LZHUF d_code table: value 0 ×32; 1..3 ×16; 4..11 ×8; 12..23 ×4; 24..47 ×2; 48..63 ×1
var t = new byte[256];
int i = 0, v = 0;
foreach (var (values, run) in new[] { (1, 32), (3, 16), (8, 8), (12, 4), (24, 2), (16, 1) })
for (int g = 0; g < values; g++, v++)
for (int k = 0; k < run; k++)
t[i++] = (byte)v;
return t;
}
static byte[] BuildDLen()
{
// Canonical LZHUF d_len table: 3 ×32, 4 ×48, 5 ×64, 6 ×48, 7 ×48, 8 ×16
var t = new byte[256];
int i = 0;
foreach (var (count, val) in new[] { (32, 3), (48, 4), (64, 5), (48, 6), (48, 7), (16, 8) })
for (int k = 0; k < count; k++)
t[i++] = (byte)val;
return t;
}
public static byte[] Decompress(byte[] file)
{
if (file.Length < 8 || BitConverter.ToUInt32(file, 0) != LzhMagic)
throw new InvalidDataException("Not an LZH!-compressed TLZRW1 stream.");
int origSize = BitConverter.ToInt32(file, 4);
return Decode(file, 8, origSize);
}
static byte[] Decode(byte[] src, int srcOffset, int origSize)
{
var output = new byte[origSize];
int outPos = 0;
// adaptive Huffman state
var freq = new int[T + 1];
var prnt = new int[T + NChar];
var son = new int[T];
// init tree
for (int i = 0; i < NChar; i++)
{
freq[i] = 1;
son[i] = i + T;
prnt[i + T] = i;
}
for (int i = 0, j = NChar; j <= R; i += 2, j++)
{
freq[j] = freq[i] + freq[i + 1];
son[j] = i;
prnt[i] = j;
prnt[i + 1] = j;
}
freq[T] = 0xFFFF;
prnt[R] = 0;
// ring buffer, initialized to spaces
var ring = new byte[N + F - 1];
Array.Fill(ring, (byte)0x20);
int r = N - F;
// bit reader
int srcPos = srcOffset;
uint getbuf = 0;
int getlen = 0;
int GetBit()
{
while (getlen <= 8)
{
uint b = srcPos < src.Length ? src[srcPos++] : 0u;
getbuf |= b << (8 - getlen);
getlen += 8;
}
int result = (int)((getbuf >> 15) & 1);
getbuf = (getbuf << 1) & 0xFFFF;
getlen--;
return result;
}
int GetByte()
{
while (getlen <= 8)
{
uint b = srcPos < src.Length ? src[srcPos++] : 0u;
getbuf |= b << (8 - getlen);
getlen += 8;
}
int result = (int)((getbuf >> 8) & 0xFF);
getbuf = (getbuf << 8) & 0xFFFF;
getlen -= 8;
return result;
}
void Update(int c)
{
if (freq[R] == MaxFreq)
Reconstruct();
c = prnt[c + T];
do
{
int k = ++freq[c];
// if order is disturbed, swap nodes
int l = c + 1;
if (k > freq[l])
{
while (k > freq[l + 1]) l++;
freq[c] = freq[l];
freq[l] = k;
int i = son[c];
prnt[i] = l;
if (i < T) prnt[i + 1] = l;
int j = son[l];
son[l] = i;
prnt[j] = c;
if (j < T) prnt[j + 1] = c;
son[c] = j;
c = l;
}
c = prnt[c];
} while (c != 0);
}
void Reconstruct()
{
// collect leaf nodes, halve frequencies
int j = 0;
for (int i = 0; i < T; i++)
{
if (son[i] >= T)
{
freq[j] = (freq[i] + 1) / 2;
son[j] = son[i];
j++;
}
}
// rebuild internal nodes
for (int i = 0, k = NChar; k < T; i += 2, k++)
{
int f = freq[i] + freq[i + 1];
int l = k;
while (f < freq[l - 1]) l--;
Array.Copy(freq, l, freq, l + 1, k - l);
freq[l] = f;
Array.Copy(son, l, son, l + 1, k - l);
son[l] = i;
}
// reconnect parent pointers
for (int i = 0; i < T; i++)
{
int k = son[i];
prnt[k] = i;
if (k < T) prnt[k + 1] = i;
}
}
int DecodeChar()
{
int c = son[R];
while (c < T)
c = son[c + GetBit()];
c -= T;
Update(c);
return c;
}
int DecodePosition()
{
// upper 6 bits from table
int i = GetByte();
int c = DCode[i] << 6;
int j = DLen[i] - 2;
// read lower 6 bits verbatim
while (j-- > 0)
i = (i << 1) + GetBit();
return c | (i & 0x3F);
}
while (outPos < origSize)
{
int c = DecodeChar();
if (c < 256)
{
output[outPos++] = (byte)c;
ring[r] = (byte)c;
r = (r + 1) & (N - 1);
}
else
{
int pos = (r - DecodePosition() - 1) & (N - 1);
int len = c - 255 + Threshold;
for (int k = 0; k < len && outPos < origSize; k++)
{
byte b = ring[(pos + k) & (N - 1)];
output[outPos++] = b;
ring[r] = b;
r = (r + 1) & (N - 1);
}
}
}
return output;
}
}

360
AssetExtractor/Program.cs Normal file
View File

@@ -0,0 +1,360 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Text.Json;
namespace AssetExtractor;
/// <summary>
/// One-time migration tool: pulls sprites out of the Delphi DFM, converts .omap files
/// (TLZRW1 "LZH!" compressed text) to JSON, extracts tileset images from .oil files,
/// and copies the sound/music files into the MonoGame Content folder.
/// </summary>
public static class Program
{
static string _srcDir = @"C:\Projects\Games\Delphi\Omega Games\Freedom Fighter";
static string _outDir = @"C:\Projects\Games\Visual Studio\FreedomFighter\FreedomFighter\Content";
static readonly string[] Maps = ["intro.omap", "test10.omap", "test7.omap", "test8.omap", "bonus.omap", "lvl5.omap"];
public static void Main(string[] args)
{
if (args.Length > 0) _srcDir = args[0];
if (args.Length > 1) _outDir = args[1];
foreach (var sub in new[] { "gfx", "maps", "tilesets", "sfx", "music" })
Directory.CreateDirectory(Path.Combine(_outDir, sub));
var oilsNeeded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
ExtractDfmImages(Path.Combine(_srcDir, "GameU.dfm"));
foreach (var map in Maps)
{
var oil = ConvertMap(Path.Combine(_srcDir, map));
if (oil != null) oilsNeeded.Add(oil);
}
foreach (var oil in oilsNeeded)
ExtractOilTileset(Path.Combine(_srcDir, oil));
CopyAudio();
Console.WriteLine("Done.");
}
// ---------------------------------------------------------------- DFM images
record ImageItem(int Index, string Name, string File, int TileWidth, int TileHeight);
static void ExtractDfmImages(string dfmPath)
{
var lines = File.ReadAllLines(dfmPath);
var manifest = new Dictionary<string, List<ImageItem>>();
string? currentList = null;
string itemName = "";
int tileW = 0, tileH = 0;
StringBuilder? hex = null;
byte[]? pictureData = null;
int itemIndex = 0;
void FlushItem()
{
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));
manifest.TryAdd(currentList, []);
// TileWidth/TileHeight of 0 means "whole image is one frame"
manifest[currentList].Add(new ImageItem(itemIndex, itemName, file, tileW, tileH));
Console.WriteLine($" {currentList}[{itemIndex}] '{itemName}' -> {file} ({tileW}x{tileH})");
itemIndex++;
pictureData = null;
itemName = "";
tileW = tileH = 0;
}
foreach (var raw in lines)
{
var line = raw.Trim();
if (hex != null)
{
bool end = line.EndsWith('}');
hex.Append(end ? line[..^1] : line);
if (end)
{
pictureData = Convert.FromHexString(hex.ToString());
hex = null;
}
continue;
}
if (line.StartsWith("object ") && line.Contains(": TOmegaImageList"))
{
FlushItem();
currentList = line["object ".Length..line.IndexOf(':')].Trim();
itemIndex = 0;
continue;
}
if (currentList == null) continue;
if (line.StartsWith("object ")) // next non-imagelist component
{
FlushItem();
currentList = null;
continue;
}
if (line == "item")
FlushItem();
else if (line.StartsWith("Name = '"))
itemName = line["Name = '".Length..^1];
else if (line.StartsWith("TileWidth = "))
tileW = int.Parse(line["TileWidth = ".Length..]);
else if (line.StartsWith("TileHeight = "))
tileH = int.Parse(line["TileHeight = ".Length..]);
else if (line.StartsWith("Picture.Data = {"))
hex = new StringBuilder(line["Picture.Data = {".Length..]);
}
FlushItem();
var json = JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(Path.Combine(_outDir, "gfx", "manifest.json"), json);
Console.WriteLine(" wrote gfx/manifest.json");
}
/// <summary>Delphi TPicture blob: [1B class-name length][class name][payload].</summary>
static void SaveGraphic(byte[] data, string outPath)
{
int nameLen = data[0];
string cls = Encoding.ASCII.GetString(data, 1, nameLen);
int payloadStart = 1 + nameLen;
if (cls == "TPNGObject")
{
File.WriteAllBytes(outPath, data[payloadStart..]);
}
else if (cls == "TBitmap")
{
// payload: [4B length][BMP bytes]
int len = BitConverter.ToInt32(data, payloadStart);
var bmpBytes = new ReadOnlySpan<byte>(data, payloadStart + 4, len);
SaveBmpAsKeyedPng(bmpBytes.ToArray(), outPath);
}
else
{
throw new InvalidDataException($"Unknown graphic class '{cls}'");
}
}
/// <summary>Convert BMP to PNG, keying out the bottom-left pixel color (Delphi Transparent convention).</summary>
static void SaveBmpAsKeyedPng(byte[] bmpBytes, string outPath)
{
using var ms = new MemoryStream(bmpBytes);
using var src = new Bitmap(ms);
using var dst = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb);
var key = src.GetPixel(0, src.Height - 1);
for (int y = 0; y < src.Height; y++)
for (int x = 0; x < src.Width; x++)
{
var c = src.GetPixel(x, y);
dst.SetPixel(x, y, c.R == key.R && c.G == key.G && c.B == key.B ? Color.Transparent : c);
}
dst.Save(outPath, ImageFormat.Png);
}
// ---------------------------------------------------------------- maps
static string? ConvertMap(string mapPath)
{
if (!File.Exists(mapPath))
{
Console.WriteLine($" MISSING map {mapPath}");
return null;
}
var text = Encoding.Latin1.GetString(Lzh.Decompress(File.ReadAllBytes(mapPath)));
var lines = text.Split("\r\n");
int p = 0;
string Next() => lines[p++];
int NextInt() => int.Parse(Next());
int w = NextInt();
int h = NextInt();
string oil = Next();
int tileW = NextInt();
int tileH = NextInt();
// Layer1: 17 lines per tile; keep only the pattern index (game uses nothing else)
var layer1 = new int[w][];
for (int a = 0; a < w; a++)
{
layer1[a] = new int[h];
for (int b = 0; b < h; b++)
{
p += 8; // x,y,r,g,b,alpha,rotation,image.index
layer1[a][b] = NextInt(); // imageindex (pattern)
p += 8; // animcount..animlooped
}
}
p += w * h * 18; // Layer2 (has extra DoCollision line)
p += w * h * 17; // Layer3
var triggers = new List<object>();
for (int a = 0; a < w; a++)
for (int b = 0; b < h; b++)
{
string name = Next();
string action = Next();
string e1 = Next(), e2 = Next(), e3 = Next();
if (action.Length > 0)
triggers.Add(new { x = a, y = b, name, action, extra1 = e1, extra2 = e2, extra3 = e3 });
}
var mapObj = new
{
widthCount = w,
heightCount = h,
oil = Path.GetFileName(oil),
tileWidth = tileW,
tileHeight = tileH,
layer1,
triggers
};
string outFile = Path.Combine(_outDir, "maps", Path.GetFileNameWithoutExtension(mapPath) + ".json");
File.WriteAllText(outFile, JsonSerializer.Serialize(mapObj));
Console.WriteLine($" {Path.GetFileName(mapPath)}: {w}x{h}, oil='{oil}', {triggers.Count} triggers -> {Path.GetFileName(outFile)}");
return oil;
}
// ---------------------------------------------------------------- OIL tilesets
static readonly byte[] PngSig = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
/// <summary>
/// .oil = binary Delphi component stream. Rather than fully parsing TPF0 we scan for
/// embedded graphics: "TPNGObject"+PNG or "TBitmap"+[len]+BMP. Item order is preserved;
/// the game's maps only use item 0 (the tileset).
/// </summary>
static void ExtractOilTileset(string oilPath)
{
if (!File.Exists(oilPath))
{
Console.WriteLine($" MISSING oil {oilPath}");
return;
}
var data = File.ReadAllBytes(oilPath);
string baseName = Path.GetFileNameWithoutExtension(oilPath).ToLowerInvariant();
int found = 0;
for (int i = 0; i < data.Length - 10; i++)
{
if (data[i] == 0x0A && Matches(data, i + 1, "TPNGObject"))
{
int start = i + 11;
if (start + 8 < data.Length && data.AsSpan(start, 8).SequenceEqual(PngSig))
{
int end = FindPngEnd(data, start);
if (end > 0)
{
Save(data[start..end], found);
found++;
i = end - 1;
}
}
}
else if (data[i] == 0x07 && Matches(data, i + 1, "TBitmap"))
{
int lenPos = i + 8;
if (lenPos + 4 >= data.Length) continue;
int len = BitConverter.ToInt32(data, lenPos);
int start = lenPos + 4;
if (len > 14 && start + len <= data.Length && data[start] == 'B' && data[start + 1] == 'M')
{
string file = Path.Combine(_outDir, "tilesets", $"{baseName}_{found:D2}.png");
SaveBmpAsKeyedPng(data[start..(start + len)], file);
Console.WriteLine($" {Path.GetFileName(oilPath)}[{found}] (bmp) -> {Path.GetFileName(file)}");
found++;
i = start + len - 1;
}
}
}
Console.WriteLine($" {Path.GetFileName(oilPath)}: {found} images extracted");
void Save(byte[] png, int idx)
{
string file = Path.Combine(_outDir, "tilesets", $"{baseName}_{idx:D2}.png");
File.WriteAllBytes(file, png);
Console.WriteLine($" {Path.GetFileName(oilPath)}[{idx}] (png) -> {Path.GetFileName(file)}");
}
}
static bool Matches(byte[] data, int pos, string s)
{
if (pos + s.Length > data.Length) return false;
for (int i = 0; i < s.Length; i++)
if (data[pos + i] != (byte)s[i]) return false;
return true;
}
static int FindPngEnd(byte[] data, int start)
{
// walk PNG chunks: [4B len BE][4B type][data][4B crc] until IEND
int p = start + 8;
while (p + 12 <= data.Length)
{
int len = (data[p] << 24) | (data[p + 1] << 16) | (data[p + 2] << 8) | data[p + 3];
string type = Encoding.ASCII.GetString(data, p + 4, 4);
p += 12 + len;
if (type == "IEND") return p;
}
return -1;
}
// ---------------------------------------------------------------- audio
static void CopyAudio()
{
// Files referenced by OmegaMusic1.Songs in GameU.dfm.
// 'Level test.mp3' (index 1, level2 music) is missing from the source folder;
// Track06.mp3 is copied as a substitute.
(string src, string sub)[] files =
[
(@"Music\white light.mp3", "music"),
(@"Music\G2 Final.mp3", "music"),
(@"Music\Nazi_Rap.mid", "music"),
(@"Music\Main Menu 2.mp3", "music"),
(@"Music\G8 Final.mp3", "music"),
(@"Music\Thunderization.mp3", "music"),
(@"Music\Intro.mp3", "music"),
(@"Music\Track06.mp3", "music"),
(@"Sounds\fire1.wav", "sfx"),
(@"Sounds\reload.wav", "sfx"),
(@"Sounds\thump1.wav", "sfx"),
(@"Sounds\beep.wav", "sfx"),
(@"Sounds\hit4.wav", "sfx"),
(@"Sounds\explode2.wav", "sfx"),
(@"Sounds\missile.wav", "sfx"),
];
foreach (var (src, sub) in files)
{
string from = Path.Combine(_srcDir, src);
if (!File.Exists(from))
{
Console.WriteLine($" MISSING audio {src}");
continue;
}
string to = Path.Combine(_outDir, sub, Path.GetFileName(src));
File.Copy(from, to, overwrite: true);
}
Console.WriteLine(" audio copied");
}
}

4
FreedomFighter.slnx Normal file
View File

@@ -0,0 +1,4 @@
<Solution>
<Project Path="AssetExtractor/AssetExtractor.csproj" />
<Project Path="FreedomFighter/FreedomFighter.csproj" />
</Solution>

View File

@@ -0,0 +1,36 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-mgcb": {
"version": "3.8.4",
"commands": [
"mgcb"
]
},
"dotnet-mgcb-editor": {
"version": "3.8.4",
"commands": [
"mgcb-editor"
]
},
"dotnet-mgcb-editor-linux": {
"version": "3.8.4",
"commands": [
"mgcb-editor-linux"
]
},
"dotnet-mgcb-editor-windows": {
"version": "3.8.4",
"commands": [
"mgcb-editor-windows"
]
},
"dotnet-mgcb-editor-mac": {
"version": "3.8.4",
"commands": [
"mgcb-editor-mac"
]
}
}
}

14
FreedomFighter/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: FreedomFighter Debug",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/FreedomFighter.csproj"
}
],
}

View File

@@ -0,0 +1,29 @@
#----------------------------- Global Properties ----------------------------#
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:Windows
/config:
/profile:Reach
/compress:False
#-------------------------------- References --------------------------------#
#---------------------------------- Content ---------------------------------#
#begin Verdana12.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=False
/processorParam:TextureFormat=Color
/build:Verdana12.spritefont
#begin Verdana20.spritefont
/importer:FontDescriptionImporter
/processor:FontDescriptionProcessor
/processorParam:PremultiplyAlpha=False
/processorParam:TextureFormat=Color
/build:Verdana20.spritefont

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<FontName>Verdana</FontName>
<Size>16</Size>
<Spacing>0</Spacing>
<UseKerning>true</UseKerning>
<Style>Bold</Style>
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<FontName>Verdana</FontName>
<Size>27</Size>
<Spacing>0</Spacing>
<UseKerning>true</UseKerning>
<Style>Bold</Style>
<CharacterRegions>
<CharacterRegion>
<Start>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>

View File

@@ -0,0 +1,278 @@
{
"OmegaImageList1": [
{
"Index": 0,
"Name": "player",
"File": "omegaimagelist1_00_player.png",
"TileWidth": 64,
"TileHeight": 64
},
{
"Index": 1,
"Name": "enemy",
"File": "omegaimagelist1_01_enemy.png",
"TileWidth": 34,
"TileHeight": 34
},
{
"Index": 2,
"Name": "bullet",
"File": "omegaimagelist1_02_bullet.png",
"TileWidth": 34,
"TileHeight": 34
},
{
"Index": 3,
"Name": "explosion",
"File": "omegaimagelist1_03_explosion.png",
"TileWidth": 32,
"TileHeight": 34
},
{
"Index": 4,
"Name": "water",
"File": "omegaimagelist1_04_water.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 5,
"Name": "lives",
"File": "omegaimagelist1_05_lives.png",
"TileWidth": 34,
"TileHeight": 34
},
{
"Index": 6,
"Name": "score",
"File": "omegaimagelist1_06_score.png",
"TileWidth": 69,
"TileHeight": 20
},
{
"Index": 7,
"Name": "enemy2",
"File": "omegaimagelist1_07_enemy2.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 8,
"Name": "enemybullet",
"File": "omegaimagelist1_08_enemybullet.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 9,
"Name": "boss",
"File": "omegaimagelist1_09_boss.png",
"TileWidth": 98,
"TileHeight": 98
},
{
"Index": 10,
"Name": "sub",
"File": "omegaimagelist1_10_sub.png",
"TileWidth": 32,
"TileHeight": 98
},
{
"Index": 11,
"Name": "bigexplosion",
"File": "omegaimagelist1_11_bigexplosion.png",
"TileWidth": 65,
"TileHeight": 65
},
{
"Index": 12,
"Name": "turret",
"File": "omegaimagelist1_12_turret.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 13,
"Name": "boss2",
"File": "omegaimagelist1_13_boss2.png",
"TileWidth": 161,
"TileHeight": 142
},
{
"Index": 14,
"Name": "cruiser",
"File": "omegaimagelist1_14_cruiser.png",
"TileWidth": 41,
"TileHeight": 197
},
{
"Index": 15,
"Name": "tactical",
"File": "omegaimagelist1_15_tactical.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 16,
"Name": "player2",
"File": "omegaimagelist1_16_player2.png",
"TileWidth": 73,
"TileHeight": 45
},
{
"Index": 17,
"Name": "mine",
"File": "omegaimagelist1_17_mine.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 18,
"Name": "missile_turret",
"File": "omegaimagelist1_18_missile_turret.png",
"TileWidth": 64,
"TileHeight": 64
},
{
"Index": 19,
"Name": "missile",
"File": "omegaimagelist1_19_missile.png",
"TileWidth": 16,
"TileHeight": 16
},
{
"Index": 20,
"Name": "scanner",
"File": "omegaimagelist1_20_scanner.png",
"TileWidth": 800,
"TileHeight": 32
}
],
"OmegaImageList2": [
{
"Index": 0,
"Name": "numbers",
"File": "omegaimagelist2_00_numbers.png",
"TileWidth": 12,
"TileHeight": 15
},
{
"Index": 1,
"Name": "title",
"File": "omegaimagelist2_01_title.png",
"TileWidth": 278,
"TileHeight": 141
},
{
"Index": 2,
"Name": "gameover",
"File": "omegaimagelist2_02_gameover.png",
"TileWidth": 97,
"TileHeight": 16
},
{
"Index": 3,
"Name": "title2",
"File": "omegaimagelist2_03_title2.png",
"TileWidth": 360,
"TileHeight": 60
},
{
"Index": 4,
"Name": "newgame",
"File": "omegaimagelist2_04_newgame.png",
"TileWidth": 360,
"TileHeight": 60
},
{
"Index": 5,
"Name": "options",
"File": "omegaimagelist2_05_options.png",
"TileWidth": 360,
"TileHeight": 60
},
{
"Index": 6,
"Name": "highscore",
"File": "omegaimagelist2_06_highscore.png",
"TileWidth": 360,
"TileHeight": 60
},
{
"Index": 7,
"Name": "credits",
"File": "omegaimagelist2_07_credits.png",
"TileWidth": 360,
"TileHeight": 60
},
{
"Index": 8,
"Name": "quit",
"File": "omegaimagelist2_08_quit.png",
"TileWidth": 360,
"TileHeight": 60
}
],
"OmegaImageList3": [
{
"Index": 0,
"Name": "bar",
"File": "omegaimagelist3_00_bar.png",
"TileWidth": 130,
"TileHeight": 14
},
{
"Index": 1,
"Name": "health",
"File": "omegaimagelist3_01_health.png",
"TileWidth": 7,
"TileHeight": 10
},
{
"Index": 2,
"Name": "weapon",
"File": "omegaimagelist3_02_weapon.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 3,
"Name": "energy",
"File": "omegaimagelist3_03_energy.png",
"TileWidth": 7,
"TileHeight": 10
}
],
"OmegaImageList4": [
{
"Index": 0,
"Name": "Tiles",
"File": "omegaimagelist4_00_Tiles.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 1,
"Name": "collision",
"File": "omegaimagelist4_01_collision.png",
"TileWidth": 32,
"TileHeight": 32
},
{
"Index": 2,
"Name": "trigger",
"File": "omegaimagelist4_02_trigger.png",
"TileWidth": 32,
"TileHeight": 32
}
],
"OmegaImageList5": [
{
"Index": 0,
"Name": "",
"File": "omegaimagelist5_00_unnamed.png",
"TileWidth": 9,
"TileHeight": 9
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

570
FreedomFighter/Entities.cs Normal file
View File

@@ -0,0 +1,570 @@
using System;
using Microsoft.Xna.Framework.Graphics;
namespace FreedomFighter;
// Entity classes ported 1:1 from GameU.pas, including quirks
// (e.g. TEnemy moves inside Draw, missiles can target stray sprites).
public class Player : Sprite
{
public int Health;
public int FireRate;
public int Power;
public Weapon Weapon;
public Player(SpriteManager m) : base(m) { }
public override void Move(float moveCount)
{
var input = G.Input;
if (input.Button5Clicked) { G.Music.Songs[6].Play(); Weapon = Weapon.Reg; }
if (input.Button6Clicked) { G.Music.Songs[6].Play(); Weapon = Weapon.Super; }
if (input.Button7Clicked) { G.Music.Songs[6].Play(); Weapon = Weapon.Multi; }
if (FireRate > 1)
FireRate--;
if (input.Button4 && FireRate == 1 && Weapon == Weapon.Reg)
Bullet.Spawn(X + Width / 2 - 16, Y - 34, 0, 3, 0);
else if (input.Button4 && FireRate == 1 && Weapon == Weapon.Super)
{
Bullet.Spawn(X + Width / 2 - 16 - 12, Y - 34, 0, 3, 0);
Bullet.Spawn(X + Width / 2 - 16 + 12, Y - 34, 0, 3, 0);
}
if (input.Button4 && FireRate == 1 && Weapon == Weapon.Multi)
{
Bullet.Spawn(X + Width / 2 - 16, Y - 34, 2, 3, -35);
Bullet.Spawn(X + Width / 2 - 16, Y - 34, 0, 3, 0);
Bullet.Spawn(X + Width / 2 - 16, Y - 34, -2, 3, 35);
}
if (input.Button8 && FireRate == 1)
{
G.Music.Songs[13].Play();
Missile.Spawn(X + Width / 2, Y, 2);
FireRate = 20;
}
if (input.Left && input.Up) { X -= 3; Y -= 3; }
else if (input.Left && input.Down) { X -= 3; Y += 3; }
else if (input.Right && input.Up) { X += 3; Y -= 3; }
else if (input.Right && input.Down) { X += 3; Y += 3; }
else if (input.Left) X -= 3;
else if (input.Right) X += 3;
else if (input.Up) Y -= 3;
else if (input.Down) Y += 3;
if (X + Width > 800) X = 800 - Width;
if (X < 0) X = 0;
if (Y + Height > 600) Y = 600 - Height;
if (Y < 0) Y = 0;
}
}
public class DemoShip : Sprite
{
public DemoShip(SpriteManager m) : base(m) { }
}
public class Enemy : Sprite
{
public float YVel;
public int Health;
public string MoveDir = "";
public AI AI;
public bool Charging;
public int HitTimer;
public int FireRate;
public int ExplodeTime;
public int AnimTicks;
public Enemy(SpriteManager m) : base(m) { }
public override void Move(float moveCount)
{
if (HitTimer < 255)
{
HitTimer += 15;
SetBlue(HitTimer);
SetGreen(HitTimer);
}
switch (AI)
{
case AI.Normal:
case AI.Hunt:
if (G.Random(1000) < 5)
EnemyBullet.Spawn(X, Y, 3, 0);
break;
case AI.Tactical:
Y += YVel;
if (!Charging && Y > 200)
{
YVel = 1;
G.TFCounter++;
if (G.TFCounter > 16)
{
G.TFCounter = 0;
if (ImageIndex < 4)
ImageIndex++;
else
{
Charging = true;
YVel = -2;
}
}
}
if (Charging && Y < -100)
{
ImageIndex = 0;
YVel = 5;
}
break;
case AI.Mine:
if (AnimTicks > ExplodeTime)
{
Kill();
Explosion.Spawn(X, Y, false, false);
for (int angle = 360; angle >= 0; angle -= 45)
EnemyBullet.Spawn(X, Y, 2, angle);
}
break;
case AI.Turret:
if (FireRate < 0)
{
EnemyBullet.Spawn(X + 16, Y + 16, 3, Rotation);
FireRate = 40;
}
break;
}
if (Y > G.ScreenHeight)
Kill();
}
public override void OnCollision(Sprite sprite, int colX, int colY)
{
if (sprite is Player)
{
G.Music.Songs[11].Play();
Kill();
G.Player.Health--;
Explosion.Spawn(X, Y, false, false);
}
if (sprite is Bullet)
{
Health--;
sprite.Kill();
if (Health < 1)
{
Explosion.Spawn(X, Y, false, false);
G.Music.Songs[11].Play();
Kill();
G.ShipsKilled++;
if (G.Player.Power < 18) G.Player.Power++;
G.Score += 10;
}
else
{
HitTimer = 0;
G.Music.Songs[10].Volume = 60;
G.Music.Songs[10].Play();
}
}
if (sprite is Missile)
{
Health -= 5;
sprite.Kill();
G.Music.Songs[10].Volume = 60;
G.Music.Songs[10].Play();
if (Health < 1)
{
Explosion.Spawn(X, Y, false, false);
G.Music.Songs[11].Play();
Kill();
G.ShipsKilled++;
if (G.Player.Power < 18) G.Player.Power++;
G.Score += 10;
}
else
HitTimer = 0;
}
}
// The original moved enemies inside Draw; preserved for identical behavior.
public override void Draw(SpriteBatch sb)
{
base.Draw(sb);
switch (AI)
{
case AI.Normal:
case AI.None:
Y += YVel;
break;
case AI.Hunt:
Y += YVel;
if (Y > 200)
{
Y += YVel; // double speed
if (G.Player.X > X) X++;
if (G.Player.X < X) X--;
}
break;
case AI.Mine:
Y += 1.5f;
Rotation += 0.5f;
if (Rotation > 359) Rotation = 0;
AnimTicks++;
break;
case AI.Turret:
Rotation = (float)(180 / Math.PI * Math.Atan2(Y - G.Player.Y, X - G.Player.X)) + 90;
if (Rotation > 360) Rotation -= 360;
else if (Rotation < 0) Rotation += 360;
FireRate--;
Y += YVel;
break;
}
if (Y > G.ScreenHeight)
Kill();
}
}
public class Boss : Sprite
{
public float YVel;
public int Health;
public string MoveDir = "";
public int HitTimer;
public AI AI;
public int Timer;
public int RandX;
public Boss(SpriteManager m) : base(m) { }
public override void Move(float moveCount)
{
if (HitTimer < 255)
{
HitTimer += 15;
SetBlue(HitTimer);
SetGreen(HitTimer);
}
if (AI == AI.Boss)
{
if (G.Random(1000) < 10)
{
EnemyBullet.Spawn(X, Y + 40, 3, 0);
EnemyBullet.Spawn(X + 32, Y + 40, 3, 0);
EnemyBullet.Spawn(X + 64, Y + 40, 3, 0);
}
Y += YVel;
if (Y > 300)
{
Y = 300;
if (MoveDir == "right") X++;
if (MoveDir == "left") X--;
if (X > G.ScreenWidth - 98 && MoveDir == "right")
{
X = G.ScreenWidth - 98;
MoveDir = "left";
}
if (X < 0 && MoveDir == "left")
{
MoveDir = "right";
X = 0;
}
}
}
else if (AI == AI.Boss2)
{
Timer++;
if (Timer > 200)
{
RandX = G.Random(10);
Timer = 0;
}
if (MoveDir == "down" && Timer % 25 == 0)
{
EnemyBullet.Spawn(X + 40, Y + Height, 3, 0);
EnemyBullet.Spawn(X + 50, Y + Height, 3, 0);
EnemyBullet.Spawn(X + 60, Y + Height, 3, 0);
EnemyBullet.Spawn(X + 70, Y + Height, 3, 0);
}
if (RandX > 7) X++;
else if (RandX > 2) { /* hold */ }
else X--;
if (MoveDir == "down") Y++;
if (MoveDir == "up") Y -= 2;
if (Y > 300 && MoveDir == "down") { Y = 300; MoveDir = "up"; }
if (Y < -100 && MoveDir == "up") { MoveDir = "down"; Y = -100; }
if (X > 800 - Width) { X = 800 - Width; RandX = 0; }
if (X < 0) { X = 0; RandX = 8; }
}
}
public override void OnCollision(Sprite sprite, int colX, int colY)
{
if (sprite is Bullet)
{
sprite.Kill();
Health--;
HitTimer = 0;
if (Health == 0)
{
Kill();
Explosion.Spawn(X, Y, true, true);
G.Score += 1000;
}
}
if (sprite is Missile)
{
sprite.Kill();
Health -= 5;
HitTimer = 0;
if (Health < 1)
{
Kill();
Explosion.Spawn(X, Y, true, true);
G.Score += 1000;
}
}
}
}
public class Bullet : Sprite
{
public float XVel, YVel;
public Bullet(SpriteManager m) : base(m) { }
public static void Spawn(float x, float y, float xVel, float yVel, float rotation)
{
var b = new Bullet(G.Sprites)
{
Width = 34,
Height = 34,
X = x,
Y = y,
XVel = xVel,
YVel = yVel,
Rotation = rotation,
Image = G.Images.IL1[2],
ImageIndex = 0,
CenterX = 0.5f,
CenterY = 0.5f,
DoCollision = true,
DoPixelCheck = true,
};
G.Player.FireRate = 20;
}
public override void Move(float moveCount)
{
X -= XVel;
Y -= YVel;
if (Y > 600 || Y < 0 || X > 800 || X < 0)
Kill();
}
}
public class EnemyBullet : Sprite
{
public float XVel, YVel;
public EnemyBullet(SpriteManager m) : base(m) { }
public static void Spawn(float x, float y, float vel, float rotation)
{
double rad = Math.PI / 180 * (rotation + 90);
_ = new EnemyBullet(G.Sprites)
{
Name = "EnemyBullet",
X = x,
Y = y,
Rotation = rotation,
CenterX = 0.5f,
CenterY = 0.5f,
XVel = vel * (float)Math.Cos(rad),
YVel = vel * (float)Math.Sin(rad),
Width = 32,
Height = 32,
Image = G.Images.IL1[8],
ImageIndex = 0,
DoCollision = true,
DoPixelCheck = true,
};
}
public override void Move(float moveCount)
{
X += XVel;
Y += YVel;
if (X > G.ScreenWidth || X < 0 || Y > G.ScreenHeight || Y < 0)
Kill();
}
public override void OnCollision(Sprite sprite, int colX, int colY)
{
if (sprite is Player)
{
Kill();
G.Player.Health--;
}
}
}
public class Missile : Sprite
{
public float XVel, YVel;
public Sprite? Target;
public Missile(SpriteManager m) : base(m) { }
public static void Spawn(float x, float y, float yVel)
{
var missile = new Missile(G.Sprites)
{
Name = "Missile",
X = x,
Y = y,
Rotation = 180,
YVel = yVel,
Width = 16,
Height = 16,
Image = G.Images.IL1[19],
ImageIndex = 0,
DoCollision = true,
DoPixelCheck = true,
};
missile.Target = missile.GetTarget();
}
public Sprite? GetTarget()
{
Sprite? target = null;
float prevDist = 800;
foreach (var s in G.Sprites.Items)
{
float dx = X - s.X;
float dy = Y - s.Y;
float distance = (float)Math.Sqrt(dx * dx + dy * dy);
// faithful to the original: filters by Name only, so unnamed sprites
// (fighters, but also player bullets and explosions) are all fair game
if (distance < prevDist && s.Name != "Player" && s.Name != "EnemyBullet" && s.Name != "Missile")
{
target = s;
prevDist = distance;
}
}
return target;
}
public override void Move(float moveCount)
{
if (Target != null)
{
if (Y > Target.Y)
{
Rotation = (float)(180 / Math.PI * Math.Atan2(Y - Target.Y, X - Target.X)) + 90;
if (Rotation > 360) Rotation -= 360;
else if (Rotation < 0) Rotation += 360;
}
double rad = Math.PI / 180 * (Rotation + 90);
X += (float)Math.Cos(rad);
Y += YVel * (float)Math.Sin(rad);
}
else
{
double rad = Math.PI / 180 * (Rotation + 90);
Y += YVel * (float)Math.Sin(rad);
X += (float)Math.Cos(rad);
Target = GetTarget();
}
if (Y < 0)
Kill();
if (X < 0 || X > G.ScreenWidth)
Kill();
}
}
public class Explosion : Sprite
{
public int MaxFrames;
public int ECounter;
public bool IsBoss;
public Explosion(SpriteManager m) : base(m) { }
public static void Spawn(float x, float y, bool big, bool boss)
{
int frames = big ? 6 : 5;
int imageIdx = big ? 11 : 3;
int tileWidth = big ? 65 : 32;
int tileHeight = big ? 65 : 34;
_ = new Explosion(G.Sprites)
{
X = x,
Y = y,
Width = tileWidth,
Height = tileHeight,
Image = G.Images.IL1[imageIdx],
ImageIndex = 0,
DoCollision = false,
MaxFrames = frames,
ECounter = 0,
IsBoss = boss,
};
}
public override void Move(float moveCount)
{
Y += 1;
ECounter++;
if (ECounter > 6)
{
ImageIndex++;
ECounter = 0;
}
if (ImageIndex > MaxFrames)
{
if (IsBoss)
G.Done = true;
Kill();
}
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.WindowsDX" Version="3.8.*" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.*" />
</ItemGroup>
<ItemGroup>
<None Include="Content\gfx\**;Content\maps\**;Content\tilesets\**;Content\sfx\**;Content\music\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="RestoreDotnetTools" BeforeTargets="CollectPackageReferences">
<Message Text="Restoring dotnet tools (this might take a while depending on your internet speed and should only happen upon building your project for the first time, or after upgrading MonoGame, or clearing your nuget cache)" Importance="High" />
<Exec Command="dotnet tool restore" />
</Target>
</Project>

74
FreedomFighter/G.cs Normal file
View File

@@ -0,0 +1,74 @@
using System;
namespace FreedomFighter;
public enum GameState { Intro, Idle, Intermission, Playing, Stats, Shop, GameOver, Credits, HighScore, EnterName }
public enum Weapon { Reg, Super, Multi }
public enum AI { Normal, Hunt, Tactical, Mine, Turret, Boss, Boss2, None }
public enum ObjectType { Fighter, Kamikaze, Tactical, Mine, MissileBase, MissileTurret, Scanner, Boss, Boss2 }
public struct Camera
{
public int X, Y, Width, Height;
}
/// <summary>
/// The global variables from GameU.pas, kept global to preserve the original's structure.
/// </summary>
public static class G
{
public const int ScreenWidth = 800;
public const int ScreenHeight = 600;
public const string Version = "BETA 5";
public const int MUS_LEVEL1 = 0;
public const int MUS_GAMEOVER = 3;
public const int MUS_TITLE = 4;
public static Game1 Game = null!;
public static SpriteManager Sprites = null!;
public static ImageLists Images = null!;
public static MusicManager Music = null!;
public static GameMap Map = null!;
public static OmegaInput Input = null!;
public static HighScores HighScores = null!;
public static Random Rnd = new();
public static GameState State;
public static bool BossMode;
public static bool Done;
public static Player Player = null!;
public static int Counter;
public static int Change;
public static int CurCount;
public static int Score;
public static int Lives;
public static int ShipsKilled;
public static int Level;
public static Camera Camera;
public static int NewCamX, NewCamY;
public static int TextNum;
public static bool TextCreated;
public static int TFCounter;
public static int CurSelection = 4;
public static int[] MainMenu = new int[5];
public static int Prev = 4;
public static GameText GameTextObj = null!;
public static GameText TitleText = null!;
public static GameText IdText = null!;
public static BitmapFont BitmapFont1 = null!; // numbers
public static BitmapFont BitmapFont2 = null!; // A-Z
public static int Random(int max) => max <= 0 ? 0 : Rnd.Next(max);
}

943
FreedomFighter/Game1.cs Normal file
View File

@@ -0,0 +1,943 @@
using System;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace FreedomFighter;
/// <summary>
/// Port of GameU.pas. The original ran all logic inside the OmegaTimer render
/// callback, so this port keeps that structure: everything happens in Draw(),
/// paced by a 120 Hz fixed timestep (the original's MaxFPS).
/// </summary>
public class Game1 : Game
{
readonly GraphicsDeviceManager _graphics;
SpriteBatch _sb = null!;
SpriteFont _font12 = null!;
SpriteFont _font20 = null!;
bool _paused;
string _enteredName = "";
DemoShip? _demoShip;
// debug screenshot support (--shot)
public string? ShotFile;
public int ShotFrames = 30;
public int StartLevel;
// FPS measurement (the original drew OmegaTimer1.FPS)
int _fps, _fpsFrames;
double _fpsTimer;
public Game1()
{
_graphics = new GraphicsDeviceManager(this)
{
PreferredBackBufferWidth = G.ScreenWidth,
PreferredBackBufferHeight = G.ScreenHeight,
SynchronizeWithVerticalRetrace = false,
};
Content.RootDirectory = "Content";
IsMouseVisible = true;
IsFixedTimeStep = true;
TargetElapsedTime = TimeSpan.FromSeconds(1 / 120.0);
Window.Title = "Freedom Fighter";
}
protected override void LoadContent()
{
_sb = new SpriteBatch(GraphicsDevice);
_font12 = Content.Load<SpriteFont>("Verdana12");
_font20 = Content.Load<SpriteFont>("Verdana20");
string contentDir = Path.Combine(AppContext.BaseDirectory, "Content");
G.Game = this;
G.Sprites = new SpriteManager();
G.Images = new ImageLists(GraphicsDevice, contentDir);
G.Music = new MusicManager();
G.Music.Load(contentDir);
G.Map = new GameMap(GraphicsDevice, contentDir);
G.Input = new OmegaInput();
G.HighScores = new HighScores();
G.HighScores.Load();
// OmegaScreen1Init: bitmap font setup
G.BitmapFont1 = new BitmapFont(G.Images.IL2[0], "0123456789");
G.BitmapFont2 = new BitmapFont(G.Images.IL5[0], "ABCDEFGHIJKLMNOPQRSTUVWXYZ ");
// FormCreate
G.GameTextObj = new GameText("", 60, G.BitmapFont2);
G.BitmapFont2.Red = 0;
G.BitmapFont2.Green = 200;
G.BitmapFont2.Blue = 200;
G.State = GameState.Idle;
G.MainMenu = [0, 0, 0, 0, 1];
G.Prev = 4;
G.Camera.Width = G.ScreenWidth;
G.Camera.Height = G.ScreenHeight;
G.Music.Songs[G.MUS_TITLE].Volume = 25;
G.Music.Songs[G.MUS_TITLE].Play();
if (StartLevel > 0) // debug: jump straight into a level
{
ResetAll();
G.Level = StartLevel;
G.State = GameState.Intermission;
G.Counter = 495;
}
}
protected override void Update(GameTime gameTime)
{
// all logic runs in Draw, matching the original's timer callback
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
_fpsFrames++;
_fpsTimer += gameTime.ElapsedGameTime.TotalSeconds;
if (_fpsTimer >= 1) { _fps = _fpsFrames; _fpsFrames = 0; _fpsTimer -= 1; }
G.Input.Update();
if (G.Input.PauseClicked)
_paused = !_paused;
if (_paused)
{
base.Draw(gameTime);
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();
base.Draw(gameTime);
if (ShotFile != null && --ShotFrames <= 0)
{
SaveScreenshot(ShotFile);
ShotFile = null;
Exit();
}
}
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);
using var fs = File.Create(file);
tex.SaveAsPng(fs, w, h);
}
// ================================================================ helpers
void PrintFps(int x, int y) =>
_sb.DrawString(_font12, "FPS: " + _fps, new Vector2(x, y), Color.White);
void ResetAll()
{
G.Change = 0;
G.Score = 0;
G.Lives = 3;
}
void ResetPlayer()
{
var p = new Player(G.Sprites) { Name = "Player" };
// original computed X before assigning Width (Width was still 0)
p.X = G.ScreenWidth / 2 - p.Width;
p.Y = G.ScreenHeight - 80;
p.Health = 16;
p.FireRate = 20;
p.Width = 73;
p.Height = 45;
p.Image = G.Images.IL1[0];
p.Weapon = Weapon.Reg;
p.ImageIndex = 0;
p.DoPixelCheck = true;
p.DoCollision = true;
p.CenterX = 0.5f;
p.CenterY = 0.5f;
G.Player = p;
}
void ClipCamera(float x, float y, int width, int height)
{
if (x < G.Camera.Width / 2f)
G.NewCamX = 0;
else
G.NewCamX = (int)(x + width / 2f - G.Camera.Width / 2f);
if (y < G.Camera.Height / 2f)
G.NewCamY = 0;
else
G.NewCamY = (int)(y + height / 2f - G.Camera.Height / 2f);
// the min/max clips were commented out in the original, leaving these unconditional
G.NewCamX = G.Map.WidthCount * 32 - G.Camera.Width;
G.NewCamY = G.Map.HeightCount * 32 - G.Camera.Height;
int frameCamSpeed = 1;
if (G.NewCamX > G.Camera.X)
{
G.Camera.X += frameCamSpeed;
if (G.NewCamX < G.Camera.X) G.Camera.X = G.NewCamX;
}
else if (G.NewCamX < G.Camera.X)
{
G.Camera.X -= frameCamSpeed;
if (G.NewCamX > G.Camera.X) G.Camera.X = G.NewCamX;
}
// Y seek was commented out in the original
}
void DrawLayer1()
{
var tileset = G.Map.Tileset;
if (tileset == null) return;
for (int a = 0; a <= G.Camera.Width / 32 + 1; a++)
for (int b = 0; b <= G.Camera.Height / 32 + 1; b++)
{
int xid = (a * 32 + G.Camera.X) / 32;
int yid = (b * 32 + G.Camera.Y) / 32;
int drawX = xid * 32 - G.Camera.X;
int drawY = yid * 32 - G.Camera.Y;
if (xid < G.Map.WidthCount && yid < G.Map.HeightCount && xid >= 0 && yid >= 0)
{
int id = G.Map.Layer1[xid][yid];
if (id != -1)
tileset.Draw(_sb, drawX, drawY, 0, 0.5f, 0.5f, 1, 1, 255, 255, 255, 255, id);
}
}
}
void MoveSelection(int curSelect)
{
G.MainMenu[G.Prev] = 0;
G.MainMenu[curSelect] = 1;
G.Prev = curSelect;
}
// ================================================================ spawning
void SpawnObject(ObjectType type, float xPos, float yPos)
{
switch (type)
{
case ObjectType.Fighter:
_ = new Enemy(G.Sprites)
{
X = xPos, Y = yPos, YVel = 1.5f, Width = 34, Height = 34,
Image = G.Images.IL1[1], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
AI = AI.Normal, Health = 2, HitTimer = 255,
};
break;
case ObjectType.Kamikaze:
_ = new Enemy(G.Sprites)
{
X = xPos, Y = yPos, YVel = 2, Width = 32, Height = 32,
Image = G.Images.IL1[7], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
AI = AI.Hunt, Health = 3, HitTimer = 255,
};
break;
case ObjectType.Tactical:
_ = new Enemy(G.Sprites)
{
X = xPos, Y = yPos, YVel = 3, Charging = false, Width = 32, Height = 32,
Image = G.Images.IL1[15], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
AI = AI.Tactical, Health = 5, HitTimer = 255,
};
break;
case ObjectType.Mine:
_ = new Enemy(G.Sprites)
{
X = xPos, Y = yPos, CenterX = 0.5f, CenterY = 0.5f, YVel = 0.5f,
AnimTicks = 0, ExplodeTime = G.Random(100) + 200, Width = 32, Height = 48,
Image = G.Images.IL1[17], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
Health = 10, AI = AI.Mine, MoveDir = "right",
};
break;
case ObjectType.MissileBase:
_ = new Enemy(G.Sprites)
{
Name = "Missile Base",
X = xPos, Y = yPos, CenterX = 0.5f, CenterY = 0.5f, YVel = 1,
Width = 32, Height = 32,
Image = G.Images.IL1[18], ImageIndex = 1,
DoCollision = false, DoPixelCheck = true,
Health = 1, AI = AI.None,
};
break;
case ObjectType.MissileTurret:
_ = new Enemy(G.Sprites)
{
Name = "Missile Turret",
X = xPos, Y = yPos, YVel = 1, CenterX = 0.5f, CenterY = 0.5f,
Width = 32, Height = 32,
Image = G.Images.IL1[18], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
Health = 4, FireRate = 1, AI = AI.Turret,
};
break;
case ObjectType.Scanner:
_ = new Enemy(G.Sprites)
{
Name = "Scanner",
X = xPos, Y = yPos, YVel = 1, CenterX = 0.5f, CenterY = 0.5f,
Width = 32, Height = 32,
Image = G.Images.IL1[20], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
Health = 10, AI = AI.None,
};
break;
case ObjectType.Boss:
_ = new Boss(G.Sprites)
{
X = xPos, Y = yPos, YVel = 1, Width = 98, Height = 98,
Image = G.Images.IL1[9], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
MoveDir = "right", Health = 50, HitTimer = 255, AI = AI.Boss,
};
break;
case ObjectType.Boss2:
_ = new Boss(G.Sprites)
{
X = xPos, Y = yPos, YVel = 1, Width = 98, Height = 98,
Image = G.Images.IL1[13], ImageIndex = 0,
DoCollision = true, DoPixelCheck = true,
MoveDir = "down", Health = 200, HitTimer = 255, AI = AI.Boss2,
Timer = 0, RandX = 4,
};
break;
}
}
void CreateSquad(ObjectType ship, int pattern)
{
int randomX = G.Random(G.ScreenWidth - 140) + 64;
switch (pattern)
{
case 0:
SpawnObject(ship, randomX, -32);
SpawnObject(ship, randomX - 32, -64);
SpawnObject(ship, randomX + 32, -64);
break;
case 1:
SpawnObject(ship, randomX, -32);
SpawnObject(ship, randomX - 32, -64);
SpawnObject(ship, randomX + 32, -64);
SpawnObject(ship, randomX + 64, -96);
SpawnObject(ship, randomX - 64, -96);
break;
}
}
// ================================================================ states
void DrawMenu()
{
PrintFps(10, 10);
// water background
for (int a = 0; a <= G.ScreenWidth / 32; a++)
for (int b = 0; b <= G.ScreenHeight / 32; b++)
G.Images.IL1[4].Draw(_sb, a * 32, b * 32, 0, 0, 0, 1, 1, 255, 255, 255, 200, 0);
G.Images.IL2[3].Draw(_sb, G.ScreenWidth / 2 - 180, 50, pattern: 0);
G.Images.IL2[4].Draw(_sb, G.ScreenWidth / 2 - 180, 200, pattern: G.MainMenu[4]);
G.Images.IL2[5].Draw(_sb, G.ScreenWidth / 2 - 180, 250, pattern: G.MainMenu[3]);
G.Images.IL2[6].Draw(_sb, G.ScreenWidth / 2 - 180, 300, pattern: G.MainMenu[2]);
G.Images.IL2[7].Draw(_sb, G.ScreenWidth / 2 - 180, 350, pattern: G.MainMenu[1]);
G.Images.IL2[8].Draw(_sb, G.ScreenWidth / 2 - 180, 400, pattern: G.MainMenu[0]);
var versionSize = _font12.MeasureString(G.Version);
_sb.DrawString(_font12, G.Version, new Vector2(G.ScreenWidth / 2 - versionSize.X / 2, 100), Color.White);
if (G.Input.UpClicked)
{
G.Music.Songs[7].Play();
G.CurSelection++;
if (G.CurSelection > 4) G.CurSelection = 0;
if (G.CurSelection < 0) G.CurSelection = 4;
MoveSelection(G.CurSelection);
}
if (G.Input.DownClicked)
{
G.Music.Songs[7].Play();
G.CurSelection--;
if (G.CurSelection > 4) G.CurSelection = 0;
if (G.CurSelection < 0) G.CurSelection = 4;
MoveSelection(G.CurSelection);
}
if (G.Input.Button3Clicked)
{
switch (G.CurSelection)
{
case 0:
Exit();
break;
case 1:
G.Music.Songs[4].Stop();
G.Music.Songs[9].Play();
G.Sprites.KillAll();
G.Sprites.RemoveDead();
G.BitmapFont2.Alpha = 0;
G.Counter = 0;
G.TextNum = 0;
G.TitleText = new GameText("DESIGN AND PROGRAMMING", 30, G.BitmapFont2);
G.TitleText.SetSpeed(3);
G.IdText = new GameText("BRIAN BICKNELL", 30, G.BitmapFont2);
G.IdText.SetSpeed(3);
G.State = GameState.Credits;
break;
case 2:
G.State = GameState.HighScore;
break;
case 3:
G.Music.Songs[8].Play();
break;
case 4:
G.Sprites.KillAll();
G.Sprites.RemoveDead();
ResetAll();
G.Level = 1;
G.State = GameState.Intermission;
G.Music.Songs[4].Stop();
G.Music.Songs[7].Play();
break;
}
}
if (G.Input.Button6Clicked) // '2' starts the demo intro
{
G.Sprites.KillAll();
G.Sprites.RemoveDead();
ResetAll();
G.Map.LoadMap("intro.omap");
G.Camera.Y = G.Map.HeightCount * 32 - G.ScreenHeight;
var demoShip = new DemoShip(G.Sprites) { Name = "DemoShip" };
demoShip.X = G.ScreenWidth / 2 - demoShip.Width; // Width still 0, like the original
demoShip.Y = G.ScreenHeight;
demoShip.Width = 73;
demoShip.Height = 45;
demoShip.Image = G.Images.IL1[0];
demoShip.ImageIndex = 0;
demoShip.DoPixelCheck = true;
demoShip.DoCollision = true;
demoShip.CenterX = 0.5f;
demoShip.CenterY = 0.5f;
_demoShip = demoShip;
G.Music.Songs[4].Stop();
G.Music.Songs[14].Play();
G.TitleText = new GameText("BICK SOFTWARE", 30, G.BitmapFont2);
G.IdText = new GameText("PRESENTS", 30, G.BitmapFont2);
G.TextCreated = false;
G.BossMode = false;
G.State = GameState.Intro;
}
if (G.Input.Button2Clicked)
Exit();
}
void DoIntro()
{
if (_demoShip != null && _demoShip.Y > 300)
_demoShip.Y -= 0.2f;
ClipCamera(G.Camera.X, G.Camera.Y, 32, 32);
DrawLayer1();
PrintFps(10, 20);
if (!G.TextCreated)
{
G.TitleText.SetText("BICK SOFTWARE PRESENTS");
G.TextCreated = true;
G.BitmapFont2.Alpha = 0;
}
if (G.Counter == 5)
{
G.TitleText.FadeOut(_sb, 200, 200, 500);
if (G.BitmapFont2.Alpha == 0)
{
G.TitleText.SetText("OMEGA SQUADREN");
G.Counter = 0;
}
}
else
{
G.TitleText.FadeIn(_sb, 200, 200, 500);
if (G.BitmapFont2.Alpha == 255)
G.Counter = 5;
}
if (G.Music.Songs[14].Position > 36)
{
if (G.Random(50) == 0)
SpawnObject(ObjectType.Fighter, G.Random(G.ScreenWidth), -32);
}
if (G.Camera.Y <= 0)
{
G.Camera.Y = 0;
if (!G.BossMode)
{
SpawnObject(ObjectType.Boss, 300, -G.ScreenHeight + 98);
G.BossMode = true;
}
}
else
G.Camera.Y--;
}
void DoIntermission()
{
G.BossMode = false;
G.Done = false;
if (G.Level > 5)
{
G.Music.Songs[12].Stop();
G.Music.Songs[3].Play();
G.State = GameState.GameOver;
}
string label = "Level " + G.Level;
var size = _font20.MeasureString("Level" + G.Level);
_sb.DrawString(_font20, label, new Vector2(G.ScreenWidth / 2 - size.X / 2, G.ScreenHeight / 2), Color.White);
G.Counter++;
G.ShipsKilled = 0;
if (G.Counter > 500)
{
switch (G.Level)
{
case 1: G.Map.LoadMap("test10.omap"); break;
case 2: G.Map.LoadMap("test7.omap"); break;
case 3: G.Map.LoadMap("test8.omap"); break;
case 4: G.Map.LoadMap("bonus.omap"); break;
case 5: G.Map.LoadMap("lvl5.omap"); break;
}
G.State = GameState.Playing;
ResetPlayer();
G.Camera.Y = G.Map.HeightCount * 32 - G.ScreenHeight;
if (G.Level > 1)
G.Music.Songs[G.Level - 2].Stop();
if (G.Level == 1)
{
G.Music.Songs[G.MUS_LEVEL1].Volume = 20;
G.Music.Songs[G.MUS_LEVEL1].Play();
}
else if (G.Level == 4)
G.Music.Songs[12].Play();
else if (G.Level == 5)
{
G.Music.Songs[12].Stop();
G.Music.Songs[4].Play();
}
else
G.Music.Songs[G.Level - 1].Play();
}
}
void GameLoop()
{
ClipCamera(G.Camera.X, G.Camera.Y, 32, 32);
DrawLayer1();
// level 4: boss chase with a wrapping camera
if (G.Level == 4)
{
if (!G.BossMode)
{
SpawnObject(ObjectType.Boss2, 300, -G.ScreenHeight + 98);
G.BossMode = true;
}
if (G.Camera.Y <= 0)
G.Camera.Y = G.Map.HeightCount * 32 - G.ScreenHeight;
else
G.Camera.Y -= 2;
}
int randomNum = G.Random(500);
if (randomNum == 0) CreateSquad(ObjectType.Fighter, 0);
if (randomNum == 1) CreateSquad(ObjectType.Kamikaze, 1);
if (randomNum == 5) SpawnObject(ObjectType.Mine, G.Random(G.ScreenWidth), -48);
if (G.Camera.Y <= 0)
{
G.Camera.Y = 0;
if (!G.BossMode)
{
SpawnObject(ObjectType.Boss, 300, -G.ScreenHeight + 98);
G.BossMode = true;
}
}
else
G.Camera.Y--;
GetStats();
G.Counter++;
if (G.Counter > 10)
{
G.Change++;
if (G.Change > 2) G.Change = 0;
G.Counter = 0;
}
PrintFps(10, 10);
_sb.DrawString(_font12, "Sprites: " + G.Sprites.Count, new Vector2(10, 30), Color.White);
if (G.Lives < 0)
{
G.Sprites.KillAll();
G.Sprites.RemoveDead();
G.Music.Songs[G.Level - 1].Stop();
G.State = GameState.GameOver;
}
if (G.Done)
{
G.Sprites.KillAll();
G.ShipsKilled++;
G.Level++;
G.State = GameState.Stats;
G.Counter = 0;
}
// spawn entities from map triggers at/below the camera
for (int a = 0; a < G.Map.WidthCount; a++)
for (int b = Math.Max(0, G.Camera.Y / 32); b < G.Map.HeightCount; b++)
{
string? action = G.Map.TriggerActions[a, b];
if (action == null) continue;
int tempX = a * 32;
int tempY = b - 128; // original quirk: tile row minus 128 pixels
switch (action)
{
case "fighter":
SpawnObject(ObjectType.Fighter, tempX, tempY);
G.Map.TriggerActions[a, b] = null;
break;
case "kamikaze":
SpawnObject(ObjectType.Kamikaze, tempX, tempY);
G.Map.TriggerActions[a, b] = null;
break;
case "tactical":
SpawnObject(ObjectType.Tactical, tempX, tempY);
G.Map.TriggerActions[a, b] = null;
break;
case "missileturret":
SpawnObject(ObjectType.MissileTurret, tempX, tempY);
SpawnObject(ObjectType.MissileBase, tempX, tempY);
G.Map.TriggerActions[a, b] = null;
break;
case "scanner":
SpawnObject(ObjectType.Scanner, tempX, tempY);
G.Map.TriggerActions[a, b] = null;
break;
}
}
if (G.Input.Button2Clicked)
{
G.Sprites.KillAll();
G.Sprites.RemoveDead();
G.Music.Songs[G.Level - 1].Stop();
G.Music.Songs[G.MUS_TITLE].Play();
G.State = GameState.Idle;
}
}
void GetStats()
{
int imageIdx = G.Player.Weapon switch
{
Weapon.Reg => 0,
Weapon.Super => 1,
Weapon.Multi => 2,
_ => -1,
};
G.Images.IL3[2].Draw(_sb, 300, 560, pattern: imageIdx);
int colWidth = -10;
for (int i = 0; i < G.Lives; i++)
{
G.Images.IL1[0].Draw(_sb, G.ScreenWidth - 800 + colWidth, G.ScreenHeight - 50,
0, 0.5f, 0.5f, 0.5f, 0.5f, 255, 255, 255, 255, 0);
colWidth += 30;
}
G.Images.IL1[6].Draw(_sb, 5, 50, pattern: 0);
G.BitmapFont1.Print(_sb, 75, 50, G.Score.ToString());
G.BitmapFont2.Print(_sb, G.ScreenWidth - 150, G.ScreenHeight - 30, "HEALTH");
G.Images.IL3[0].Draw(_sb, G.ScreenWidth - 150, G.ScreenHeight - 20, pattern: 0);
int barWidth = 0;
for (int i = 0; i < G.Player.Health; i++)
{
G.Images.IL3[1].Draw(_sb, G.ScreenWidth - 148 + barWidth, G.ScreenHeight - 18, pattern: 0);
barWidth += 8;
}
G.BitmapFont2.Print(_sb, G.ScreenWidth / 6, G.ScreenHeight - 30, "POWER");
G.Images.IL3[0].Draw(_sb, G.ScreenWidth / 6, G.ScreenHeight - 20, pattern: 0);
int energyWidth = 0;
for (int i = 0; i < G.Player.Power; i++)
{
G.Images.IL3[3].Draw(_sb, G.ScreenWidth / 6 + 2 + energyWidth, G.ScreenHeight - 18,
0, 0, 0, 1, 1, 0, 100 + energyWidth, 0, 255, 0);
energyWidth += 7;
}
if (G.Player.Health < 0)
{
G.Player.Kill();
G.Lives--;
ResetPlayer();
}
}
void DoStats()
{
if (G.Counter > 10 && G.CurCount < G.ShipsKilled)
{
G.Music.Songs[5].Play();
G.CurCount++;
G.Counter = 0;
}
if (G.Input.Button1Clicked && G.CurCount != G.ShipsKilled)
{
G.CurCount = G.ShipsKilled;
G.Counter = 0;
}
string text = "Ships Killed: " + G.CurCount;
var size = _font12.MeasureString(text);
_sb.DrawString(_font12, text, new Vector2(G.ScreenWidth / 2 - size.X / 2, G.ScreenHeight / 2), Color.White);
G.Counter++;
_sb.DrawString(_font12, G.Counter.ToString(), new Vector2(10, 10), Color.White);
if (G.Counter > 500)
{
G.Counter = 0;
G.CurCount = 0;
G.State = GameState.Shop;
}
}
void DoShop()
{
_sb.DrawString(_font12, "This is the blackmarket shop", new Vector2(10, 10), Color.White);
_sb.DrawString(_font12, "PRESS [SPACE] TO CONTINUE", new Vector2(10, 550), Color.White);
if (G.Input.Button1Clicked)
{
G.State = GameState.Intermission;
G.Music.Songs[G.Level - 2].Stop();
G.Music.Songs[7].Play();
}
}
void DoGameOver()
{
G.Images.IL2[2].Draw(_sb, G.ScreenWidth / 2 - 49, G.ScreenHeight / 2, pattern: 0);
G.Images.IL1[6].Draw(_sb, G.ScreenWidth / 2 - 34, G.ScreenHeight / 2 + 100, pattern: 0);
G.BitmapFont1.Print(_sb, G.ScreenWidth / 2 + 30, G.ScreenHeight / 2 + 100, G.Score.ToString());
if (G.Input.Button1Clicked)
{
G.Music.Songs[3].Stop();
G.Music.Songs[9].Play();
G.BitmapFont2.Alpha = 0;
G.Counter = 0;
G.TextNum = 0;
G.TitleText = new GameText("DESIGN AND PROGRAMMING", 30, G.BitmapFont2);
G.TitleText.SetSpeed(3);
G.IdText = new GameText("BRIAN BICKNELL", 30, G.BitmapFont2);
G.IdText.SetSpeed(3);
if (G.HighScores.Qualifies(G.Score))
{
_enteredName = "";
G.State = GameState.EnterName; // replaces the Form2 dialog
}
else
G.State = GameState.Credits;
}
}
void DoCredits()
{
G.TitleText.FadeIn(_sb,
G.ScreenWidth / 2 - G.TitleText.GetText().Length * G.BitmapFont2.CharWidth / 2,
G.ScreenHeight / 2 - 15, 5);
if (G.TitleText.IsDone)
G.IdText.TypePrint(_sb,
G.ScreenWidth / 2 - G.IdText.GetText().Length * G.BitmapFont2.CharWidth / 2,
G.ScreenHeight / 2 + 15);
if (G.IdText.IsDone)
{
G.Counter++;
if (G.Counter > 400)
{
G.TextNum++;
switch (G.TextNum)
{
case 1:
G.BitmapFont2.Alpha = 0;
G.TitleText.SetText("GRAPHICS BY");
G.IdText.SetText("BRIAN MAZUROWSKI");
break;
case 2:
G.BitmapFont2.Alpha = 0;
G.TitleText.SetText("MUSIC BY");
G.IdText.SetText("ADAM HAIRE");
break;
case 3:
G.BitmapFont2.Alpha = 0;
G.TitleText.SetText("SPECIAL THANKS TO");
G.IdText.SetText("THE OMEGA TEAM");
break;
case 4:
G.BitmapFont2.Alpha = 0;
G.TitleText.SetText("THANK YOU FROM");
G.IdText.SetText("BICK SOFTWARE");
break;
case 5:
G.State = GameState.HighScore;
break;
}
G.Counter = 0;
}
}
if (G.Input.Button2Clicked)
{
G.Music.Songs[9].Stop();
G.Music.Songs[G.MUS_TITLE].Play();
G.State = GameState.Idle;
}
}
void DoHighScore()
{
// The original printed via OmegaBitmapFont3, which had no image list assigned;
// here the letter and number fonts are combined so the table is actually visible.
int delta = 0;
int savedAlpha = G.BitmapFont2.Alpha;
G.BitmapFont2.Alpha = 255;
for (int i = 0; i < 10; i++)
{
G.BitmapFont2.Print(_sb, 10, 10 + delta, G.HighScores.List[i].Name.ToUpperInvariant());
G.BitmapFont1.Print(_sb, 200, 10 + delta, G.HighScores.List[i].Score.ToString());
delta += 20;
}
G.BitmapFont2.Alpha = savedAlpha;
if (G.Input.Button1Clicked)
G.State = GameState.Idle;
}
void DoEnterName()
{
int savedAlpha = G.BitmapFont2.Alpha;
G.BitmapFont2.Alpha = 255;
G.BitmapFont2.Print(_sb, 200, 250, "NEW HIGH SCORE");
G.BitmapFont2.Print(_sb, 200, 280, "ENTER YOUR NAME");
G.BitmapFont2.Print(_sb, 200, 320, _enteredName);
G.BitmapFont2.Alpha = savedAlpha;
foreach (var key in G.Input.Current.GetPressedKeys())
{
if (!G.Input.Previous.IsKeyUp(key)) continue;
if (key >= Keys.A && key <= Keys.Z && _enteredName.Length < 20)
_enteredName += (char)('A' + (key - Keys.A));
else if (key == Keys.Space && _enteredName.Length < 20)
_enteredName += ' ';
else if (key == Keys.Back && _enteredName.Length > 0)
_enteredName = _enteredName[..^1];
else if (key == Keys.Enter && _enteredName.Length > 0)
{
G.HighScores.Enter(_enteredName, G.Score);
G.State = GameState.Credits;
}
}
}
}

61
FreedomFighter/GameMap.cs Normal file
View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.Xna.Framework.Graphics;
namespace FreedomFighter;
/// <summary>
/// Port of TOmegaMap, loading the JSON files converted from the .omap format.
/// Only Layer1 pattern indices and triggers are used by the game.
/// </summary>
public class GameMap
{
public int WidthCount;
public int HeightCount;
public int TileWidth = 32;
public int TileHeight = 32;
public int[][] Layer1 = []; // [x][y] -> tileset pattern index, -1 = empty
public string?[,] TriggerActions = new string?[0, 0];
public OmegaImage? Tileset;
readonly GraphicsDevice _gd;
readonly string _contentDir;
readonly Dictionary<string, OmegaImage> _tilesetCache = new();
public GameMap(GraphicsDevice gd, string contentDir)
{
_gd = gd;
_contentDir = contentDir;
}
record TriggerJson(int x, int y, string name, string action);
record MapJson(int widthCount, int heightCount, string oil, int tileWidth, int tileHeight,
int[][] layer1, TriggerJson[] triggers);
public void LoadMap(string mapName)
{
string path = Path.Combine(_contentDir, "maps", Path.GetFileNameWithoutExtension(mapName) + ".json");
var m = JsonSerializer.Deserialize<MapJson>(File.ReadAllText(path))!;
WidthCount = m.widthCount;
HeightCount = m.heightCount;
TileWidth = m.tileWidth;
TileHeight = m.tileHeight;
Layer1 = m.layer1;
TriggerActions = new string?[WidthCount, HeightCount];
foreach (var t in m.triggers)
TriggerActions[t.x, t.y] = t.action;
string tilesetFile = Path.GetFileNameWithoutExtension(m.oil).ToLowerInvariant() + "_00.png";
if (!_tilesetCache.TryGetValue(tilesetFile, out var tileset))
{
tileset = OmegaImage.Load(_gd, Path.Combine(_contentDir, "tilesets", tilesetFile), TileWidth, TileHeight, m.oil);
_tilesetCache[tilesetFile] = tileset;
}
Tileset = tileset;
}
}

142
FreedomFighter/GameText.cs Normal file
View File

@@ -0,0 +1,142 @@
using System;
using Microsoft.Xna.Framework.Graphics;
namespace FreedomFighter;
/// <summary>
/// Port of TOmegaBitmapFont: a fixed-cell bitmap font backed by an image list item,
/// with mutable color/alpha state (the credits animate Alpha on the shared font).
/// </summary>
public class BitmapFont
{
public string Characters = "";
public OmegaImage Image = null!;
public int Red = 255, Green = 255, Blue = 255;
public int Alpha = 255;
public int CharWidth => Image.TileWidth;
public int CharHeight => Image.TileHeight;
public BitmapFont(OmegaImage image, string characters)
{
Image = image;
Characters = characters;
}
public void PrintFast(SpriteBatch sb, float x, float y, char c)
{
int idx = Characters.IndexOf(c);
if (idx < 0) return;
Image.Draw(sb, x, y, 0, 0, 0, 1, 1, Red, Green, Blue, Math.Clamp(Alpha, 0, 255), idx);
}
public void Print(SpriteBatch sb, float x, float y, string text)
{
for (int i = 0; i < text.Length; i++)
PrintFast(sb, x + i * CharWidth, y, text[i]);
}
}
/// <summary>
/// Port of the TGameText helper (GameText.pas): typewriter printing and
/// fade in/out of text through a bitmap font's shared Alpha.
/// </summary>
public class GameText
{
string _text;
int _length;
int _curChar;
int _maxLength;
int _timer;
int _speed = 10;
bool _done;
readonly BitmapFont _font;
public GameText(string msg, int rowLength, BitmapFont font)
{
_text = msg;
_length = msg.Length;
_maxLength = rowLength;
_font = font;
}
public void SetText(string msg)
{
_text = msg;
_length = msg.Length;
_curChar = 0;
_timer = 0;
}
public void SetSpeed(int speed) => _speed = speed;
public bool IsDone => _done;
public string GetText() => _text;
public int GetLength() => _length * _font.CharWidth;
// The Pascal original indexed the 1-based string with i = 0..CurChar; index 0 was a no-op.
void PrintChars(SpriteBatch sb, float x, float y, int upTo)
{
for (int i = 1; i <= upTo && i <= _length; i++)
{
char c = _text[i - 1];
if (i <= _maxLength)
_font.PrintFast(sb, x + i * _font.CharWidth, y, c);
else
{
int tempChar = i;
while (tempChar > _maxLength) tempChar -= _maxLength;
_font.PrintFast(sb, x + tempChar * 12, y + (int)Math.Ceiling(i / (double)_maxLength) * 24 - 24, c);
}
}
}
public void TypePrint(SpriteBatch sb, float x, float y)
{
_done = false;
PrintChars(sb, x, y, _curChar);
if (_curChar < _length)
{
_timer++;
if (_timer > _speed)
{
_curChar++;
if (_curChar <= _length && _curChar >= 1 && _text[_curChar - 1] == ' ')
_curChar++;
_timer = 0;
}
}
else
_done = true;
}
public void FadeIn(SpriteBatch sb, float x, float y, int speed)
{
_done = false;
PrintChars(sb, x, y, _length);
_timer++;
if (_timer > speed && _font.Alpha < 255)
_font.Alpha++;
else
_done = true; // original quirk: also "done" while the timer is still counting
}
public void FadeOut(SpriteBatch sb, float x, float y, int speed)
{
_done = false;
PrintChars(sb, x, y, _length);
_timer++;
if (_timer > speed && _font.Alpha > 0)
_font.Alpha--;
else
_done = true;
}
public void Reset()
{
_curChar = 0;
_timer = 0;
_speed = 10;
_done = false;
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace FreedomFighter;
public record HighScoreEntry(string Name, int Score);
/// <summary>High score table; JSON replaces the original Highscore.dat record file.</summary>
public class HighScores
{
const string FileName = "highscore.json";
public HighScoreEntry[] List = new HighScoreEntry[10];
public void Load()
{
if (File.Exists(FileName))
{
try
{
var loaded = JsonSerializer.Deserialize<HighScoreEntry[]>(File.ReadAllText(FileName));
if (loaded is { Length: 10 }) { List = loaded; return; }
}
catch { /* fall through to defaults */ }
}
for (int i = 0; i < 10; i++)
List[i] = new HighScoreEntry("Nameless", 0);
Save();
}
public void Save() => File.WriteAllText(FileName, JsonSerializer.Serialize(List));
public bool Qualifies(int score) => score > List[9].Score;
public void Add(int num, string name, int score)
{
for (int i = 9; i >= num + 1; i--)
List[i] = List[i - 1];
List[num] = new HighScoreEntry(name, score);
}
/// <summary>Port of EnterHighScoreList: insert from the bottom up.</summary>
public void Enter(string name, int score)
{
for (int i = 8; i >= 0; i--)
{
if (score <= List[i].Score)
{
Add(i + 1, name, score);
break;
}
if (i == 0)
{
Add(i, name, score);
break;
}
}
Save();
}
}

BIN
FreedomFighter/Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1,114 @@
using System;
using System.IO;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Media;
namespace FreedomFighter;
/// <summary>
/// Replacement for TOmegaMusic: a flat list of "songs" where some entries are
/// streamed music (mp3 via MediaPlayer) and some are sound effects (wav).
/// Indices mirror OmegaMusic1.Songs in GameU.dfm.
/// </summary>
public class MusicManager
{
public class Entry
{
readonly MusicManager _mgr;
public string Name = "";
public Song? Song; // streamed music
public SoundEffect? Effect; // wav sfx
public int Volume = 100; // 0..100, Omega convention
public Entry(MusicManager mgr) => _mgr = mgr;
public void Play()
{
if (Song != null)
{
MediaPlayer.Volume = Volume / 100f;
MediaPlayer.Play(Song);
_mgr._current = this;
}
else if (Effect != null)
{
Effect.Play(Volume / 100f, 0, 0);
Volume = 100; // one-shot volume, like the original's per-play setting
}
}
public void Stop()
{
if (Song != null && _mgr._current == this)
{
MediaPlayer.Stop();
_mgr._current = null;
}
}
/// <summary>Playback position in seconds (original used this in the intro).</summary>
public double Position => Song != null && _mgr._current == this
? MediaPlayer.PlayPosition.TotalSeconds : 0;
}
Entry? _current;
public Entry[] Songs = [];
public void Load(string contentDir)
{
MediaPlayer.IsRepeating = true; // OmegaMusic1.Loop was called every menu frame
// Index order mirrors OmegaMusic1.Songs in GameU.dfm.
// [1] 'Level test.mp3' is lost — Track06.mp3 substitutes.
// [3] gameover was a MIDI (Nazi_Rap.mid); MonoGame has no MIDI playback, so it is silent.
(string name, string? file)[] entries =
[
("level1", @"music\white light.mp3"),
("level2", @"music\Track06.mp3"),
("level3", @"music\G2 Final.mp3"),
("gameover", null),
("title", @"music\Main Menu 2.mp3"),
("fire1", @"sfx\fire1.wav"),
("reload", @"sfx\reload.wav"),
("thump", @"sfx\thump1.wav"),
("beep", @"sfx\beep.wav"),
("credits", @"music\G8 Final.mp3"),
("hit", @"sfx\hit4.wav"),
("explosion", @"sfx\explode2.wav"),
("level4", @"music\Thunderization.mp3"),
("missile", @"sfx\missile.wav"),
("intro", @"music\Intro.mp3"),
];
Songs = new Entry[entries.Length];
for (int i = 0; i < entries.Length; i++)
{
var e = new Entry(this) { Name = entries[i].name };
var file = entries[i].file;
if (file != null)
{
string path = Path.Combine(contentDir, file);
if (File.Exists(path))
{
try
{
if (path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
{
using var fs = File.OpenRead(path);
e.Effect = SoundEffect.FromStream(fs);
}
else
{
e.Song = Song.FromUri(entries[i].name, new Uri(path, UriKind.Absolute));
}
}
catch (Exception ex)
{
Console.WriteLine($"Audio load failed for {file}: {ex.Message}");
}
}
}
Songs[i] = e;
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace FreedomFighter;
/// <summary>
/// Equivalent of a TOmegaImageList item: a texture subdivided into equally-sized
/// patterns (frames), drawn by pattern index. Also builds alpha masks for
/// pixel-perfect collision (Omega's DoPixelCheck).
/// </summary>
public class OmegaImage
{
public string Name = "";
public Texture2D Texture = null!;
public int TileWidth;
public int TileHeight;
public int Columns;
public int PatternCount;
public bool[] OpaqueMask = null!; // per-pixel alpha>0, whole texture, row-major
public static OmegaImage Load(GraphicsDevice gd, string file, int tileW, int tileH, string name)
{
using var fs = File.OpenRead(file);
var tex = Texture2D.FromStream(gd, fs);
int tw = tileW > 0 ? tileW : tex.Width;
int th = tileH > 0 ? tileH : tex.Height;
var pixels = new Color[tex.Width * tex.Height];
tex.GetData(pixels);
var mask = new bool[pixels.Length];
for (int i = 0; i < pixels.Length; i++)
mask[i] = pixels[i].A > 0;
return new OmegaImage
{
Name = name,
Texture = tex,
TileWidth = tw,
TileHeight = th,
Columns = Math.Max(1, tex.Width / tw),
PatternCount = Math.Max(1, tex.Width / tw) * Math.Max(1, tex.Height / th),
OpaqueMask = mask,
};
}
public Rectangle PatternRect(int pattern)
{
if (pattern < 0 || pattern >= PatternCount) pattern = 0;
return new Rectangle(pattern % Columns * TileWidth, pattern / Columns * TileHeight, TileWidth, TileHeight);
}
/// <summary>Is the pixel at (px,py) within the given pattern opaque? Coordinates are pattern-local.</summary>
public bool IsOpaque(int pattern, int px, int py)
{
if (px < 0 || py < 0 || px >= TileWidth || py >= TileHeight) return false;
var r = PatternRect(pattern);
int x = r.X + px, y = r.Y + py;
if (x >= Texture.Width || y >= Texture.Height) return false;
return OpaqueMask[y * Texture.Width + x];
}
/// <summary>
/// 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.
/// </summary>
public void Draw(SpriteBatch sb, float x, float y, float rotation = 0,
float centerX = 0, float centerY = 0, float scaleX = 1, float scaleY = 1,
int red = 255, int green = 255, int blue = 255, int alpha = 255, int pattern = 0)
{
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 color = new Color(red, green, blue, alpha);
sb.Draw(Texture, pos, src, color, MathHelper.ToRadians(rotation), origin,
new Vector2(scaleX, scaleY), SpriteEffects.None, 0);
}
}
/// <summary>Loads the image lists extracted from GameU.dfm via gfx/manifest.json.</summary>
public class ImageLists
{
readonly Dictionary<string, List<OmegaImage>> _lists = new();
public List<OmegaImage> IL1 => _lists["OmegaImageList1"];
public List<OmegaImage> IL2 => _lists["OmegaImageList2"];
public List<OmegaImage> IL3 => _lists["OmegaImageList3"];
public List<OmegaImage> IL4 => _lists["OmegaImageList4"];
public List<OmegaImage> IL5 => _lists["OmegaImageList5"];
record ManifestItem(int Index, string Name, string File, int TileWidth, int TileHeight);
public ImageLists(GraphicsDevice gd, string contentDir)
{
string gfxDir = Path.Combine(contentDir, "gfx");
var manifest = JsonSerializer.Deserialize<Dictionary<string, List<ManifestItem>>>(
File.ReadAllText(Path.Combine(gfxDir, "manifest.json")))!;
foreach (var (listName, items) in manifest)
{
var list = new List<OmegaImage>();
foreach (var item in items)
list.Add(OmegaImage.Load(gd, Path.Combine(gfxDir, item.File), item.TileWidth, item.TileHeight, item.Name));
_lists[listName] = list;
}
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.Xna.Framework.Input;
namespace FreedomFighter;
/// <summary>
/// Replacement for TOmegaInput's keyboard state sets, with the key assignments
/// from GameU.dfm: B1=Space, B2=Esc, B3=Enter, B4=RCtrl, B5=1, B6=2, B7=3, B8=M.
/// "States" = held; "StatesClicked" = pressed this frame.
/// </summary>
public class OmegaInput
{
KeyboardState _prev;
KeyboardState _cur;
public void Update()
{
_prev = _cur;
_cur = Keyboard.GetState();
}
bool Held(Keys k) => _cur.IsKeyDown(k);
bool Clicked(Keys k) => _cur.IsKeyDown(k) && _prev.IsKeyUp(k);
public bool Up => Held(Keys.Up);
public bool Down => Held(Keys.Down);
public bool Left => Held(Keys.Left);
public bool Right => Held(Keys.Right);
public bool Button4 => Held(Keys.RightControl) || Held(Keys.LeftControl);
public bool Button8 => Held(Keys.M);
public bool UpClicked => Clicked(Keys.Up);
public bool DownClicked => Clicked(Keys.Down);
public bool Button1Clicked => Clicked(Keys.Space);
public bool Button2Clicked => Clicked(Keys.Escape);
public bool Button3Clicked => Clicked(Keys.Enter);
public bool Button5Clicked => Clicked(Keys.D1) || Clicked(Keys.NumPad1);
public bool Button6Clicked => Clicked(Keys.D2) || Clicked(Keys.NumPad2);
public bool Button7Clicked => Clicked(Keys.D3) || Clicked(Keys.NumPad3);
public bool PauseClicked => Clicked(Keys.Pause);
public KeyboardState Current => _cur;
public KeyboardState Previous => _prev;
}

17
FreedomFighter/Program.cs Normal file
View File

@@ -0,0 +1,17 @@
using System;
using var game = new FreedomFighter.Game1();
// debug aid: --shot <file> [frames] renders, saves the backbuffer to a PNG, and exits
var cmdArgs = Environment.GetCommandLineArgs();
for (int i = 1; i < cmdArgs.Length - 1; i++)
{
if (cmdArgs[i] == "--shot")
{
game.ShotFile = cmdArgs[i + 1];
if (i + 2 < cmdArgs.Length && int.TryParse(cmdArgs[i + 2], out int frames))
game.ShotFrames = frames;
}
if (cmdArgs[i] == "--level" && int.TryParse(cmdArgs[i + 1], out int level))
game.StartLevel = level;
}
game.Run();

120
FreedomFighter/Sprites.cs Normal file
View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace FreedomFighter;
/// <summary>
/// Port of the Omega TSprite base class. Position is the pattern's top-left;
/// CenterX/CenterY only affect the rotation pivot. Width/Height define the
/// logical collision box, independent of the drawn pattern size.
/// </summary>
public abstract class Sprite
{
public string Name = "";
public float X, Y;
public int Width, Height;
public float Rotation; // degrees
public float CenterX, CenterY; // 0..1 fraction of pattern
public float ScaleX = 1, ScaleY = 1;
public int Red = 255, Green = 255, Blue = 255, Alpha = 255;
public OmegaImage? Image;
public int ImageIndex;
public bool DoCollision;
public bool DoPixelCheck;
public bool IsDead;
protected Sprite(SpriteManager manager) => manager.Add(this);
public void SetGreen(int v) => Green = Math.Clamp(v, 0, 255);
public void SetBlue(int v) => Blue = Math.Clamp(v, 0, 255);
/// <summary>Marks the sprite for removal (Omega's Dead).</summary>
public void Kill() => IsDead = true;
public virtual void Move(float moveCount) { }
public virtual void Draw(SpriteBatch sb)
{
Image?.Draw(sb, (int)Math.Round(X), (int)Math.Round(Y), Rotation,
CenterX, CenterY, ScaleX, ScaleY, Red, Green, Blue, Alpha, ImageIndex);
}
public virtual void OnCollision(Sprite sprite, int colX, int colY) { }
public Rectangle Bounds => new((int)X, (int)Y, Width, Height);
}
/// <summary>
/// Port of TOmegaSprite: owns all sprites, runs movement, collision
/// (bounding box + optional pixel check), removal, and drawing.
/// </summary>
public class SpriteManager
{
public readonly List<Sprite> Items = new();
public int Count => Items.Count;
public void Add(Sprite s) => Items.Add(s);
public void KillAll()
{
foreach (var s in Items) s.Kill();
}
public void Move(float moveCount)
{
// like the Pascal for-loop, sprites spawned during iteration are not moved this frame
int n = Items.Count;
for (int i = 0; i < n; i++)
Items[i].Move(moveCount);
}
public void Collision()
{
int n = Items.Count;
for (int i = 0; i < n; i++)
{
var a = Items[i];
if (!a.DoCollision || a.IsDead) continue;
for (int j = i + 1; j < n; j++)
{
var b = Items[j];
if (!b.DoCollision || b.IsDead) continue;
var overlap = Rectangle.Intersect(a.Bounds, b.Bounds);
if (overlap.IsEmpty) continue;
if ((a.DoPixelCheck || b.DoPixelCheck) && !PixelOverlap(a, b, overlap)) continue;
a.OnCollision(b, overlap.X, overlap.Y);
b.OnCollision(a, overlap.X, overlap.Y);
}
}
}
/// <summary>Pixel test on the unrotated pattern masks (rotation ignored, like a cheap approximation).</summary>
static bool PixelOverlap(Sprite a, Sprite b, Rectangle overlap)
{
if (a.Image == null || b.Image == null) return true;
for (int y = overlap.Top; y < overlap.Bottom; y++)
for (int x = overlap.Left; x < overlap.Right; x++)
{
bool pa = a.Image.IsOpaque(a.ImageIndex, x - (int)a.X, y - (int)a.Y);
bool pb = b.Image.IsOpaque(b.ImageIndex, x - (int)b.X, y - (int)b.Y);
if (pa && pb) return true;
}
return false;
}
public void RemoveDead() => Items.RemoveAll(s => s.IsDead);
public void Draw(SpriteBatch sb)
{
int n = Items.Count;
for (int i = 0; i < n; i++)
if (!Items[i].IsDead)
Items[i].Draw(sb);
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="FreedomFighter"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on and is
is designed to work with. Uncomment the appropriate elements and Windows will
automatically selected the most compatible environment. -->
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>