Added Enemy and BrownGuard classes. Added health and damage.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-13 19:41:02 +01:00
parent 7a0143cb80
commit 3c0e8f7d8a
3 changed files with 182 additions and 96 deletions

23
lib/classes/enemy.dart Normal file
View File

@@ -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,
});
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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<WolfRenderer>
bool _isLoading = true;
bool _spaceWasPressed = false;
int playerHealth = 100;
double damageFlashOpacity = 0.0;
List<Entity> entities = [];
// Track door animations
@@ -115,12 +120,10 @@ class _WolfRendererState extends State<WolfRenderer>
} 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<WolfRenderer>
_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<WolfRenderer>
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<WolfRenderer>
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,
),
),
),
],
);
},
),