Can now open secret walls and pick up machine gun
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -46,9 +46,20 @@ class DoorManager {
|
|||||||
return offsets;
|
return offsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void tryOpenDoor(int x, int y) {
|
||||||
|
String key = '$x,$y';
|
||||||
|
if (doors.containsKey(key)) {
|
||||||
|
// If it's closed or closing, interact() will usually start it opening
|
||||||
|
if (doors[key]!.offset == 0.0) {
|
||||||
|
doors[key]!.interact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool isDoorOpenEnough(int x, int y) {
|
bool isDoorOpenEnough(int x, int y) {
|
||||||
String key = '$x,$y';
|
String key = '$x,$y';
|
||||||
if (doors.containsKey(key)) {
|
if (doors.containsKey(key)) {
|
||||||
|
// 0.7 offset means 70% open, similar to the original engine's check
|
||||||
return doors[key]!.offset > 0.7;
|
return doors[key]!.offset > 0.7;
|
||||||
}
|
}
|
||||||
return false; // Not a door we manage
|
return false; // Not a door we manage
|
||||||
|
|||||||
@@ -58,18 +58,26 @@ class BrownGuard extends Enemy {
|
|||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
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,
|
||||||
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
|
|
||||||
// 1. Wake up logic
|
// 1. Wake up logic (Matches SightPlayer & FirstSighting)
|
||||||
if (state == EntityState.idle &&
|
if (state == EntityState.idle &&
|
||||||
hasLineOfSight(playerPosition, isWalkable)) {
|
hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
state = EntityState.patrolling;
|
if (reactionTimeMs == 0) {
|
||||||
|
// Init reaction delay: ~1 to 4 tics in C (1 tic = ~14ms, but plays out longer in engine ticks).
|
||||||
|
// Let's approximate human-feeling reaction time: 200ms - 800ms
|
||||||
|
reactionTimeMs = elapsedMs + 200 + math.Random().nextInt(600);
|
||||||
|
} else if (elapsedMs >= reactionTimeMs) {
|
||||||
|
state =
|
||||||
|
EntityState.patrolling; // Equivalent to FirstSighting chase frame
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
|
reactionTimeMs = 0; // Reset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Pre-calculate spatial relations
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
double angleToPlayer = position.angleTo(playerPosition);
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
|
|
||||||
@@ -77,7 +85,7 @@ class BrownGuard extends Enemy {
|
|||||||
newAngle = angleToPlayer;
|
newAngle = angleToPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate Octant for sprite direction
|
// Octant logic remains the same
|
||||||
double diff = newAngle - angleToPlayer;
|
double diff = newAngle - angleToPlayer;
|
||||||
while (diff <= -math.pi) {
|
while (diff <= -math.pi) {
|
||||||
diff += 2 * math.pi;
|
diff += 2 * math.pi;
|
||||||
@@ -85,6 +93,7 @@ class BrownGuard extends Enemy {
|
|||||||
while (diff > math.pi) {
|
while (diff > math.pi) {
|
||||||
diff -= 2 * 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;
|
||||||
|
|
||||||
@@ -96,22 +105,34 @@ class BrownGuard extends Enemy {
|
|||||||
|
|
||||||
case EntityState.patrolling:
|
case EntityState.patrolling:
|
||||||
if (distance > 0.8) {
|
if (distance > 0.8) {
|
||||||
// Calculate movement intent
|
// Jitter fix: Use continuous vector movement instead of single-axis snapping
|
||||||
movement =
|
double moveX = math.cos(angleToPlayer) * speed;
|
||||||
Coordinate2D(math.cos(newAngle), math.sin(newAngle)) * speed;
|
double moveY = math.sin(angleToPlayer) * speed;
|
||||||
|
|
||||||
|
Coordinate2D intendedMovement = Coordinate2D(moveX, moveY);
|
||||||
|
|
||||||
|
// Pass tryOpenDoor down!
|
||||||
|
movement = getValidMovement(
|
||||||
|
intendedMovement,
|
||||||
|
isWalkable,
|
||||||
|
tryOpenDoor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animation fix: Update the sprite so he actually turns and walks!
|
||||||
int walkFrame = (elapsedMs ~/ 150) % 4;
|
int walkFrame = (elapsedMs ~/ 150) % 4;
|
||||||
spriteIndex = 58 + (walkFrame * 8) + octant;
|
spriteIndex = 58 + (walkFrame * 8) + octant;
|
||||||
|
|
||||||
if (distance < 5.0 && elapsedMs - lastActionTime > 2000) {
|
// Shooting fix: Give him permission to stop and shoot you
|
||||||
|
// (1500ms delay between shots)
|
||||||
|
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
|
||||||
if (hasLineOfSight(playerPosition, isWalkable)) {
|
if (hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
state = EntityState.shooting;
|
state = EntityState.shooting;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
_hasFiredThisCycle = false;
|
_hasFiredThisCycle = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break; // Fallthrough fix: Don't forget the break!
|
||||||
|
|
||||||
case EntityState.shooting:
|
case EntityState.shooting:
|
||||||
int timeShooting = elapsedMs - lastActionTime;
|
int timeShooting = elapsedMs - lastActionTime;
|
||||||
@@ -152,6 +173,7 @@ class BrownGuard extends Enemy {
|
|||||||
spriteIndex = 95;
|
spriteIndex = 95;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,21 +51,28 @@ class Dog extends Enemy {
|
|||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required void Function(int x, int y) tryOpenDoor, // NEW
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
|
|
||||||
|
// 1. Wake up logic
|
||||||
if (state == EntityState.idle &&
|
if (state == EntityState.idle &&
|
||||||
hasLineOfSight(playerPosition, isWalkable)) {
|
hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
|
if (reactionTimeMs == 0) {
|
||||||
|
reactionTimeMs = elapsedMs + 100 + math.Random().nextInt(200);
|
||||||
|
} else if (elapsedMs >= reactionTimeMs) {
|
||||||
state = EntityState.patrolling;
|
state = EntityState.patrolling;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
|
reactionTimeMs = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
double angleToPlayer = position.angleTo(playerPosition);
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
|
|
||||||
if (state == EntityState.patrolling || state == EntityState.shooting) {
|
if (state != EntityState.idle && state != EntityState.dead) {
|
||||||
newAngle = angleToPlayer;
|
newAngle = angleToPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +83,32 @@ class Dog extends Enemy {
|
|||||||
while (diff > math.pi) {
|
while (diff > math.pi) {
|
||||||
diff -= 2 * 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. Clean State Machine
|
||||||
|
switch (state) {
|
||||||
|
case EntityState.idle:
|
||||||
spriteIndex = 99 + octant;
|
spriteIndex = 99 + octant;
|
||||||
} else if (state == EntityState.patrolling) {
|
break;
|
||||||
|
|
||||||
|
case EntityState.patrolling:
|
||||||
if (distance > 0.8) {
|
if (distance > 0.8) {
|
||||||
movement = Coordinate2D(math.cos(newAngle), math.sin(newAngle)) * speed;
|
double deltaX = playerPosition.x - position.x;
|
||||||
|
double deltaY = playerPosition.y - position.y;
|
||||||
|
|
||||||
|
double moveX = deltaX > 0 ? speed : (deltaX < 0 ? -speed : 0);
|
||||||
|
double moveY = deltaY > 0 ? speed : (deltaY < 0 ? -speed : 0);
|
||||||
|
|
||||||
|
Coordinate2D intendedMovement = Coordinate2D(moveX, moveY);
|
||||||
|
|
||||||
|
// Pass tryOpenDoor down!
|
||||||
|
movement = getValidMovement(
|
||||||
|
intendedMovement,
|
||||||
|
isWalkable,
|
||||||
|
tryOpenDoor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
int walkFrame = (elapsedMs ~/ 100) % 4;
|
int walkFrame = (elapsedMs ~/ 100) % 4;
|
||||||
@@ -94,18 +119,24 @@ class Dog extends Enemy {
|
|||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
_hasBittenThisCycle = false;
|
_hasBittenThisCycle = false;
|
||||||
}
|
}
|
||||||
} else if (state == EntityState.shooting) {
|
break;
|
||||||
|
|
||||||
|
case EntityState.shooting:
|
||||||
int timeAttacking = elapsedMs - lastActionTime;
|
int timeAttacking = elapsedMs - lastActionTime;
|
||||||
if (timeAttacking < 200) {
|
if (timeAttacking < 200) {
|
||||||
spriteIndex = 139;
|
spriteIndex = 139;
|
||||||
if (!_hasBittenThisCycle) {
|
if (!_hasBittenThisCycle) {
|
||||||
onDamagePlayer(5); // DOG BITE
|
onDamagePlayer(5);
|
||||||
_hasBittenThisCycle = true;
|
_hasBittenThisCycle = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state = EntityState.patrolling;
|
state = EntityState.patrolling;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (movement: movement, newAngle: newAngle);
|
return (movement: movement, newAngle: newAngle);
|
||||||
|
|||||||
@@ -14,21 +14,22 @@ abstract class Enemy extends Entity {
|
|||||||
super.lastActionTime,
|
super.lastActionTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Standard guard health
|
|
||||||
int health = 25;
|
int health = 25;
|
||||||
int damage = 10;
|
int damage = 10;
|
||||||
bool isDying = false;
|
bool isDying = false;
|
||||||
|
bool hasDroppedItem = false;
|
||||||
|
|
||||||
|
// Replaces ob->temp2 for reaction delays
|
||||||
|
int reactionTimeMs = 0;
|
||||||
|
|
||||||
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;
|
||||||
// Mark the start of the death/pain
|
|
||||||
lastActionTime = currentTime;
|
lastActionTime = currentTime;
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
state = EntityState.dead;
|
state = EntityState.dead;
|
||||||
// This triggers the dying animation
|
|
||||||
isDying = true;
|
isDying = true;
|
||||||
} else if (math.Random().nextDouble() < 0.5) {
|
} else if (math.Random().nextDouble() < 0.5) {
|
||||||
state = EntityState.pain;
|
state = EntityState.pain;
|
||||||
@@ -37,12 +38,9 @@ abstract class Enemy extends Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
// 0=East, 1=North, 2=West, 3=South
|
|
||||||
int direction = normalizedId % 4;
|
int direction = normalizedId % 4;
|
||||||
|
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case 0:
|
case 0:
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -57,14 +55,19 @@ abstract class Enemy extends Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The enemy can now check its own line of sight!
|
// Matches WL_STATE.C's 'CheckLine' using canonical Integer DDA traversal
|
||||||
bool hasLineOfSight(
|
bool hasLineOfSight(
|
||||||
Coordinate2D playerPosition,
|
Coordinate2D playerPosition,
|
||||||
bool Function(int x, int y) isWalkable,
|
bool Function(int x, int y) isWalkable,
|
||||||
) {
|
) {
|
||||||
double distance = position.distanceTo(playerPosition);
|
// 1. Proximity Check (Matches WL_STATE.C 'MINSIGHT')
|
||||||
|
// If the player is very close, sight is automatic regardless of facing angle.
|
||||||
|
// This compensates for our lack of a noise/gunshot alert system!
|
||||||
|
if (position.distanceTo(playerPosition) < 2.0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. FOV Check
|
// 2. FOV Check (Matches original sight angles)
|
||||||
double angleToPlayer = position.angleTo(playerPosition);
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
double diff = angle - angleToPlayer;
|
double diff = angle - angleToPlayer;
|
||||||
|
|
||||||
@@ -77,40 +80,92 @@ abstract class Enemy extends Entity {
|
|||||||
|
|
||||||
if (diff.abs() > math.pi / 2) return false;
|
if (diff.abs() > math.pi / 2) return false;
|
||||||
|
|
||||||
// 2. Map Check
|
// 3. Map Check (Corrected Integer Bresenham)
|
||||||
Coordinate2D dir = (playerPosition - position).normalized;
|
int currentX = position.x.toInt();
|
||||||
double stepSize = 0.2;
|
int currentY = position.y.toInt();
|
||||||
|
int targetX = playerPosition.x.toInt();
|
||||||
|
int targetY = playerPosition.y.toInt();
|
||||||
|
|
||||||
for (double i = 0; i < distance; i += stepSize) {
|
int dx = (targetX - currentX).abs();
|
||||||
Coordinate2D checkPos = position + (dir * i);
|
int dy = -(targetY - currentY).abs();
|
||||||
if (!isWalkable(checkPos.x.toInt(), checkPos.y.toInt())) return false;
|
int sx = currentX < targetX ? 1 : -1;
|
||||||
|
int sy = currentY < targetY ? 1 : -1;
|
||||||
|
int err = dx + dy;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!isWalkable(currentX, currentY)) return false;
|
||||||
|
if (currentX == targetX && currentY == targetY) break;
|
||||||
|
|
||||||
|
int e2 = 2 * err;
|
||||||
|
if (e2 >= dy) {
|
||||||
|
err += dy;
|
||||||
|
currentX += sx;
|
||||||
|
}
|
||||||
|
if (e2 <= dx) {
|
||||||
|
err += dx;
|
||||||
|
currentY += sy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The weapon asks the enemy if it is unobstructed from the shooter
|
Coordinate2D getValidMovement(
|
||||||
bool hasLineOfSightFrom(
|
Coordinate2D intendedMovement,
|
||||||
Coordinate2D source,
|
|
||||||
double sourceAngle,
|
|
||||||
double distance,
|
|
||||||
bool Function(int x, int y) isWalkable,
|
bool Function(int x, int y) isWalkable,
|
||||||
|
void Function(int x, int y) tryOpenDoor,
|
||||||
) {
|
) {
|
||||||
double dirX = math.cos(sourceAngle);
|
double newX = position.x + intendedMovement.x;
|
||||||
double dirY = math.sin(sourceAngle);
|
double newY = position.y + intendedMovement.y;
|
||||||
|
|
||||||
for (double i = 0.5; i < distance; i += 0.2) {
|
int currentTileX = position.x.toInt();
|
||||||
int checkX = (source.x + dirX * i).toInt();
|
int currentTileY = position.y.toInt();
|
||||||
int checkY = (source.y + dirY * i).toInt();
|
int targetTileX = newX.toInt();
|
||||||
if (!isWalkable(checkX, checkY)) return false;
|
int targetTileY = newY.toInt();
|
||||||
|
|
||||||
|
bool movedX = currentTileX != targetTileX;
|
||||||
|
bool movedY = currentTileY != targetTileY;
|
||||||
|
|
||||||
|
// 1. Check Diagonal Movement
|
||||||
|
if (movedX && movedY) {
|
||||||
|
bool canMoveX = isWalkable(targetTileX, currentTileY);
|
||||||
|
bool canMoveY = isWalkable(currentTileX, targetTileY);
|
||||||
|
bool canMoveDiag = isWalkable(targetTileX, targetTileY);
|
||||||
|
|
||||||
|
if (!canMoveX || !canMoveY || !canMoveDiag) {
|
||||||
|
// Trigger doors if they are blocking the path
|
||||||
|
if (!canMoveX) tryOpenDoor(targetTileX, currentTileY);
|
||||||
|
if (!canMoveY) tryOpenDoor(currentTileX, targetTileY);
|
||||||
|
if (!canMoveDiag) tryOpenDoor(targetTileX, targetTileY);
|
||||||
|
|
||||||
|
if (canMoveX) return Coordinate2D(intendedMovement.x, 0);
|
||||||
|
if (canMoveY) return Coordinate2D(0, intendedMovement.y);
|
||||||
|
return const Coordinate2D(0, 0);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Check Cardinal Movement
|
||||||
|
if (movedX && !movedY) {
|
||||||
|
if (!isWalkable(targetTileX, currentTileY)) {
|
||||||
|
tryOpenDoor(targetTileX, currentTileY); // Try to open!
|
||||||
|
return Coordinate2D(0, intendedMovement.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (movedY && !movedX) {
|
||||||
|
if (!isWalkable(currentTileX, targetTileY)) {
|
||||||
|
tryOpenDoor(currentTileX, targetTileY); // Try to open!
|
||||||
|
return Coordinate2D(intendedMovement.x, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intendedMovement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated Signature
|
||||||
({Coordinate2D movement, double newAngle}) update({
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required void Function(int x, int y) tryOpenDoor, // NEW
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,40 @@ abstract class Entity<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Coordinate2D get position => Coordinate2D(x, y);
|
Coordinate2D get position => Coordinate2D(x, y);
|
||||||
|
|
||||||
|
// NEW: Checks if a projectile or sightline from 'source' can reach this entity
|
||||||
|
bool hasLineOfSightFrom(
|
||||||
|
Coordinate2D source,
|
||||||
|
double sourceAngle,
|
||||||
|
double distance,
|
||||||
|
bool Function(int x, int y) isWalkable,
|
||||||
|
) {
|
||||||
|
// Corrected Integer Bresenham Algorithm
|
||||||
|
int currentX = source.x.toInt();
|
||||||
|
int currentY = source.y.toInt();
|
||||||
|
int targetX = x.toInt();
|
||||||
|
int targetY = y.toInt();
|
||||||
|
|
||||||
|
int dx = (targetX - currentX).abs();
|
||||||
|
int dy = -(targetY - currentY).abs();
|
||||||
|
int sx = currentX < targetX ? 1 : -1;
|
||||||
|
int sy = currentY < targetY ? 1 : -1;
|
||||||
|
int err = dx + dy;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!isWalkable(currentX, currentY)) return false;
|
||||||
|
if (currentX == targetX && currentY == targetY) break;
|
||||||
|
|
||||||
|
int e2 = 2 * err;
|
||||||
|
if (e2 >= dy) {
|
||||||
|
err += dy;
|
||||||
|
currentX += sx;
|
||||||
|
}
|
||||||
|
if (e2 <= dx) {
|
||||||
|
err += dx;
|
||||||
|
currentY += sy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ typedef EntitySpawner =
|
|||||||
);
|
);
|
||||||
|
|
||||||
abstract class EntityRegistry {
|
abstract class EntityRegistry {
|
||||||
// Add future enemies (SSGuard, Dog, etc.) to this list!
|
|
||||||
static final List<EntitySpawner> _spawners = [
|
static final List<EntitySpawner> _spawners = [
|
||||||
Collectible.trySpawn, // Check collectibles
|
BrownGuard.trySpawn,
|
||||||
Decorative.trySpawn, // Then check decorations
|
Dog.trySpawn,
|
||||||
BrownGuard.trySpawn, // Then check guards
|
Collectible.trySpawn,
|
||||||
Dog.trySpawn, // Then check dogs
|
Decorative.trySpawn,
|
||||||
];
|
];
|
||||||
|
|
||||||
static Entity? spawn(
|
static Entity? spawn(
|
||||||
|
|||||||
121
lib/features/entities/pushwall_manager.dart
Normal file
121
lib/features/entities/pushwall_manager.dart
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_dart/classes/matrix.dart';
|
||||||
|
|
||||||
|
class Pushwall {
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
int mapId; // The wall texture ID
|
||||||
|
int dirX = 0;
|
||||||
|
int dirY = 0;
|
||||||
|
double offset = 0.0;
|
||||||
|
int tilesMoved = 0;
|
||||||
|
|
||||||
|
Pushwall(this.x, this.y, this.mapId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushwallManager {
|
||||||
|
final Map<String, Pushwall> pushwalls = {};
|
||||||
|
Pushwall? activePushwall;
|
||||||
|
|
||||||
|
void initPushwalls(Matrix<int> wallGrid, Matrix<int> objectGrid) {
|
||||||
|
pushwalls.clear();
|
||||||
|
activePushwall = null;
|
||||||
|
|
||||||
|
for (int y = 0; y < objectGrid.length; y++) {
|
||||||
|
for (int x = 0; x < objectGrid[y].length; x++) {
|
||||||
|
// Map ID 98 in the object grid marks a pushwall!
|
||||||
|
if (objectGrid[y][x] == 98) {
|
||||||
|
pushwalls['$x,$y'] = Pushwall(x, y, wallGrid[y][x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Duration elapsed, Matrix<int> wallGrid) {
|
||||||
|
if (activePushwall == null) return;
|
||||||
|
final pw = activePushwall!;
|
||||||
|
|
||||||
|
// Original logic: 1/128 tile per tick.
|
||||||
|
// At 70 ticks/sec, that is roughly 0.54 tiles per second.
|
||||||
|
const double originalSpeed = 0.546875;
|
||||||
|
pw.offset += (elapsed.inMilliseconds / 1000.0) * originalSpeed;
|
||||||
|
|
||||||
|
// Once it crosses a full tile boundary, we update the collision grid!
|
||||||
|
if (pw.offset >= 1.0) {
|
||||||
|
pw.offset -= 1.0;
|
||||||
|
pw.tilesMoved++;
|
||||||
|
|
||||||
|
int nextX = pw.x + pw.dirX;
|
||||||
|
int nextY = pw.y + pw.dirY;
|
||||||
|
|
||||||
|
// Move the solid block in the physical grid
|
||||||
|
wallGrid[nextY][nextX] = pw.mapId;
|
||||||
|
wallGrid[pw.y][pw.x] = 0; // Clear the old space so the player can walk in
|
||||||
|
|
||||||
|
// Update the dictionary key
|
||||||
|
pushwalls.remove('${pw.x},${pw.y}');
|
||||||
|
pw.x = nextX;
|
||||||
|
pw.y = nextY;
|
||||||
|
pushwalls['${pw.x},${pw.y}'] = pw;
|
||||||
|
|
||||||
|
// Check if we should keep sliding
|
||||||
|
bool blocked = false;
|
||||||
|
int checkX = pw.x + pw.dirX;
|
||||||
|
int checkY = pw.y + pw.dirY;
|
||||||
|
|
||||||
|
if (checkX < 0 ||
|
||||||
|
checkX >= wallGrid[0].length ||
|
||||||
|
checkY < 0 ||
|
||||||
|
checkY >= wallGrid.length) {
|
||||||
|
blocked = true;
|
||||||
|
} else if (wallGrid[checkY][checkX] != 0) {
|
||||||
|
blocked = true; // Blocked by another wall or a door
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Wolf3D pushwalls move exactly 2 tiles (or 1 if blocked)
|
||||||
|
if (pw.tilesMoved >= 2 || blocked) {
|
||||||
|
activePushwall = null;
|
||||||
|
pw.offset = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleInteraction(
|
||||||
|
double playerX,
|
||||||
|
double playerY,
|
||||||
|
double playerAngle,
|
||||||
|
Matrix<int> wallGrid,
|
||||||
|
) {
|
||||||
|
// Only one pushwall can move at a time in the original engine!
|
||||||
|
if (activePushwall != null) return;
|
||||||
|
|
||||||
|
int targetX = (playerX + math.cos(playerAngle)).toInt();
|
||||||
|
int targetY = (playerY + math.sin(playerAngle)).toInt();
|
||||||
|
|
||||||
|
String key = '$targetX,$targetY';
|
||||||
|
if (pushwalls.containsKey(key)) {
|
||||||
|
final pw = pushwalls[key]!;
|
||||||
|
|
||||||
|
// Determine the push direction based on the player's relative position
|
||||||
|
double dx = (targetX + 0.5) - playerX;
|
||||||
|
double dy = (targetY + 0.5) - playerY;
|
||||||
|
|
||||||
|
if (dx.abs() > dy.abs()) {
|
||||||
|
pw.dirX = dx > 0 ? 1 : -1;
|
||||||
|
pw.dirY = 0;
|
||||||
|
} else {
|
||||||
|
pw.dirX = 0;
|
||||||
|
pw.dirY = dy > 0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the tile behind the wall is empty before starting the push
|
||||||
|
int checkX = targetX + pw.dirX;
|
||||||
|
int checkY = targetY + pw.dirY;
|
||||||
|
|
||||||
|
if (wallGrid[checkY][checkX] == 0) {
|
||||||
|
activePushwall = pw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||||
import 'package:wolf_dart/features/entities/collectible.dart';
|
import 'package:wolf_dart/features/entities/collectible.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
|
||||||
import 'package:wolf_dart/features/entities/entity.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';
|
||||||
@@ -162,15 +163,20 @@ class Player {
|
|||||||
|
|
||||||
case CollectibleType.weapon:
|
case CollectibleType.weapon:
|
||||||
if (item.mapId == 50) {
|
if (item.mapId == 50) {
|
||||||
if (!weapons.containsKey(WeaponType.machineGun)) {
|
// Machine Gun Pickup
|
||||||
|
if (weapons[WeaponType.machineGun] == null) {
|
||||||
weapons[WeaponType.machineGun] = MachineGun();
|
weapons[WeaponType.machineGun] = MachineGun();
|
||||||
|
hasMachineGun = true;
|
||||||
}
|
}
|
||||||
|
// The original game ALWAYS switches to a superior weapon on pickup
|
||||||
requestWeaponSwitch(WeaponType.machineGun);
|
requestWeaponSwitch(WeaponType.machineGun);
|
||||||
pickedUp = true;
|
pickedUp = true;
|
||||||
}
|
}
|
||||||
if (item.mapId == 51) {
|
if (item.mapId == 51) {
|
||||||
if (!weapons.containsKey(WeaponType.chainGun)) {
|
// Chain Gun Pickup
|
||||||
|
if (weapons[WeaponType.chainGun] == null) {
|
||||||
weapons[WeaponType.chainGun] = ChainGun();
|
weapons[WeaponType.chainGun] = ChainGun();
|
||||||
|
hasChainGun = true;
|
||||||
}
|
}
|
||||||
requestWeaponSwitch(WeaponType.chainGun);
|
requestWeaponSwitch(WeaponType.chainGun);
|
||||||
pickedUp = true;
|
pickedUp = true;
|
||||||
@@ -199,6 +205,10 @@ class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void releaseTrigger() {
|
||||||
|
currentWeapon.releaseTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
void updateWeapon({
|
void updateWeapon({
|
||||||
required int currentTime,
|
required int currentTime,
|
||||||
@@ -212,7 +222,6 @@ class Player {
|
|||||||
if (currentWeapon.state == WeaponState.firing &&
|
if (currentWeapon.state == WeaponState.firing &&
|
||||||
oldFrame == 0 &&
|
oldFrame == 0 &&
|
||||||
currentWeapon.frameIndex == 1) {
|
currentWeapon.frameIndex == 1) {
|
||||||
// The weapon handles everything itself!
|
|
||||||
currentWeapon.performHitscan(
|
currentWeapon.performHitscan(
|
||||||
playerX: x,
|
playerX: x,
|
||||||
playerY: y,
|
playerY: y,
|
||||||
@@ -220,8 +229,28 @@ class Player {
|
|||||||
entities: entities,
|
entities: entities,
|
||||||
isWalkable: isWalkable,
|
isWalkable: isWalkable,
|
||||||
currentTime: currentTime,
|
currentTime: currentTime,
|
||||||
onEnemyKilled: (int pointsToAdd) {
|
onEnemyKilled: (Enemy killedEnemy) {
|
||||||
|
// Dynamic scoring based on the enemy type!
|
||||||
|
int pointsToAdd = 0;
|
||||||
|
|
||||||
|
switch (killedEnemy.runtimeType.toString()) {
|
||||||
|
case 'BrownGuard':
|
||||||
|
pointsToAdd = 100;
|
||||||
|
break;
|
||||||
|
case 'Dog':
|
||||||
|
pointsToAdd = 200;
|
||||||
|
break;
|
||||||
|
// You can easily plug in future enemies here!
|
||||||
|
// case 'SSOfficer': pointsToAdd = 500; break;
|
||||||
|
default:
|
||||||
|
pointsToAdd = 100; // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
score += pointsToAdd;
|
score += pointsToAdd;
|
||||||
|
// Optional: Print to console so you can see it working
|
||||||
|
print(
|
||||||
|
"Killed ${killedEnemy.runtimeType}! +$pointsToAdd (Score: $score)",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||||
import 'package:wolf_dart/classes/matrix.dart';
|
import 'package:wolf_dart/classes/matrix.dart';
|
||||||
import 'package:wolf_dart/features/entities/entity.dart';
|
import 'package:wolf_dart/features/entities/entity.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/pushwall_manager.dart'; // NEW IMPORT
|
||||||
import 'package:wolf_dart/features/player/player.dart';
|
import 'package:wolf_dart/features/player/player.dart';
|
||||||
import 'package:wolf_dart/features/renderer/color_palette.dart';
|
import 'package:wolf_dart/features/renderer/color_palette.dart';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
final Player player;
|
final Player player;
|
||||||
final double fov;
|
final double fov;
|
||||||
final Map<String, double> doorOffsets;
|
final Map<String, double> doorOffsets;
|
||||||
|
final Pushwall? activePushwall; // NEW
|
||||||
final List<Matrix<int>> sprites;
|
final List<Matrix<int>> sprites;
|
||||||
final List<Entity> entities;
|
final List<Entity> entities;
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
required this.player,
|
required this.player,
|
||||||
required this.fov,
|
required this.fov,
|
||||||
required this.doorOffsets,
|
required this.doorOffsets,
|
||||||
|
this.activePushwall, // NEW
|
||||||
required this.sprites,
|
required this.sprites,
|
||||||
required this.entities,
|
required this.entities,
|
||||||
});
|
});
|
||||||
@@ -49,13 +52,11 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
|
|
||||||
List<double> zBuffer = List.filled(renderWidth, 0.0);
|
List<double> zBuffer = List.filled(renderWidth, 0.0);
|
||||||
|
|
||||||
// --- Coordinate2D Camera Vectors ---
|
|
||||||
Coordinate2D dir = Coordinate2D(
|
Coordinate2D dir = Coordinate2D(
|
||||||
math.cos(player.angle),
|
math.cos(player.angle),
|
||||||
math.sin(player.angle),
|
math.sin(player.angle),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The camera plane is perpendicular to the direction vector, scaled by FOV
|
|
||||||
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
|
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
|
||||||
|
|
||||||
// --- 1. CAST WALLS ---
|
// --- 1. CAST WALLS ---
|
||||||
@@ -70,7 +71,7 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
double sideDistY;
|
double sideDistY;
|
||||||
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
||||||
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
||||||
double perpWallDist;
|
double perpWallDist = 0.0;
|
||||||
|
|
||||||
int stepX;
|
int stepX;
|
||||||
int stepY;
|
int stepY;
|
||||||
@@ -78,7 +79,9 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
bool hitOutOfBounds = false;
|
bool hitOutOfBounds = false;
|
||||||
int side = 0;
|
int side = 0;
|
||||||
int hitWallId = 0;
|
int hitWallId = 0;
|
||||||
double doorOffset = 0.0;
|
|
||||||
|
double textureOffset = 0.0; // Replaces doorOffset to handle both
|
||||||
|
bool customDistCalculated = false; // Flag to skip standard distance
|
||||||
Set<String> ignoredDoors = {};
|
Set<String> ignoredDoors = {};
|
||||||
|
|
||||||
if (rayDir.x < 0) {
|
if (rayDir.x < 0) {
|
||||||
@@ -115,9 +118,11 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
hit = true;
|
hit = true;
|
||||||
hitOutOfBounds = true;
|
hitOutOfBounds = true;
|
||||||
} else if (map[mapY][mapX] > 0) {
|
} else if (map[mapY][mapX] > 0) {
|
||||||
String doorKey = '$mapX,$mapY';
|
String mapKey = '$mapX,$mapY';
|
||||||
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(doorKey)) {
|
|
||||||
double currentOffset = doorOffsets[doorKey] ?? 0.0;
|
// --- DOOR LOGIC ---
|
||||||
|
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
|
||||||
|
double currentOffset = doorOffsets[mapKey] ?? 0.0;
|
||||||
if (currentOffset > 0.0) {
|
if (currentOffset > 0.0) {
|
||||||
double perpWallDistTemp = (side == 0)
|
double perpWallDistTemp = (side == 0)
|
||||||
? (sideDistX - deltaDistX)
|
? (sideDistX - deltaDistX)
|
||||||
@@ -127,24 +132,99 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
: player.x + perpWallDistTemp * rayDir.x;
|
: player.x + perpWallDistTemp * rayDir.x;
|
||||||
wallXTemp -= wallXTemp.floor();
|
wallXTemp -= wallXTemp.floor();
|
||||||
if (wallXTemp < currentOffset) {
|
if (wallXTemp < currentOffset) {
|
||||||
ignoredDoors.add(doorKey);
|
ignoredDoors.add(mapKey);
|
||||||
continue;
|
continue; // Ray passed through the open door gap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doorOffset = currentOffset;
|
|
||||||
}
|
|
||||||
hit = true;
|
hit = true;
|
||||||
hitWallId = map[mapY][mapX];
|
hitWallId = map[mapY][mapX];
|
||||||
|
textureOffset = currentOffset;
|
||||||
|
}
|
||||||
|
// --- PUSHWALL LOGIC ---
|
||||||
|
else if (activePushwall != null &&
|
||||||
|
mapX == activePushwall!.x &&
|
||||||
|
mapY == activePushwall!.y) {
|
||||||
|
hit = true;
|
||||||
|
hitWallId = map[mapY][mapX];
|
||||||
|
|
||||||
|
double pOffset = activePushwall!.offset;
|
||||||
|
int pDirX = activePushwall!.dirX;
|
||||||
|
int pDirY = activePushwall!.dirY;
|
||||||
|
|
||||||
|
perpWallDist = (side == 0)
|
||||||
|
? (sideDistX - deltaDistX)
|
||||||
|
: (sideDistY - deltaDistY);
|
||||||
|
|
||||||
|
// Did we hit the face that is being pushed deeper?
|
||||||
|
if (side == 0 && pDirX != 0) {
|
||||||
|
if (pDirX == stepX) {
|
||||||
|
double intersect = perpWallDist + pOffset * deltaDistX;
|
||||||
|
if (intersect < sideDistY) {
|
||||||
|
perpWallDist = intersect; // Hit the recessed front face
|
||||||
|
} else {
|
||||||
|
side =
|
||||||
|
1; // Missed the front face, hit the newly exposed side!
|
||||||
|
perpWallDist = sideDistY - deltaDistY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
||||||
|
}
|
||||||
|
} else if (side == 1 && pDirY != 0) {
|
||||||
|
if (pDirY == stepY) {
|
||||||
|
double intersect = perpWallDist + pOffset * deltaDistY;
|
||||||
|
if (intersect < sideDistX) {
|
||||||
|
perpWallDist = intersect;
|
||||||
|
} else {
|
||||||
|
side = 0;
|
||||||
|
perpWallDist = sideDistX - deltaDistX;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We hit the side of the sliding block. Did the ray slip behind it?
|
||||||
|
double wallFraction = (side == 0)
|
||||||
|
? player.y + perpWallDist * rayDir.y
|
||||||
|
: player.x + perpWallDist * rayDir.x;
|
||||||
|
wallFraction -= wallFraction.floor();
|
||||||
|
|
||||||
|
if (side == 0) {
|
||||||
|
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
||||||
|
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||||
|
if (hit) {
|
||||||
|
textureOffset =
|
||||||
|
pOffset * pDirY; // Stick the texture to the block
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
||||||
|
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||||
|
if (hit) {
|
||||||
|
textureOffset =
|
||||||
|
pOffset * pDirX; // Stick the texture to the block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hit) continue; // The ray slipped past! Keep looping.
|
||||||
|
customDistCalculated = true; // Lock in our custom distance math
|
||||||
|
}
|
||||||
|
// --- STANDARD WALL ---
|
||||||
|
else {
|
||||||
|
hit = true;
|
||||||
|
hitWallId = map[mapY][mapX];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hitOutOfBounds) continue;
|
if (hitOutOfBounds) continue;
|
||||||
|
|
||||||
|
// Apply standard math ONLY if we didn't calculate a sub-tile pushwall distance
|
||||||
|
if (!customDistCalculated) {
|
||||||
if (side == 0) {
|
if (side == 0) {
|
||||||
perpWallDist = (sideDistX - deltaDistX);
|
perpWallDist = (sideDistX - deltaDistX);
|
||||||
} else {
|
} else {
|
||||||
perpWallDist = (sideDistY - deltaDistY);
|
perpWallDist = (sideDistY - deltaDistY);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
zBuffer[x] = perpWallDist;
|
zBuffer[x] = perpWallDist;
|
||||||
|
|
||||||
@@ -164,15 +244,15 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
size,
|
size,
|
||||||
hitWallId,
|
hitWallId,
|
||||||
textures,
|
textures,
|
||||||
doorOffset,
|
textureOffset,
|
||||||
columnPaint,
|
columnPaint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. DRAW SPRITES ---
|
// --- 2. DRAW SPRITES ---
|
||||||
|
// (Keep your existing sprite rendering logic exactly the same)
|
||||||
List<Entity> activeSprites = List.from(entities);
|
List<Entity> activeSprites = List.from(entities);
|
||||||
|
|
||||||
// Sort sprites from furthest to closest using Coordinate2D
|
|
||||||
activeSprites.sort((a, b) {
|
activeSprites.sort((a, b) {
|
||||||
double distA = player.position.distanceTo(a.position);
|
double distA = player.position.distanceTo(a.position);
|
||||||
double distB = player.position.distanceTo(b.position);
|
double distB = player.position.distanceTo(b.position);
|
||||||
@@ -180,10 +260,8 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (Entity entity in activeSprites) {
|
for (Entity entity in activeSprites) {
|
||||||
// Relative position to player
|
|
||||||
Coordinate2D spritePos = entity.position - player.position;
|
Coordinate2D spritePos = entity.position - player.position;
|
||||||
|
|
||||||
// Transform sprite with the inverse camera matrix
|
|
||||||
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
|
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
|
||||||
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
|
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
|
||||||
double transformY =
|
double transformY =
|
||||||
@@ -245,7 +323,7 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
Size size,
|
Size size,
|
||||||
int hitWallId,
|
int hitWallId,
|
||||||
List<Matrix<int>> textures,
|
List<Matrix<int>> textures,
|
||||||
double doorOffset,
|
double textureOffset,
|
||||||
Paint paint,
|
Paint paint,
|
||||||
) {
|
) {
|
||||||
if (distance <= 0.01) distance = 0.01;
|
if (distance <= 0.01) distance = 0.01;
|
||||||
@@ -253,15 +331,19 @@ class RaycasterPainter extends CustomPainter {
|
|||||||
double wallHeight = size.height / distance;
|
double wallHeight = size.height / distance;
|
||||||
int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt();
|
int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt();
|
||||||
|
|
||||||
int texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
int texNum;
|
||||||
int texX = (wallX * 64).toInt().clamp(0, 63);
|
int texX;
|
||||||
|
|
||||||
if (hitWallId >= 90) {
|
if (hitWallId >= 90) {
|
||||||
|
// DOORS
|
||||||
texNum = 98.clamp(0, textures.length - 1);
|
texNum = 98.clamp(0, textures.length - 1);
|
||||||
texX = ((wallX - doorOffset) * 64).toInt().clamp(0, 63);
|
texX = ((wallX - textureOffset) * 64).toInt().clamp(0, 63);
|
||||||
} else {
|
} else {
|
||||||
|
// WALLS & PUSHWALLS
|
||||||
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
||||||
if (side == 1) texNum += 1;
|
if (side == 1) texNum += 1;
|
||||||
|
// We apply the modulo % 1.0 to handle negative texture offsets smoothly!
|
||||||
|
texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX;
|
if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:wolf_dart/features/entities/door_manager.dart';
|
|||||||
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
|
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
|
||||||
import 'package:wolf_dart/features/entities/entity.dart';
|
import 'package:wolf_dart/features/entities/entity.dart';
|
||||||
import 'package:wolf_dart/features/entities/entity_registry.dart';
|
import 'package:wolf_dart/features/entities/entity_registry.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/pushwall_manager.dart';
|
||||||
import 'package:wolf_dart/features/input/input_manager.dart';
|
import 'package:wolf_dart/features/input/input_manager.dart';
|
||||||
import 'package:wolf_dart/features/map/wolf_map.dart';
|
import 'package:wolf_dart/features/map/wolf_map.dart';
|
||||||
import 'package:wolf_dart/features/player/player.dart';
|
import 'package:wolf_dart/features/player/player.dart';
|
||||||
@@ -38,6 +39,7 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
final InputManager inputManager = InputManager();
|
final InputManager inputManager = InputManager();
|
||||||
final DoorManager doorManager = DoorManager();
|
final DoorManager doorManager = DoorManager();
|
||||||
|
final PushwallManager pushwallManager = PushwallManager();
|
||||||
|
|
||||||
late Ticker _gameLoop;
|
late Ticker _gameLoop;
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
@@ -68,6 +70,8 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
|
|
||||||
final Matrix<int> objectLevel = gameMap.levels[0].objectGrid;
|
final Matrix<int> objectLevel = gameMap.levels[0].objectGrid;
|
||||||
|
|
||||||
|
pushwallManager.initPushwalls(currentLevel, objectLevel);
|
||||||
|
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int y = 0; y < 64; y++) {
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int x = 0; x < 64; x++) {
|
||||||
int objId = objectLevel[y][x];
|
int objId = objectLevel[y][x];
|
||||||
@@ -180,6 +184,7 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
final inputResult = _processInputs(elapsed);
|
final inputResult = _processInputs(elapsed);
|
||||||
|
|
||||||
doorManager.update(elapsed);
|
doorManager.update(elapsed);
|
||||||
|
pushwallManager.update(elapsed, currentLevel);
|
||||||
|
|
||||||
// 2. Explicit State Updates
|
// 2. Explicit State Updates
|
||||||
player.updateWeaponSwitch();
|
player.updateWeaponSwitch();
|
||||||
@@ -214,11 +219,6 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _takeDamage(int damage) {
|
|
||||||
player.takeDamage(damage);
|
|
||||||
damageFlashOpacity = 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a Record containing both movement delta and rotation delta
|
// Returns a Record containing both movement delta and rotation delta
|
||||||
({Coordinate2D movement, double dAngle}) _processInputs(Duration elapsed) {
|
({Coordinate2D movement, double dAngle}) _processInputs(Duration elapsed) {
|
||||||
inputManager.update();
|
inputManager.update();
|
||||||
@@ -235,6 +235,8 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
|
|
||||||
if (inputManager.isFiring) {
|
if (inputManager.isFiring) {
|
||||||
player.fire(elapsed.inMilliseconds);
|
player.fire(elapsed.inMilliseconds);
|
||||||
|
} else {
|
||||||
|
player.releaseTrigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate intended rotation
|
// Calculate intended rotation
|
||||||
@@ -260,6 +262,12 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
player.y,
|
player.y,
|
||||||
player.angle,
|
player.angle,
|
||||||
);
|
);
|
||||||
|
pushwallManager.handleInteraction(
|
||||||
|
player.x,
|
||||||
|
player.y,
|
||||||
|
player.angle,
|
||||||
|
currentLevel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (movement: movement, dAngle: dAngle);
|
return (movement: movement, dAngle: dAngle);
|
||||||
@@ -301,37 +309,50 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
return Coordinate2D(newX, newY);
|
return Coordinate2D(newX, newY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderer.dart
|
|
||||||
void _updateEntities(Duration elapsed) {
|
void _updateEntities(Duration elapsed) {
|
||||||
List<Entity> itemsToRemove = [];
|
List<Entity> itemsToRemove = [];
|
||||||
|
List<Entity> itemsToAdd = []; // NEW: Buffer for dropped items
|
||||||
|
|
||||||
for (Entity entity in entities) {
|
for (Entity entity in entities) {
|
||||||
if (entity is Enemy) {
|
if (entity is Enemy) {
|
||||||
// 1. Get Intent
|
// 1. Get Intent (Now passing tryOpenDoor!)
|
||||||
final intent = entity.update(
|
final intent = entity.update(
|
||||||
elapsedMs: elapsed.inMilliseconds,
|
elapsedMs: elapsed.inMilliseconds,
|
||||||
playerPosition: player.position,
|
playerPosition: player.position,
|
||||||
isWalkable: _isWalkable,
|
isWalkable: _isWalkable,
|
||||||
onDamagePlayer: _takeDamage,
|
tryOpenDoor: doorManager.tryOpenDoor,
|
||||||
|
onDamagePlayer: (int damage) {
|
||||||
|
player.takeDamage(damage);
|
||||||
|
damageFlashOpacity = 0.5;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Update Angle
|
// 2. Update Angle
|
||||||
entity.angle = intent.newAngle;
|
entity.angle = intent.newAngle;
|
||||||
|
|
||||||
// 3. Resolve Movement & Collision
|
// 3. Resolve Movement
|
||||||
// We reuse the same logic we used for the player!
|
// We NO LONGER use _calculateValidatedPosition here!
|
||||||
Coordinate2D validatedPos = _calculateValidatedPosition(
|
// The enemy's internal getValidMovement already did the math perfectly.
|
||||||
entity.position,
|
entity.x += intent.movement.x;
|
||||||
intent.movement,
|
entity.y += intent.movement.y;
|
||||||
);
|
|
||||||
|
|
||||||
entity.position = validatedPos;
|
// 4. Handle Item Drops & Score (Matches KillActor in C code)
|
||||||
|
// Check if they just died this exact frame
|
||||||
|
if (entity.state == EntityState.dead &&
|
||||||
|
entity.isDying &&
|
||||||
|
!entity.hasDroppedItem) {
|
||||||
|
entity.hasDroppedItem = true; // Make sure we only drop once!
|
||||||
|
|
||||||
// 4. Handle Attacking (if the enemy logic decides to)
|
// You will need to add a `bool hasDroppedItem = false;` to your base Enemy class.
|
||||||
// You can move 'onDamagePlayer' calls into the enemy's
|
|
||||||
// internal state check here if preferred.
|
if (entity.runtimeType.toString() == 'BrownGuard') {
|
||||||
|
// Example: Spawn an ammo clip where the guard died
|
||||||
|
// itemsToAdd.add(Collectible(x: entity.x, y: entity.y, type: CollectibleType.ammoClip));
|
||||||
|
} else if (entity.runtimeType.toString() == 'Dog') {
|
||||||
|
// Dogs don't drop items, but maybe they give different points!
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (entity is Collectible) {
|
} else if (entity is Collectible) {
|
||||||
// Collectible pickup logic remains the same
|
|
||||||
if (player.position.distanceTo(entity.position) < 0.5) {
|
if (player.position.distanceTo(entity.position) < 0.5) {
|
||||||
if (player.tryPickup(entity)) {
|
if (player.tryPickup(entity)) {
|
||||||
itemsToRemove.add(entity);
|
itemsToRemove.add(entity);
|
||||||
@@ -340,9 +361,13 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up dead items and add new drops
|
||||||
if (itemsToRemove.isNotEmpty) {
|
if (itemsToRemove.isNotEmpty) {
|
||||||
entities.removeWhere((e) => itemsToRemove.contains(e));
|
entities.removeWhere((e) => itemsToRemove.contains(e));
|
||||||
}
|
}
|
||||||
|
if (itemsToAdd.isNotEmpty) {
|
||||||
|
entities.addAll(itemsToAdd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes an input and returns a value instead of implicitly changing state
|
// Takes an input and returns a value instead of implicitly changing state
|
||||||
@@ -392,6 +417,7 @@ class _WolfRendererState extends State<WolfRenderer>
|
|||||||
doorOffsets: doorManager.getOffsetsForRenderer(),
|
doorOffsets: doorManager.getOffsetsForRenderer(),
|
||||||
entities: entities,
|
entities: entities,
|
||||||
sprites: gameMap.sprites,
|
sprites: gameMap.sprites,
|
||||||
|
activePushwall: pushwallManager.activePushwall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ abstract class Weapon {
|
|||||||
final List<int> fireFrames;
|
final List<int> fireFrames;
|
||||||
final int damage;
|
final int damage;
|
||||||
final int msPerFrame;
|
final int msPerFrame;
|
||||||
|
final bool isAutomatic;
|
||||||
|
|
||||||
WeaponState state = WeaponState.idle;
|
WeaponState state = WeaponState.idle;
|
||||||
int frameIndex = 0;
|
int frameIndex = 0;
|
||||||
int lastFrameTime = 0;
|
int lastFrameTime = 0;
|
||||||
|
bool _triggerReleased = true;
|
||||||
|
|
||||||
Weapon({
|
Weapon({
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -25,16 +27,24 @@ abstract class Weapon {
|
|||||||
required this.fireFrames,
|
required this.fireFrames,
|
||||||
required this.damage,
|
required this.damage,
|
||||||
this.msPerFrame = 100,
|
this.msPerFrame = 100,
|
||||||
|
this.isAutomatic = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
int get currentSprite =>
|
int get currentSprite =>
|
||||||
state == WeaponState.idle ? idleSprite : fireFrames[frameIndex];
|
state == WeaponState.idle ? idleSprite : fireFrames[frameIndex];
|
||||||
|
|
||||||
|
void releaseTrigger() {
|
||||||
|
_triggerReleased = true;
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
if (!isAutomatic && !_triggerReleased) return false;
|
||||||
|
|
||||||
state = WeaponState.firing;
|
state = WeaponState.firing;
|
||||||
frameIndex = 0;
|
frameIndex = 0;
|
||||||
lastFrameTime = currentTime;
|
lastFrameTime = currentTime;
|
||||||
|
_triggerReleased = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -61,7 +71,7 @@ abstract class Weapon {
|
|||||||
required List<Entity> entities,
|
required List<Entity> entities,
|
||||||
required bool Function(int x, int y) isWalkable,
|
required bool Function(int x, int y) isWalkable,
|
||||||
required int currentTime,
|
required int currentTime,
|
||||||
required void Function(int scoreToAdd) onEnemyKilled,
|
required void Function(Enemy killedEnemy) onEnemyKilled,
|
||||||
}) {
|
}) {
|
||||||
Enemy? closestEnemy;
|
Enemy? closestEnemy;
|
||||||
double minDistance = 15.0;
|
double minDistance = 15.0;
|
||||||
@@ -85,7 +95,6 @@ abstract class Weapon {
|
|||||||
if (angleDiff.abs() < threshold) {
|
if (angleDiff.abs() < threshold) {
|
||||||
Coordinate2D source = Coordinate2D(playerX, playerY);
|
Coordinate2D source = Coordinate2D(playerX, playerY);
|
||||||
|
|
||||||
// Delegate to the enemy to check if it's visible
|
|
||||||
if (entity.hasLineOfSightFrom(
|
if (entity.hasLineOfSightFrom(
|
||||||
source,
|
source,
|
||||||
playerAngle,
|
playerAngle,
|
||||||
@@ -103,8 +112,10 @@ abstract class Weapon {
|
|||||||
|
|
||||||
if (closestEnemy != null) {
|
if (closestEnemy != null) {
|
||||||
closestEnemy.takeDamage(damage, currentTime);
|
closestEnemy.takeDamage(damage, currentTime);
|
||||||
|
// If the shot was fatal, pass the enemy back so the Player class
|
||||||
|
// can calculate the correct score based on enemy type!
|
||||||
if (closestEnemy.state == EntityState.dead) {
|
if (closestEnemy.state == EntityState.dead) {
|
||||||
onEnemyKilled(100);
|
onEnemyKilled(closestEnemy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Knife extends Weapon {
|
|||||||
fireFrames: [417, 418, 419, 420],
|
fireFrames: [417, 418, 419, 420],
|
||||||
damage: 15,
|
damage: 15,
|
||||||
msPerFrame: 120,
|
msPerFrame: 120,
|
||||||
|
isAutomatic: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ class Pistol extends Weapon {
|
|||||||
idleSprite: 421,
|
idleSprite: 421,
|
||||||
fireFrames: [422, 423, 424, 425],
|
fireFrames: [422, 423, 424, 425],
|
||||||
damage: 20,
|
damage: 20,
|
||||||
|
isAutomatic: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user