diff --git a/lib/features/entities/enemies/enemy.dart b/lib/features/entities/enemies/enemy.dart index 183fb5a..8f84eac 100644 --- a/lib/features/entities/enemies/enemy.dart +++ b/lib/features/entities/enemies/enemy.dart @@ -14,18 +14,21 @@ abstract class Enemy extends Entity { super.lastActionTime, }); - int health = 25; // Standard guard health + // Standard guard health + int health = 25; 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 + // Mark the start of the death/pain + lastActionTime = currentTime; if (health <= 0) { state = EntityState.dead; - isDying = true; // This triggers the animation in BrownGuard + // This triggers the dying animation + isDying = true; } else if (math.Random().nextDouble() < 0.5) { state = EntityState.pain; } else { @@ -36,7 +39,8 @@ abstract class Enemy extends Entity { // Decodes the Map ID to figure out which way the enemy is facing static double getInitialAngle(int objId) { int normalizedId = (objId - 108) % 36; - int direction = normalizedId % 4; // 0=East, 1=North, 2=West, 3=South + // 0=East, 1=North, 2=West, 3=South + int direction = normalizedId % 4; switch (direction) { case 0: @@ -88,7 +92,24 @@ abstract class Enemy extends Entity { return true; } - // Update signature is cleaner now + // The weapon asks the enemy if it is unobstructed from the shooter + bool hasLineOfSightFrom( + LinearCoordinates source, + double sourceAngle, + double distance, + bool Function(int x, int y) isWalkable, + ) { + double dirX = math.cos(sourceAngle); + double dirY = math.sin(sourceAngle); + + for (double i = 0.5; i < distance; i += 0.2) { + int checkX = (source.x + dirX * i).toInt(); + int checkY = (source.y + dirY * i).toInt(); + if (!isWalkable(checkX, checkY)) return false; + } + return true; + } + void update({ required int elapsedMs, required LinearCoordinates player, diff --git a/lib/features/player/player.dart b/lib/features/player/player.dart index fd03c29..7082e8f 100644 --- a/lib/features/player/player.dart +++ b/lib/features/player/player.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/features/entities/collectible.dart'; +import 'package:wolf_dart/features/entities/entity.dart'; import 'package:wolf_dart/features/weapon/weapon.dart'; import 'package:wolf_dart/features/weapon/weapons/chain_gun.dart'; import 'package:wolf_dart/features/weapon/weapons/knife.dart'; @@ -194,15 +195,30 @@ class Player { } /// Returns true only on the specific frame where the hit should be calculated - bool updateWeapon(int currentTime) { + void updateWeapon({ + required int currentTime, + required List entities, + required bool Function(int x, int y) isWalkable, + }) { int oldFrame = currentWeapon.frameIndex; currentWeapon.update(currentTime); + // If we just crossed into the firing frame... if (currentWeapon.state == WeaponState.firing && oldFrame == 0 && currentWeapon.frameIndex == 1) { - return true; + // The weapon handles everything itself! + currentWeapon.performHitscan( + playerX: x, + playerY: y, + playerAngle: angle, + entities: entities, + isWalkable: isWalkable, + currentTime: currentTime, + onEnemyKilled: (int pointsToAdd) { + score += pointsToAdd; + }, + ); } - return false; } } diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 50df099..79168a7 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -193,9 +193,11 @@ class _WolfRendererState extends State damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity); // 3. Combat - if (player.updateWeapon(elapsed.inMilliseconds)) { - _performRaycastAttack(elapsed); - } + player.updateWeapon( + currentTime: elapsed.inMilliseconds, + entities: entities, + isWalkable: _isWalkable, + ); // 4. Render setState(() {}); @@ -206,62 +208,6 @@ class _WolfRendererState extends State damageFlashOpacity = 0.5; } - void _performRaycastAttack(Duration elapsed) { - Enemy? closestEnemy; - double minDistance = 15.0; - - for (Entity entity in entities) { - if (entity is Enemy && entity.state != EntityState.dead) { - double dx = entity.x - player.x; - double dy = entity.y - player.y; - double angleToEnemy = math.atan2(dy, dx); - - double angleDiff = player.angle - angleToEnemy; - while (angleDiff <= -math.pi) { - angleDiff += 2 * math.pi; - } - while (angleDiff > math.pi) { - angleDiff -= 2 * math.pi; - } - - double dist = math.sqrt(dx * dx + dy * dy); - double threshold = 0.2 / dist; - - if (angleDiff.abs() < threshold) { - if (_hasLineOfSightToEnemy(entity, dist)) { - if (dist < minDistance) { - minDistance = dist; - closestEnemy = entity; - } - } - } - } - } - - if (closestEnemy != null) { - closestEnemy.takeDamage( - player.currentWeapon.damage, - elapsed.inMilliseconds, - ); - - 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; - } - // Returns movement deltas instead of modifying class variables ({double dx, double dy}) _processInputs(Duration elapsed) { inputManager.update(); diff --git a/lib/features/weapon/weapon.dart b/lib/features/weapon/weapon.dart index 3171704..04ec3ca 100644 --- a/lib/features/weapon/weapon.dart +++ b/lib/features/weapon/weapon.dart @@ -1,3 +1,9 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/linear_coordinates.dart'; +import 'package:wolf_dart/features/entities/enemies/enemy.dart'; +import 'package:wolf_dart/features/entities/entity.dart'; + enum WeaponState { idle, firing } enum WeaponType { knife, pistol, machineGun, chainGun } @@ -24,7 +30,6 @@ abstract class Weapon { int get currentSprite => state == WeaponState.idle ? idleSprite : fireFrames[frameIndex]; - /// Core firing logic. Returns true if a bullet was spent. bool fire(int currentTime, {required int currentAmmo}) { if (state == WeaponState.idle && currentAmmo > 0) { state = WeaponState.firing; @@ -47,4 +52,60 @@ abstract class Weapon { } } } + + // NEW: The weapon calculates its own hits and applies damage! + void performHitscan({ + required double playerX, + required double playerY, + required double playerAngle, + required List entities, + required bool Function(int x, int y) isWalkable, + required int currentTime, + required void Function(int scoreToAdd) onEnemyKilled, + }) { + Enemy? closestEnemy; + double minDistance = 15.0; + + for (Entity entity in entities) { + if (entity is Enemy && entity.state != EntityState.dead) { + double dx = entity.x - playerX; + double dy = entity.y - playerY; + double angleToEnemy = math.atan2(dy, dx); + + double angleDiff = playerAngle - angleToEnemy; + while (angleDiff <= -math.pi) { + angleDiff += 2 * math.pi; + } + while (angleDiff > math.pi) { + angleDiff -= 2 * math.pi; + } + double dist = math.sqrt(dx * dx + dy * dy); + double threshold = 0.2 / dist; + + if (angleDiff.abs() < threshold) { + LinearCoordinates source = (x: playerX, y: playerY); + + // Delegate to the enemy to check if it's visible + if (entity.hasLineOfSightFrom( + source, + playerAngle, + dist, + isWalkable, + )) { + if (dist < minDistance) { + minDistance = dist; + closestEnemy = entity; + } + } + } + } + } + + if (closestEnemy != null) { + closestEnemy.takeDamage(damage, currentTime); + if (closestEnemy.state == EntityState.dead) { + onEnemyKilled(100); + } + } + } }