Create a Decorative object class

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-13 19:54:22 +01:00
parent 3c0e8f7d8a
commit 7835a6051e
4 changed files with 122 additions and 125 deletions

View File

@@ -0,0 +1,22 @@
import 'package:wolf_dart/classes/entity.dart';
class Decorative extends Entity {
Decorative({
required super.x,
required super.y,
required super.spriteIndex,
required super.mapId,
super.state = EntityState.staticObj, // Defaults to static
});
// Checks if the Map ID belongs to a standard decoration
static bool isDecoration(int objId) {
return (objId >= 23 && objId <= 70) || objId == 124;
}
// Safely calculates the VSWAP sprite index for standard decorations
static int getSpriteIndex(int objId) {
if (objId == 124) return 95; // Dead guard
return objId - 21;
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:wolf_dart/classes/entity.dart'; import 'package:wolf_dart/classes/entity.dart';
import 'package:wolf_dart/classes/linear_coordinates.dart'; import 'package:wolf_dart/classes/linear_coordinates.dart';
@@ -12,12 +14,66 @@ abstract class Enemy extends Entity {
super.lastActionTime, super.lastActionTime,
}); });
// Every enemy must implement its own brain! // 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
switch (direction) {
case 0:
return 0.0;
case 1:
return 3 * math.pi / 2;
case 2:
return math.pi;
case 3:
return math.pi / 2;
default:
return 0.0;
}
}
// The enemy can now check its own line of sight!
bool hasLineOfSight(
LinearCoordinates player,
bool Function(int x, int y) isWalkable,
) {
double dx = player.x - x;
double dy = player.y - y;
double distance = math.sqrt(dx * dx + dy * dy);
// 1. FOV Check
double angleToPlayer = math.atan2(dy, dx);
double diff = angle - angleToPlayer;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
if (diff.abs() > math.pi / 2) return false;
// 2. Map Check
double dirX = dx / distance;
double dirY = dy / distance;
double stepSize = 0.2;
for (double i = 0; i < distance; i += stepSize) {
int checkX = (x + dirX * i).toInt();
int checkY = (y + dirY * i).toInt();
if (!isWalkable(checkX, checkY)) return false;
}
return true;
}
// Update signature is cleaner now
void update({ void update({
required int elapsedMs, required int elapsedMs,
required LinearCoordinates player, required LinearCoordinates player,
required bool Function(int x, int y) isWalkable, required bool Function(int x, int y) isWalkable,
required bool Function(Entity entity) hasLineOfSight,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
}); });
} }

View File

