Files
alien-attack/AlientAttack.MonoGame/GameLoops/GameLoop.cs
2026-01-06 20:35:52 -05:00

393 lines
13 KiB
C#

using AlienAttack.MonoGame.Things;
using AlienAttack.MonoGame.Things.Enemies;
using AlienAttack.MonoGame.Things.Enemies.Mines;
using AlienAttack.MonoGame.Things.Stars;
using AlienAttack.MonoGame.View;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
namespace AlienAttack.MonoGame.GameLoops;
internal class GameLoop : GameLoopBase
{
private readonly List<Sprite> _spritesToAdd = [];
private readonly Random _random = new();
private int _spawnNewEnemyThreshold = 100;
private Starfield _starfield;
private Texture2D _pixel;
private Player _player;
protected readonly List<Sprite> Sprites = [];
public GameLoop(AlienAttackGame game) : base(game)
{
InitializeStarField(game);
_player = new Player(game.ViewTransform.ScreenWidth / 2 - 32, game.ViewTransform.ScreenHeight - 64);
Sprites.Add(_player);
}
private void InitializeStarField(AlienAttackGame game)
{
_starfield = new Starfield(seed: 42);
//// Far layer: many tiny, slow, faint
//_starfield.AddLayer(count: 450, speedPxPerSec: 20f, minSize: 1f, maxSize: 1.5f, minAlpha: 0.15f, maxAlpha: 0.35f);
//// Mid layer
//_starfield.AddLayer(count: 220, speedPxPerSec: 45f, minSize: 1.5f, maxSize: 2.2f, minAlpha: 0.25f, maxAlpha: 0.60f);
//// Near layer: fewer, bigger, faster, brighter
//_starfield.AddLayer(count: 90, speedPxPerSec: 85f, minSize: 2.0f, maxSize: 3.2f, minAlpha: 0.40f, maxAlpha: 0.90f);
_starfield.AddLayer(count: 450, velocityPxPerSec: new Vector2(+4f, 20f), minSize: 1f, maxSize: 1.5f, minAlpha: 0.15f, maxAlpha: 0.35f);
_starfield.AddLayer(count: 220, velocityPxPerSec: new Vector2(-10f, 45f), minSize: 1.5f, maxSize: 2.2f, minAlpha: 0.25f, maxAlpha: 0.60f);
_starfield.AddLayer(count: 90, velocityPxPerSec: new Vector2(+20f, 85f), minSize: 2.0f, maxSize: 3.2f, minAlpha: 0.40f, maxAlpha: 0.90f);
_pixel = new Texture2D(game.GraphicsDevice, 1, 1);
_pixel.SetData(new[] { Color.White });
_starfield.Initialize(ViewTransform.ScreenWidth, ViewTransform.ScreenHeight);
}
public override void OnDraw(GameTime gameTime)
{
_starfield.Draw(SpriteBatch, _pixel);
//DrawBackground();
DrawHud(gameTime);
DrawSprites();
}
private void DrawHud(GameTime gameTime)
{
double t = gameTime.TotalGameTime.TotalSeconds;
DrawSideBars(SpriteBatch, ViewTransform.ScreenWidth, ViewTransform.ScreenHeight, _player.Health, _player.Shield, t);
}
private void DrawSideBars(SpriteBatch sb, int screenW, int screenH, int health, int shield, double totalSeconds)
{
health = Math.Clamp(health, 0, 100);
shield = Math.Clamp(shield, 0, 100);
int marginTop = 10;
int marginBottom = 10;
int segments = 100;
int gap = 4;
int barWidth = 10; // thickness of the colored segments
int borderPad = 2; // thickness around the bar (border area)
int borderW = barWidth + borderPad * 2;
int barHeight = screenH - marginTop - marginBottom;
int segHeight = Math.Max(1, (barHeight - gap * (segments - 1)) / segments);
int usedHeight = segments * segHeight + (segments - 1) * gap;
int barTopY = marginTop;
int barBottomY = marginTop + usedHeight;
// Put left / right bars near screen edges
int leftBorderX = 8;
int rightBorderX = screenW - 8 - borderW;
// Colors (tweak as desired)
Color healthOn = new Color(255, 140, 0); // orange
Color shieldOn = new Color(60, 140, 255); // blue
Color off = new Color(28, 28, 28); // empty segment base
// Draw metallic frames (behind segments)
//DrawMetallicFrame(sb, new Rectangle(leftBorderX, barTopY - borderPad, borderW, usedHeight + borderPad * 2));
//DrawMetallicFrame(sb, new Rectangle(rightBorderX, barTopY - borderPad, borderW, usedHeight + borderPad * 2));
// Segment drawing X starts inside the border
int leftSegX = leftBorderX + borderPad;
int rightSegX = rightBorderX + borderPad;
// Low health pulse settings
bool lowHealth = health <= 20;
float pulse = lowHealth
? (0.85f + 0.15f * (float)Math.Sin(totalSeconds * 6.0)) // subtle wobble
: 1f;
DrawSegmentedBar(
sb, leftSegX, barBottomY, barWidth, segHeight, gap,
value0To100: health,
onColor: healthOn,
offColor: off,
segments: segments,
// pulse only on filled health segments
pulseMultiplier: pulse,
pulseFilledOnly: true
);
DrawSegmentedBar(
sb, rightSegX, barBottomY, barWidth, segHeight, gap,
value0To100: shield,
onColor: shieldOn,
offColor: off,
segments: segments,
// no pulse for shield by default
pulseMultiplier: 1f,
pulseFilledOnly: true
);
}
private void DrawSegmentedBar(
SpriteBatch sb,
int x,
int bottomY,
int barWidth,
int segHeight,
int gap,
int value0To100,
Color onColor,
Color offColor,
int segments,
float pulseMultiplier,
bool pulseFilledOnly)
{
// Gradient: bottom darker, top brighter
float minBrightness = 0.55f;
float maxBrightness = 1.00f;
for (int i = 0; i < segments; i++)
{
int y = bottomY - (i + 1) * segHeight - i * gap;
// i=0 bottom, i=segments-1 top
float t = i / (float)(segments - 1);
float brightness = MathHelper.Lerp(minBrightness, maxBrightness, t);
bool filled = i < value0To100;
Color baseColor = filled ? onColor : offColor;
// Apply pulse (usually only on the filled parts)
if (pulseMultiplier != 1f && (!pulseFilledOnly || filled))
{
brightness *= pulseMultiplier;
brightness = Math.Min(brightness, 1.2f); // allow a tiny “pop”, still safe
}
Color finalColor = ScaleRgb(baseColor, brightness);
sb.Draw(_pixel, new Rectangle(x, y, barWidth, segHeight), finalColor);
// Optional: little “shine” line on filled segments (adds a nice DOS UI feel)
// Draw a 1px highlight on the left edge of lit segments.
if (filled && barWidth >= 3)
{
var shine = ScaleRgb(Color.White, 0.10f * brightness);
sb.Draw(_pixel, new Rectangle(x, y, 1, segHeight), shine);
}
}
}
private static Color ScaleRgb(Color c, float mul)
{
mul = Math.Max(0f, mul);
byte r = (byte)Math.Clamp((int)(c.R * mul), 0, 255);
byte g = (byte)Math.Clamp((int)(c.G * mul), 0, 255);
byte b = (byte)Math.Clamp((int)(c.B * mul), 0, 255);
return new Color(r, g, b, c.A);
}
private void DrawMetallicFrame(SpriteBatch sb, Rectangle outer)
{
// “Metal” palette
Color outerBorder = new Color(10, 10, 10);
Color fill = new Color(45, 45, 45);
Color highlight = new Color(90, 90, 90); // top/left
Color shadow = new Color(20, 20, 20); // bottom/right
// Outer border
sb.Draw(_pixel, outer, outerBorder);
// Inner fill
var inner = new Rectangle(outer.X + 1, outer.Y + 1, outer.Width - 2, outer.Height - 2);
if (inner.Width > 0 && inner.Height > 0)
sb.Draw(_pixel, inner, fill);
// Bevel lines (1px)
// Top highlight
sb.Draw(_pixel, new Rectangle(inner.X, inner.Y, inner.Width, 1), highlight);
// Left highlight
sb.Draw(_pixel, new Rectangle(inner.X, inner.Y, 1, inner.Height), highlight);
// Bottom shadow
sb.Draw(_pixel, new Rectangle(inner.X, inner.Bottom - 1, inner.Width, 1), shadow);
// Right shadow
sb.Draw(_pixel, new Rectangle(inner.Right - 1, inner.Y, 1, inner.Height), shadow);
// Optional: an even tighter inner “lip” for extra metal feel
var lip = new Rectangle(inner.X + 1, inner.Y + 1, inner.Width - 2, inner.Height - 2);
if (lip.Width > 0 && lip.Height > 0)
{
Color lipDark = new Color(35, 35, 35);
sb.Draw(_pixel, lip, lipDark);
}
}
private void DrawSprites()
{
SpriteDrawArgs args = new(Game);
foreach (Sprite sprite in Sprites)
{
sprite.Draw(args);
}
}
public override void OnUpdate(GameTime gameTime)
{
_starfield.Update(gameTime);
UpdateSprites(gameTime);
CheckSpriteCollisions();
ClearDeadSprites();
AddEnemySprites();
AddNewSprites();
}
private void UpdateSprites(GameTime gameTime)
{
SpriteUpdateContext args = new(Game)
{
Random = _random,
GameTime = gameTime,
Content = Content,
SpawnSprite = _spritesToAdd.Add
};
foreach (Sprite sprite in Sprites)
{
sprite.Update(args);
}
}
private void CheckSpriteCollisions()
{
var collisionSprites = Sprites.Where(x => x.CanCollide).ToList();
foreach (var sprite in collisionSprites)
{
foreach (var otherSprite in collisionSprites.Where(x => x != sprite))
{
SpriteCollisionContext context = new(Game)
{
Sprite = otherSprite,
SpawnSprite = _spritesToAdd.Add,
Random = _random,
Content = Content
};
// TODO: If perfomed once; do not perform again
if (sprite.Intersects(otherSprite))
{
sprite.OnCollision(context);
}
}
}
}
private void ClearDeadSprites()
{
var deadSprites = Sprites.Where(x => x.IsDead).ToList();
foreach (var sprite in deadSprites)
{
Sprites.Remove(sprite);
}
}
private void AddNewSprites()
{
foreach (Sprite sprite in _spritesToAdd)
{
Sprites.Add(sprite);
}
_spritesToAdd.Clear();
}
private void AddEnemySprites()
{
if (_spawnNewEnemyThreshold > 0)
{
_spawnNewEnemyThreshold--;
}
if (_spawnNewEnemyThreshold == 0)
{
int randomNumber = _random.Next(0, 100);
if (randomNumber == 0)
{
GreenEnemy enemy = new(_random.Next(0, ViewTransform.ScreenWidth - 64), -64);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 1)
{
RedEnemy enemy = new(_random.Next(0, ViewTransform.ScreenWidth - 64), -64);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 2)
{
TealEnemy enemy = new(_random.Next(0, ViewTransform.ScreenWidth - 64), -64);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 3)
{
Enemy02Green enemy = new(_random.Next(0, ViewTransform.ScreenWidth - 64), -64);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 4)
{
Enemy02Red enemy = new(_random.Next(0, ViewTransform.ScreenWidth - 64), -64);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 5)
{
Enemy02Teal enemy = new(_random.Next(0, ViewTransform.ScreenWidth - 64), -64);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 6)
{
GreenMine enemy = new(_random.Next(0, ViewTransform.ScreenWidth - Mine.Width), -Mine.Height);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 7)
{
RedMine enemy = new(_random.Next(0, ViewTransform.ScreenWidth - Mine.Width), -Mine.Height);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
else if (randomNumber == 8)
{
BlueMine enemy = new(_random.Next(0, ViewTransform.ScreenWidth - Mine.Width), -Mine.Height);
Sprites.Add(enemy);
_spawnNewEnemyThreshold = 100 + _random.Next(0, 100);
}
}
}
}
public record DrawContext
{
public GameTime GameTime;
public SpriteBatch SpriteBatch;
public ViewTransform ViewTransoform;
}