From 2f66ba451a56a14857b66fcacf6b378bd4895abc Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 13 Mar 2026 21:03:34 +0100 Subject: [PATCH] Kill kill kill Signed-off-by: Hans Kokx --- .../entities/enemies/brown_guard.dart | 96 ++++++++++++------- lib/features/entities/enemies/enemy.dart | 19 ++++ lib/features/player/player.dart | 13 ++- lib/features/renderer/renderer.dart | 69 ++++++++++++- 4 files changed, 158 insertions(+), 39 deletions(-) diff --git a/lib/features/entities/enemies/brown_guard.dart b/lib/features/entities/enemies/brown_guard.dart index 1880f16..ff1dfbd 100644 --- a/lib/features/entities/enemies/brown_guard.dart +++ b/lib/features/entities/enemies/brown_guard.dart @@ -52,6 +52,7 @@ class BrownGuard extends Enemy { return null; // Not a Brown Guard! } + @override @override void update({ required int elapsedMs, @@ -59,64 +60,59 @@ class BrownGuard extends Enemy { required bool Function(int x, int y) isWalkable, required void Function(int damage) onDamagePlayer, }) { - // 1. Wake up if the player is spotted! - if (state == EntityState.idle) { - // Look how clean this is now: - if (hasLineOfSight(player, isWalkable)) { - state = EntityState.patrolling; - lastActionTime = elapsedMs; - } + // 1. Wake up logic + if (state == EntityState.idle && hasLineOfSight(player, isWalkable)) { + state = EntityState.patrolling; + lastActionTime = elapsedMs; } - // 2. State-based Logic & Animation - if (state == EntityState.idle || - state == EntityState.patrolling || - state == EntityState.shooting) { - double dx = player.x - x; - double dy = player.y - y; - double distance = math.sqrt(dx * dx + dy * dy); - double angleToPlayer = math.atan2(dy, dx); + // 2. Pre-calculate angles (needed for almost all states) + double dx = player.x - x; + double dy = player.y - y; + double distance = math.sqrt(dx * dx + dy * dy); + double angleToPlayer = math.atan2(dy, dx); - if (state == EntityState.patrolling || state == EntityState.shooting) { - angle = angleToPlayer; - } + // Face the player if active + if (state != EntityState.idle && state != EntityState.dead) { + angle = angleToPlayer; + } - double diff = angle - angleToPlayer; - while (diff <= -math.pi) { - diff += 2 * math.pi; - } - while (diff > math.pi) { - diff -= 2 * math.pi; - } + double diff = angle - angleToPlayer; + while (diff <= -math.pi) diff += 2 * math.pi; + while (diff > math.pi) diff -= 2 * math.pi; - int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; - if (octant < 0) octant += 8; + int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8; + if (octant < 0) octant += 8; - if (state == EntityState.idle) { + // 3. State Machine + switch (state) { + case EntityState.idle: spriteIndex = 50 + octant; - } else if (state == EntityState.patrolling) { + break; + + case EntityState.patrolling: + // Movement if (distance > 0.8) { double moveX = x + math.cos(angle) * speed; double moveY = y + math.sin(angle) * speed; - if (isWalkable(moveX.toInt(), y.toInt())) x = moveX; if (isWalkable(x.toInt(), moveY.toInt())) y = moveY; } - + // Animation int walkFrame = (elapsedMs ~/ 150) % 4; spriteIndex = 58 + (walkFrame * 8) + octant; - + // Shooting transition if (distance < 5.0 && elapsedMs - lastActionTime > 2000) { - // Clean call here too: if (hasLineOfSight(player, isWalkable)) { state = EntityState.shooting; lastActionTime = elapsedMs; _hasFiredThisCycle = false; } } - } else if (state == EntityState.shooting) { - int timeShooting = elapsedMs - lastActionTime; + break; + case EntityState.shooting: + int timeShooting = elapsedMs - lastActionTime; if (timeShooting < 150) { spriteIndex = 96; } else if (timeShooting < 300) { @@ -131,7 +127,35 @@ class BrownGuard extends Enemy { state = EntityState.patrolling; lastActionTime = elapsedMs; } - } + break; + + case EntityState.pain: + spriteIndex = 90; + if (elapsedMs - lastActionTime > 250) { + // Slight delay + state = EntityState.patrolling; + lastActionTime = elapsedMs; // Reset so they don't immediately shoot + } + break; + + case EntityState.dead: + if (isDying) { + // Use max(0, ...) to prevent negative numbers if currentTime is wonky + int timeSinceDeath = math.max(0, elapsedMs - lastActionTime); + int deathFrame = timeSinceDeath ~/ 150; + + if (deathFrame < 4) { + spriteIndex = 91 + deathFrame; + } else { + spriteIndex = 95; // The final corpse + isDying = false; // Stop animating + } + } else { + spriteIndex = 95; // Keep showing the corpse + } + break; + default: + break; } } } diff --git a/lib/features/entities/enemies/enemy.dart b/lib/features/entities/enemies/enemy.dart index 6c53ae1..183fb5a 100644 --- a/lib/features/entities/enemies/enemy.dart +++ b/lib/features/entities/enemies/enemy.dart @@ -14,6 +14,25 @@ abstract class Enemy extends Entity { super.lastActionTime, }); + int health = 25; // Standard guard health + bool isDying = false; + + void takeDamage(int amount, int currentTime) { + if (state == EntityState.dead) return; + + health -= amount; + lastActionTime = currentTime; // CRITICAL: Mark the start of the death/pain + + if (health <= 0) { + state = EntityState.dead; + isDying = true; // This triggers the animation in BrownGuard + } else if (math.Random().nextDouble() < 0.5) { + state = EntityState.pain; + } else { + state = EntityState.patrolling; + } + } + // Decodes the Map ID to figure out which way the enemy is facing static double getInitialAngle(int objId) { int normalizedId = (objId - 108) % 36; diff --git a/lib/features/player/player.dart b/lib/features/player/player.dart index 12de74d..acecf23 100644 --- a/lib/features/player/player.dart +++ b/lib/features/player/player.dart @@ -127,8 +127,19 @@ class Player { } } - void updateWeapon(int currentTime) { + /// Returns true only on the specific frame where the hit should be calculated + bool updateWeapon(int currentTime) { + int oldFrame = currentWeapon.frameIndex; currentWeapon.update(currentTime); + + // In your Pistol (Indices 212-215), Index 213 is the flash. + // This translates to frameIndex == 1 in our fireFrames list. + if (currentWeapon.state == WeaponState.firing && + oldFrame == 0 && + currentWeapon.frameIndex == 1) { + return true; + } + return false; } // Logic to switch weapons (e.g., picking up the Machine Gun) diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 118b5af..2b712ca 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -307,9 +307,16 @@ class _WolfRendererState extends State } // 5. Weapon - player.currentWeapon.update(elapsed.inMilliseconds); + // Update weapon animation and check for flash frame + bool shouldCheckHit = player.updateWeapon(elapsed.inMilliseconds); - if (pressedKeys.contains(LogicalKeyboardKey.controlLeft)) { + if (shouldCheckHit) { + _performRaycastAttack(elapsed); + } + + // Input to trigger firing + if (pressedKeys.contains(LogicalKeyboardKey.controlLeft) && + !_spaceWasPressed) { player.fire(elapsed.inMilliseconds); } @@ -327,6 +334,64 @@ class _WolfRendererState extends State damageFlashOpacity = 0.5; } + void _performRaycastAttack(Duration elapsed) { + Enemy? closestEnemy; + double minDistance = 15.0; // Maximum range of the gun + + for (Entity entity in entities) { + if (entity is Enemy && entity.state != EntityState.dead) { + // 1. Calculate the angle from player to enemy + double dx = entity.x - player.x; + double dy = entity.y - player.y; + double angleToEnemy = math.atan2(dy, dx); + + // 2. Check if that angle is close to our player's aiming angle + double angleDiff = player.angle - angleToEnemy; + while (angleDiff <= -math.pi) angleDiff += 2 * math.pi; + while (angleDiff > math.pi) angleDiff -= 2 * math.pi; + + // 3. Simple bounding box check (approx 0.4 units wide) + double dist = math.sqrt(dx * dx + dy * dy); + double threshold = + 0.2 / dist; // Smaller threshold as they get further away + + if (angleDiff.abs() < threshold) { + // 4. Ensure there is no wall between you and the enemy + if (_hasLineOfSightToEnemy(entity, dist)) { + if (dist < minDistance) { + minDistance = dist; + closestEnemy = entity; + } + } + } + } + } + + if (closestEnemy != null) { + closestEnemy.takeDamage( + player.currentWeapon.damage, + elapsed.inMilliseconds, + ); + + // If the shot was fatal, reward the player + if (closestEnemy.state == EntityState.dead) { + player.score += 100; + } + } + } + + bool _hasLineOfSightToEnemy(Enemy enemy, double distance) { + double dirX = math.cos(player.angle); + double dirY = math.sin(player.angle); + + for (double i = 0.5; i < distance; i += 0.2) { + int checkX = (player.x + dirX * i).toInt(); + int checkY = (player.y + dirY * i).toInt(); + if (!_isWalkable(checkX, checkY)) return false; + } + return true; + } + @override Widget build(BuildContext context) { if (_isLoading) {