Moving attacks to Domain-Driven Design and Entity-Component architecture
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -14,18 +14,21 @@ abstract class Enemy extends Entity {
|
|||||||
super.lastActionTime,
|
super.lastActionTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
int health = 25; // Standard guard health
|
// Standard guard health
|
||||||
|
int health = 25;
|
||||||
bool isDying = false;
|
bool isDying = false;
|
||||||
|
|
||||||
void takeDamage(int amount, int currentTime) {
|
void takeDamage(int amount, int currentTime) {
|
||||||
if (state == EntityState.dead) return;
|
if (state == EntityState.dead) return;
|
||||||
|
|
||||||
health -= amount;
|
health -= amount;
|
||||||
lastActionTime = currentTime; // CRITICAL: Mark the start of the death/pain
|
// Mark the start of the death/pain
|
||||||
|
lastActionTime = currentTime;
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
state = EntityState.dead;
|
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) {
|
} else if (math.Random().nextDouble() < 0.5) {
|
||||||
state = EntityState.pain;
|
state = EntityState.pain;
|
||||||
} else {
|
} else {
|
||||||
@@ -36,7 +39,8 @@ abstract class Enemy extends Entity {
|
|||||||
// Decodes the Map ID to figure out which way the enemy is facing
|
// Decodes the Map ID to figure out which way the enemy is facing
|
||||||
static double getInitialAngle(int objId) {
|
static double getInitialAngle(int objId) {
|
||||||
int normalizedId = (objId - 108) % 36;
|
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) {
|
switch (direction) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -88,7 +92,24 @@ abstract class Enemy extends Entity {
|
|||||||
return true;
|
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({
|
void update({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required LinearCoordinates player,
|
required LinearCoordinates player,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:wolf_dart/classes/linear_coordinates.dart';
|
import 'package:wolf_dart/classes/linear_coordinates.dart';
|
||||||
import 'package:wolf_dart/features/entities/collectible.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/weapon.dart';
|
||||||
import 'package:wolf_dart/features/weapon/weapons/chain_gun.dart';
|
import 'package:wolf_dart/features/weapon/weapons/chain_gun.dart';
|
||||||
import 'package:wolf_dart/features/weapon/weapons/knife.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
|
/// Returns true only on the specific frame where the hit should be calculated
|
||||||
bool updateWeapon(int currentTime) {
|
void updateWeapon({
|
||||||
|
required int currentTime,
|
||||||
|
required List<Entity> entities,
|
||||||
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
}) {
|
||||||
int oldFrame = currentWeapon.frameIndex;
|
int oldFrame = currentWeapon.frameIndex;
|
||||||
currentWeapon.update(currentTime);
|
currentWeapon.update(currentTime);
|
||||||
|
|
||||||
|
// If we just crossed into the firing frame...
|
||||||
if (currentWeapon.state == WeaponState.firing &&
|
if (currentWeapon.state == WeaponState.firing &&
|
||||||
oldFrame == 0 &&
|
oldFrame == 0 &&
|
||||||
currentWeapon.frameIndex == 1) {
|
currentWeapon.frameIndex == 1) {
|
||||||
return true;
|
// The weapon handles everything itself!
|
||||||
}
|
currentWeapon.performHitscan(
|
||||||
return false;
|
playerX: x,
|
||||||
|
playerY: y,
|
||||||
|
playerAngle: angle,
|
||||||
|
entities: entities,
|
||||||
|
isWalkable: isWalkable,
|
||||||
|
currentTime: currentTime,
|
||||||
|
onEnemyKilled: (int pointsToAdd) {
|
||||||
|
score += pointsToAdd;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,9 +193,11 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity);
|
damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity);
|
||||||
|
|
||||||
// 3. Combat
|
// 3. Combat
|
||||||
if (player.updateWeapon(elapsed.inMilliseconds)) {
|
player.updateWeapon(
|
||||||
_performRaycastAttack(elapsed);
|
currentTime: elapsed.inMilliseconds,
|
||||||
}
|
entities: entities,
|
||||||
|
isWalkable: _isWalkable,
|
||||||
|
);
|
||||||
|
|
||||||
// 4. Render
|
// 4. Render
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -206,62 +208,6 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
damageFlashOpacity = 0.5;
|
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
|
// Returns movement deltas instead of modifying class variables
|
||||||
({double dx, double dy}) _processInputs(Duration elapsed) {
|
({double dx, double dy}) _processInputs(Duration elapsed) {
|
||||||
inputManager.update();
|
inputManager.update();
|
||||||
|
|||||||
@@ -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 WeaponState { idle, firing }
|
||||||
|
|
||||||
enum WeaponType { knife, pistol, machineGun, chainGun }
|
enum WeaponType { knife, pistol, machineGun, chainGun }
|
||||||
@@ -24,7 +30,6 @@ abstract class Weapon {
|
|||||||
int get currentSprite =>
|
int get currentSprite =>
|
||||||
state == WeaponState.idle ? idleSprite : fireFrames[frameIndex];
|
state == WeaponState.idle ? idleSprite : fireFrames[frameIndex];
|
||||||
|
|
||||||
/// Core firing logic. Returns true if a bullet was spent.
|
|
||||||
bool fire(int currentTime, {required int currentAmmo}) {
|
bool fire(int currentTime, {required int currentAmmo}) {
|
||||||
if (state == WeaponState.idle && currentAmmo > 0) {
|
if (state == WeaponState.idle && currentAmmo > 0) {
|
||||||
state = WeaponState.firing;
|
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<Entity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user