Add project files.
14
AssetExtractor/AssetExtractor.csproj
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="AssetExtractor/AssetExtractor.csproj" />
|
||||||
|
<Project Path="FreedomFighter/FreedomFighter.csproj" />
|
||||||
|
</Solution>
|
||||||
36
FreedomFighter/.config/dotnet-tools.json
Normal 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
@@ -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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
29
FreedomFighter/Content/Content.mgcb
Normal 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
|
||||||
|
|
||||||
16
FreedomFighter/Content/Verdana12.spritefont
Normal 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> </Start>
|
||||||
|
<End>~</End>
|
||||||
|
</CharacterRegion>
|
||||||
|
</CharacterRegions>
|
||||||
|
</Asset>
|
||||||
|
</XnaContent>
|
||||||
16
FreedomFighter/Content/Verdana20.spritefont
Normal 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> </Start>
|
||||||
|
<End>~</End>
|
||||||
|
</CharacterRegion>
|
||||||
|
</CharacterRegions>
|
||||||
|
</Asset>
|
||||||
|
</XnaContent>
|
||||||
278
FreedomFighter/Content/gfx/manifest.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
FreedomFighter/Content/gfx/omegaimagelist1_00_player.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_01_enemy.png
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_02_bullet.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_03_explosion.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_04_water.png
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_05_lives.png
Normal file
|
After Width: | Height: | Size: 624 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_06_score.png
Normal file
|
After Width: | Height: | Size: 687 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_07_enemy2.png
Normal file
|
After Width: | Height: | Size: 1012 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_08_enemybullet.png
Normal file
|
After Width: | Height: | Size: 271 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_09_boss.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_10_sub.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_11_bigexplosion.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_12_turret.png
Normal file
|
After Width: | Height: | Size: 440 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_13_boss2.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_14_cruiser.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_15_tactical.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_16_player2.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_17_mine.png
Normal file
|
After Width: | Height: | Size: 340 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_18_missile_turret.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_19_missile.png
Normal file
|
After Width: | Height: | Size: 962 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist1_20_scanner.png
Normal file
|
After Width: | Height: | Size: 312 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_00_numbers.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_01_title.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_02_gameover.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_03_title2.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_04_newgame.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_05_options.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_06_highscore.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_07_credits.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist2_08_quit.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist3_00_bar.png
Normal file
|
After Width: | Height: | Size: 206 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist3_01_health.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist3_02_weapon.png
Normal file
|
After Width: | Height: | Size: 267 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist3_03_energy.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist4_00_Tiles.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
FreedomFighter/Content/gfx/omegaimagelist4_01_collision.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist4_02_trigger.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
FreedomFighter/Content/gfx/omegaimagelist5_00_unnamed.png
Normal file
|
After Width: | Height: | Size: 663 B |
1
FreedomFighter/Content/maps/bonus.json
Normal file
1
FreedomFighter/Content/maps/intro.json
Normal file
1
FreedomFighter/Content/maps/lvl5.json
Normal file
1
FreedomFighter/Content/maps/test10.json
Normal file
1
FreedomFighter/Content/maps/test7.json
Normal file
1
FreedomFighter/Content/maps/test8.json
Normal file
BIN
FreedomFighter/Content/music/G2 Final.mp3
Normal file
BIN
FreedomFighter/Content/music/G8 Final.mp3
Normal file
BIN
FreedomFighter/Content/music/Intro.mp3
Normal file
BIN
FreedomFighter/Content/music/Main Menu 2.mp3
Normal file
BIN
FreedomFighter/Content/music/Nazi_Rap.mid
Normal file
BIN
FreedomFighter/Content/music/Thunderization.mp3
Normal file
BIN
FreedomFighter/Content/music/Track06.mp3
Normal file
BIN
FreedomFighter/Content/music/white light.mp3
Normal file
BIN
FreedomFighter/Content/sfx/beep.wav
Normal file
BIN
FreedomFighter/Content/sfx/explode2.wav
Normal file
BIN
FreedomFighter/Content/sfx/fire1.wav
Normal file
BIN
FreedomFighter/Content/sfx/hit4.wav
Normal file
BIN
FreedomFighter/Content/sfx/missile.wav
Normal file
BIN
FreedomFighter/Content/sfx/reload.wav
Normal file
BIN
FreedomFighter/Content/sfx/thump1.wav
Normal file
BIN
FreedomFighter/Content/tilesets/test5_00.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
FreedomFighter/Content/tilesets/test5_01.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
FreedomFighter/Content/tilesets/test5_02.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
FreedomFighter/Content/tilesets/test5_03.png
Normal file
|
After Width: | Height: | Size: 940 B |
BIN
FreedomFighter/Content/tilesets/test6_00.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
FreedomFighter/Content/tilesets/test6_01.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
FreedomFighter/Content/tilesets/test6_02.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
FreedomFighter/Content/tilesets/test_00.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
FreedomFighter/Content/tilesets/test_01.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
FreedomFighter/Content/tilesets/test_02.png
Normal file
|
After Width: | Height: | Size: 234 B |
570
FreedomFighter/Entities.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
FreedomFighter/FreedomFighter.csproj
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
FreedomFighter/HighScores.cs
Normal 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
|
After Width: | Height: | Size: 144 KiB |
114
FreedomFighter/MusicManager.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
FreedomFighter/OmegaImage.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
FreedomFighter/OmegaInput.cs
Normal 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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
FreedomFighter/app.manifest
Normal 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>
|
||||||