@@ -18,17 +18,33 @@ class BrownGuard extends Enemy {
state: EntityState.idle, state: EntityState.idle,
); );
// Checks if a Map ID is a valid Brown Guard for the selected difficulty
static bool isSpawnableForDifficulty(int objId, int difficultyLevel) {
switch (difficultyLevel) {
case 0:
return objId >= 108 && objId <= 115;
case 1:
return objId >= 144 && objId <= 151;
case 2:
return objId >= 180 && objId <= 187;
case 3:
return objId >= 216 && objId <= 223;
default:
return false;
}
}
@override @override
void update({ void update({
required int elapsedMs, required int elapsedMs,
required LinearCoordinates player, required LinearCoordinates player,
required bool Function(int x, int y) isWalkable, required bool Function(int x, int y) isWalkable,
required bool Function(Entity entity) hasLineOfSight,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
}) { }) {
// 1. Wake up if the player is spotted! // 1. Wake up if the player is spotted!
if (state == EntityState.idle) { if (state == EntityState.idle) {
if (hasLineOfSight(this)) { // Look how clean this is now:
if (hasLineOfSight(player, isWalkable)) {
state = EntityState.patrolling; state = EntityState.patrolling;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
} }
@@ -48,12 +64,8 @@ class BrownGuard extends Enemy {
} }
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;
@@ -61,7 +73,6 @@ class BrownGuard extends Enemy {
if (state == EntityState.idle) { if (state == EntityState.idle) {
spriteIndex = 50 + octant; spriteIndex = 50 + octant;
} else if (state == EntityState.patrolling) { } else if (state == EntityState.patrolling) {
// A. Move towards the player
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;
@@ -70,33 +81,30 @@ class BrownGuard extends Enemy {
if (isWalkable(x.toInt(), moveY.toInt())) y = moveY; if (isWalkable(x.toInt(), moveY.toInt())) y = moveY;
} }
// B. Animate the walk cycle
int walkFrame = (elapsedMs ~/ 150) % 4; int walkFrame = (elapsedMs ~/ 150) % 4;
spriteIndex = 58 + (walkFrame * 8) + octant; spriteIndex = 58 + (walkFrame * 8) + octant;
// C. Decide to shoot!
if (distance < 5.0 && elapsedMs - lastActionTime > 2000) { if (distance < 5.0 && elapsedMs - lastActionTime > 2000) {
if (hasLineOfSight(this)) { // Clean call here too:
if (hasLineOfSight(player, isWalkable)) {
state = EntityState.shooting; state = EntityState.shooting;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;
_hasFiredThisCycle = false; // Arm the weapon _hasFiredThisCycle = false;
} }
} }
} else if (state == EntityState.shooting) { } else if (state == EntityState.shooting) {
int timeShooting = elapsedMs - lastActionTime; int timeShooting = elapsedMs - lastActionTime;
if (timeShooting < 150) { if (timeShooting < 150) {
spriteIndex = 96; // Aiming spriteIndex = 96;
} else if (timeShooting < 300) { } else if (timeShooting < 300) {
spriteIndex = 97; // BANG! spriteIndex = 97;
// DEAL DAMAGE ONCE!
if (!_hasFiredThisCycle) { if (!_hasFiredThisCycle) {
onDamagePlayer(10); // 10 damage per shot onDamagePlayer(10);
_hasFiredThisCycle = true; _hasFiredThisCycle = true;
} }
} else if (timeShooting < 450) { } else if (timeShooting < 450) {
spriteIndex = 98; // Recoil spriteIndex = 98;
} else { } else {
state = EntityState.patrolling; state = EntityState.patrolling;
lastActionTime = elapsedMs; lastActionTime = elapsedMs;

View File

@@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:wolf_dart/classes/decorative.dart';
import 'package:wolf_dart/classes/difficulty.dart'; import 'package:wolf_dart/classes/difficulty.dart';
import 'package:wolf_dart/classes/enemy.dart'; import 'package:wolf_dart/classes/enemy.dart';
import 'package:wolf_dart/classes/entity.dart'; import 'package:wolf_dart/classes/entity.dart';
@@ -87,43 +88,31 @@ class _WolfRendererState extends State<WolfRenderer>
case 22: case 22:
playerAngle = math.pi; playerAngle = math.pi;
} }
} } // 1. POPULATE DECORATIONS & DEAD BODIES
// NEW: Populate the Entities! else if (Decorative.isDecoration(objId)) {
else if (objId >= 23 && objId <= 70) { int spriteIdx = Decorative.getSpriteIndex(objId);
int calculatedSpriteIndex = objId - 21; if (spriteIdx >= 0 && spriteIdx < gameMap.sprites.length) {
if (calculatedSpriteIndex >= 0 &&
calculatedSpriteIndex < gameMap.sprites.length) {
entities.add( entities.add(
Entity( Decorative(
x: x + 0.5, x: x + 0.5,
y: y + 0.5, y: y + 0.5,
spriteIndex: calculatedSpriteIndex, spriteIndex: spriteIdx,
state: EntityState.staticObj,
mapId: objId, mapId: objId,
// NEW: Dynamically assign the state!
state: objId == 124 ? EntityState.dead : EntityState.staticObj,
), ),
); );
} }
} } else if (BrownGuard.isSpawnableForDifficulty(
// NEW: The Dead Guard (FIXED INDEX) objId,
else if (objId == 124) { widget.difficulty.level,
if (95 < gameMap.sprites.length) { )) {
entities.add(
Entity(
x: x + 0.5,
y: y + 0.5,
spriteIndex: 95,
state: EntityState.dead,
mapId: objId,
),
);
}
} else if (_isGuardForDifficulty(objId)) {
if (50 < gameMap.sprites.length) { if (50 < gameMap.sprites.length) {
entities.add( entities.add(
BrownGuard( BrownGuard(
x: x + 0.5, x: x + 0.5,
y: y + 0.5, y: y + 0.5,
angle: _getGuardAngle(objId), angle: Enemy.getInitialAngle(objId),
mapId: objId, mapId: objId,
), ),
); );
@@ -291,8 +280,7 @@ class _WolfRendererState extends State<WolfRenderer>
elapsedMs: elapsed.inMilliseconds, elapsedMs: elapsed.inMilliseconds,
player: player, player: player,
isWalkable: _isWalkable, isWalkable: _isWalkable,
hasLineOfSight: _hasLineOfSight, onDamagePlayer: _takeDamage,
onDamagePlayer: _takeDamage, // Pass the callback!
); );
} }
} }
@@ -305,83 +293,6 @@ class _WolfRendererState extends State<WolfRenderer>
setState(() {}); setState(() {});
} }
bool _isGuardForDifficulty(int objId) {
switch (widget.difficulty.level) {
case 0: // Baby
return objId >= 108 && objId <= 115;
case 1: // Easy
return objId >= 144 && objId <= 151;
case 2: // Normal
return objId >= 180 && objId <= 187;
case 3: // Hard
return objId >= 216 && objId <= 223;
default:
return false;
}
}
// Decodes the Map ID to figure out which way the guard is facing
double _getGuardAngle(int objId) {
// Normalizes the ID across the 4 difficulty tiers
int normalizedId = (objId - 108) % 36;
int direction = normalizedId % 4; // 0=East, 1=North, 2=West, 3=South
// Matches the player spawn angles you already set up
switch (direction) {
case 0:
return 0.0; // East
case 1:
return 3 * math.pi / 2; // North
case 2:
return math.pi; // West
case 3:
return math.pi / 2; // South
default:
return 0.0;
}
}
bool _hasLineOfSight(Entity guard) {
double dx = player.x - guard.x;
double dy = player.y - guard.y;
double distance = math.sqrt(dx * dx + dy * dy);
// 1. FOV Check (Are you in front of them?)
double angleToPlayer = math.atan2(dy, dx);
double diff = guard.angle - angleToPlayer;
while (diff <= -math.pi) {
diff += 2 * math.pi;
}
while (diff > math.pi) {
diff -= 2 * math.pi;
}
// A standard guard FOV is about 180 degrees (90 degrees left/right)
if (diff.abs() > math.pi / 2) {
return false; // You are behind them!
}
// 2. Line of Sight Check (Are there walls in the way?)
double dirX = dx / distance;
double dirY = dy / distance;
// Step along the ray in small increments to check for solid blocks
double stepSize = 0.2;
for (double i = 0; i < distance; i += stepSize) {
int checkX = (guard.x + dirX * i).toInt();
int checkY = (guard.y + dirY * i).toInt();
// If we hit a solid wall or closed door, vision is blocked
if (!_isWalkable(checkX, checkY)) {
return false;
}
}
// If we made it all the way to the player without hitting a wall...
return true;
}
// A helper method to handle getting shot // A helper method to handle getting shot
void _takeDamage(int damage) { void _takeDamage(int damage) {
playerHealth -= damage; playerHealth -= damage;