From 3c0e8f7d8a44d30c3a91e9a7bd48eb23a9da9023 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 13 Mar 2026 19:41:02 +0100 Subject: [PATCH] Added Enemy and BrownGuard classes. Added health and damage. Signed-off-by: Hans Kokx --- lib/classes/enemy.dart | 23 ++++ lib/features/enemies/brown_guard.dart | 107 +++++++++++++++++++ lib/features/renderer/renderer.dart | 148 +++++++++----------------- 3 files changed, 182 insertions(+), 96 deletions(-) create mode 100644 lib/classes/enemy.dart create mode 100644 lib/features/enemies/brown_guard.dart diff --git a/lib/classes/enemy.dart b/lib/classes/enemy.dart new file mode 100644 index 0000000..3dc393e --- /dev/null +++ b/lib/classes/enemy.dart @@ -0,0 +1,23 @@ +import 'package:wolf_dart/classes/entity.dart'; +import 'package:wolf_dart/classes/linear_coordinates.dart'; + +abstract class Enemy extends Entity { + Enemy({ + required super.x, + required super.y, + required super.spriteIndex, + super.angle, + super.state, + super.mapId, + super.lastActionTime, + }); + + // Every enemy must implement its own brain! + void update({ + required int elapsedMs, + required LinearCoordinates player, + required bool Function(int x, int y) isWalkable, + required bool Function(Entity entity) hasLineOfSight, + required void Function(int damage) onDamagePlayer, + }); +} diff --git a/lib/features/enemies/brown_guard.dart b/lib/features/enemies/brown_guard.dart new file mode 100644 index 0000000..8beb710 --- /dev/null +++ b/lib/features/enemies/brown_guard.dart @@ -0,0 +1,107 @@ +import 'dart:math' as math; + +import 'package:wolf_dart/classes/enemy.dart'; +import 'package:wolf_dart/classes/entity.dart'; +import 'package:wolf_dart/classes/linear_coordinates.dart'; + +class BrownGuard extends Enemy { + static const double speed = 0.03; + bool _hasFiredThisCycle = false; + + BrownGuard({ + required super.x, + required super.y, + required super.angle, + required super.mapId, + }) : super( + spriteIndex: 50, // Default front-facing idle + state: EntityState.idle, + ); + + @override + void update({ + required int elapsedMs, + required LinearCoordinates player, + required bool Function(int x, int y) isWalkable, + required bool Function(Entity entity) hasLineOfSight, + required void Function(int damage) onDamagePlayer, + }) { + // 1. Wake up if the player is spotted! + if (state == EntityState.idle) { + if (hasLineOfSight(this)) { + 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); + + if (state == EntityState.patrolling || state == EntityState.shooting) { + angle = angleToPlayer; + } + + 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; + + if (state == EntityState.idle) { + spriteIndex = 50 + octant; + } else if (state == EntityState.patrolling) { + // A. Move towards the player + 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; + } + + // B. Animate the walk cycle + int walkFrame = (elapsedMs ~/ 150) % 4; + spriteIndex = 58 + (walkFrame * 8) + octant; + + // C. Decide to shoot! + if (distance < 5.0 && elapsedMs - lastActionTime > 2000) { + if (hasLineOfSight(this)) { + state = EntityState.shooting; + lastActionTime = elapsedMs; + _hasFiredThisCycle = false; // Arm the weapon + } + } + } else if (state == EntityState.shooting) { + int timeShooting = elapsedMs - lastActionTime; + + if (timeShooting < 150) { + spriteIndex = 96; // Aiming + } else if (timeShooting < 300) { + spriteIndex = 97; // BANG! + + // DEAL DAMAGE ONCE! + if (!_hasFiredThisCycle) { + onDamagePlayer(10); // 10 damage per shot + _hasFiredThisCycle = true; + } + } else if (timeShooting < 450) { + spriteIndex = 98; // Recoil + } else { + state = EntityState.patrolling; + lastActionTime = elapsedMs; + } + } + } + } +} diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 512066c..16c3696 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:wolf_dart/classes/difficulty.dart'; +import 'package:wolf_dart/classes/enemy.dart'; import 'package:wolf_dart/classes/entity.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/matrix.dart'; +import 'package:wolf_dart/features/enemies/brown_guard.dart'; import 'package:wolf_dart/features/map/wolf_map.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart'; import 'package:wolf_dart/sprite_gallery.dart'; @@ -42,6 +44,9 @@ class _WolfRendererState extends State bool _isLoading = true; bool _spaceWasPressed = false; + int playerHealth = 100; + double damageFlashOpacity = 0.0; + List entities = []; // Track door animations @@ -115,12 +120,10 @@ class _WolfRendererState extends State } else if (_isGuardForDifficulty(objId)) { if (50 < gameMap.sprites.length) { entities.add( - Entity( + BrownGuard( x: x + 0.5, y: y + 0.5, - spriteIndex: 50, // Will be overridden dynamically in tick! - state: EntityState.idle, - angle: _getGuardAngle(objId), // NEW! + angle: _getGuardAngle(objId), mapId: objId, ), ); @@ -282,89 +285,21 @@ class _WolfRendererState extends State _spaceWasPressed = isSpacePressed; // --- 4. UPDATE ENTITY LOGIC --- - const double enemySpeed = 0.03; - for (Entity entity in entities) { - // 1. Wake up if the player is spotted! - if (entity.state == EntityState.idle) { - if (_hasLineOfSight(entity)) { - entity.state = EntityState.patrolling; - entity.lastActionTime = - elapsed.inMilliseconds; // Start the attack timer - } + if (entity is Enemy) { + entity.update( + elapsedMs: elapsed.inMilliseconds, + player: player, + isWalkable: _isWalkable, + hasLineOfSight: _hasLineOfSight, + onDamagePlayer: _takeDamage, // Pass the callback! + ); } + } - // 2. State-based Logic & Animation - if (entity.state == EntityState.idle || - entity.state == EntityState.patrolling || - entity.state == EntityState.shooting) { - // NEW: Include shooting state - - double dx = player.x - entity.x; - double dy = player.y - entity.y; - double distance = math.sqrt(dx * dx + dy * dy); - double angleToPlayer = math.atan2(dy, dx); - - // If patrolling or shooting, force the guard to face the player - if (entity.state == EntityState.patrolling || - entity.state == EntityState.shooting) { - entity.angle = angleToPlayer; - } - - // Calculate the octant for the 8-directional sprites (used for idle/patrol) - double diff = entity.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; - - if (entity.state == EntityState.idle) { - entity.spriteIndex = 50 + octant; - } else if (entity.state == EntityState.patrolling) { - // A. Move towards the player - if (distance > 0.8) { - double moveX = entity.x + math.cos(entity.angle) * enemySpeed; - double moveY = entity.y + math.sin(entity.angle) * enemySpeed; - - if (_isWalkable(moveX.toInt(), entity.y.toInt())) entity.x = moveX; - if (_isWalkable(entity.x.toInt(), moveY.toInt())) entity.y = moveY; - } - - // B. Animate the walk cycle - int walkFrame = (elapsed.inMilliseconds ~/ 150) % 4; - entity.spriteIndex = 58 + (walkFrame * 8) + octant; - - // C. Decide to shoot! - // If close enough (distance < 5.0) AND 2 seconds have passed since last action - if (distance < 5.0 && - elapsed.inMilliseconds - entity.lastActionTime > 2000) { - if (_hasLineOfSight(entity)) { - entity.state = EntityState.shooting; - entity.lastActionTime = - elapsed.inMilliseconds; // Reset timer for the animation - } - } - } - // NEW: The Attack Sequence! - else if (entity.state == EntityState.shooting) { - // Calculate exactly how many milliseconds we've been shooting - int timeShooting = elapsed.inMilliseconds - entity.lastActionTime; - - if (timeShooting < 150) { - entity.spriteIndex = 96; // Aiming - } else if (timeShooting < 300) { - entity.spriteIndex = 97; // BANG! (Muzzle flash) - } else if (timeShooting < 450) { - entity.spriteIndex = 98; // Recoil - } else { - // Done shooting! Go back to chasing. - entity.state = EntityState.patrolling; - entity.lastActionTime = - elapsed.inMilliseconds; // Reset cooldown timer - } - } - } + // Fade out the damage flash smoothly + if (damageFlashOpacity > 0) { + damageFlashOpacity = math.max(0.0, damageFlashOpacity - 0.05); } setState(() {}); @@ -447,6 +382,15 @@ class _WolfRendererState extends State return true; } + // A helper method to handle getting shot + void _takeDamage(int damage) { + playerHealth -= damage; + damageFlashOpacity = 0.5; // Trigger the red flash + if (playerHealth <= 0) { + print("YOU DIED! (We should add a game over screen later)"); + } + } + @override Widget build(BuildContext context) { if (_isLoading) { @@ -465,18 +409,30 @@ class _WolfRendererState extends State onKeyEvent: (_) {}, child: LayoutBuilder( builder: (context, constraints) { - return CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: RaycasterPainter( - map: currentLevel, - textures: gameMap.textures, - player: player, - playerAngle: playerAngle, - fov: fov, - doorOffsets: doorOffsets, - entities: entities, - sprites: gameMap.sprites, - ), + return Stack( + children: [ + CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: RaycasterPainter( + map: currentLevel, + textures: gameMap.textures, + player: player, + playerAngle: playerAngle, + fov: fov, + doorOffsets: doorOffsets, + entities: entities, + sprites: gameMap.sprites, + ), + ), + if (damageFlashOpacity > 0) + Positioned.fill( + child: Container( + color: Colors.red.withValues( + alpha: damageFlashOpacity, + ), + ), + ), + ], ); }, ),