Kill kill kill

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-13 21:03:34 +01:00
parent d4d5a84bc4
commit 2f66ba451a
4 changed files with 158 additions and 39 deletions

View File

@@ -52,6 +52,7 @@ class BrownGuard extends Enemy {
return null; // Not a Brown Guard! return null; // Not a Brown Guard!
} }
@override
@override @override
void update({ void update({
required int elapsedMs, required int elapsedMs,
@@ -59,64 +60,59 @@ class BrownGuard extends Enemy {
required bool Function(int x, int y) isWalkable, required bool Function(int x, int y) isWalkable,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
}) { }) {
// 1. Wake up if the player is spotted! // 1. Wake up logic
if (state == EntityState.idle) { if (state == EntityState.idle && hasLineOfSight(player, isWalkable)) {
// Look how clean this is now: state = EntityState.patrolling;
if (hasLineOfSight(player, isWalkable)) { lastActionTime = elapsedMs;
state = EntityState.patrolling;
lastActionTime = elapsedMs;
}
} }
// 2. State-based Logic & Animation // 2. Pre-calculate angles (needed for almost all states)
if (state == EntityState.idle || double dx = player.x - x;
state == EntityState.patrolling || double dy = player.y - y;
state == EntityState.shooting) { double distance = math.sqrt(dx * dx + dy * dy);
double dx = player.x - x; double angleToPlayer = math.atan2(dy, dx);
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) { // Face the player if active
angle = angleToPlayer; if (state != EntityState.idle && state != EntityState.dead) {
} angle = angleToPlayer;
}
double diff = angle - angleToPlayer; double diff = angle - angleToPlayer;
while (diff <= -math.pi) { while (diff <= -math.pi) diff += 2 * math.pi;
diff += 2 * math.pi; 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; int octant = ((diff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
if (octant < 0) octant += 8; if (octant < 0) octant += 8;
if (state == EntityState.idle) { // 3. State Machine
switch (state) {
case EntityState.idle:
spriteIndex = 50 + octant; spriteIndex = 50 + octant;
} else if (state == EntityState.patrolling) { break;
case EntityState.patrolling:
// Movement
if (distance > 0.8) { if (distance > 0.8) {
double moveX = x + math.cos(angle) * speed; double moveX = x + math.cos(angle) * speed;
double moveY = y + math.sin(angle) * speed; double moveY = y + math.sin(angle) * speed;
if (isWalkable(moveX.toInt(), y.toInt())) x = moveX; if (isWalkable(moveX.toInt(), y.toInt())) x = moveX;
if (isWalkable(x.toInt(), moveY.toInt())) y = moveY; if (isWalkable(x.toInt(), moveY.toInt())) y = moveY;
} }
// Animation
int walkFrame = (elapsedMs ~/ 150) % 4; int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = 58 + (walkFrame * 8) + octant; spriteIndex = 58 + (walkFrame * 8) + octant;
// Shooting transition
if (distance < 5.0 && elapsedMs - lastActionTime > 2000) { if (distance < 5.0 && elapsedMs - lastActionTime > 2000) {
// Clean call here too:
if (hasLineOfSight(player, isWalkable)) { if (hasLineOfSight(player, isWalkable)) {
state = EntityState.shooting; state = EntityState.shooting;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
_hasFiredThisCycle = false; _hasFiredThisCycle = false;
} }
} }
} else if (state == EntityState.shooting) { break;
int timeShooting = elapsedMs - lastActionTime;
case EntityState.shooting:
int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) { if (timeShooting < 150) {
spriteIndex = 96; spriteIndex = 96;
} else if (timeShooting < 300) { } else if (timeShooting < 300) {
@@ -131,7 +127,35 @@ class BrownGuard extends Enemy {
state = EntityState.patrolling; state = EntityState.patrolling;
lastActionTime = elapsedMs; 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;
} }
} }
} }

View File

@@ -14,6 +14,25 @@ abstract class Enemy extends Entity {
super.lastActionTime, 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 // 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;

View File

@@ -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); 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) // Logic to switch weapons (e.g., picking up the Machine Gun)

View File

@@ -307,9 +307,16 @@ class _WolfRendererState extends State<WolfRenderer>
} }
// 5. Weapon // 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); player.fire(elapsed.inMilliseconds);
} }
@@ -327,6 +334,64 @@ class _WolfRendererState extends State<WolfRenderer>
damageFlashOpacity = 0.5; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading) {