using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace FreedomFighter; /// /// 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. /// 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); /// Marks the sprite for removal (Omega's Dead). 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); } /// /// Port of TOmegaSprite: owns all sprites, runs movement, collision /// (bounding box + optional pixel check), removal, and drawing. /// public class SpriteManager { public readonly List 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); } } } /// Pixel test on the unrotated pattern masks (rotation ignored, like a cheap approximation). 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) { // Omega drew newest sprites first, so earlier-created sprites appear on top // (e.g. the missile turret gun is spawned before its base and must cover it) for (int i = Items.Count - 1; i >= 0; i--) if (!Items[i].IsDead) Items[i].Draw(sb); } }