Adding more enemy types
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -12,9 +12,13 @@ class Decorative extends Entity {
|
|||||||
|
|
||||||
// Checks if the Map ID belongs to a standard decoration
|
// Checks if the Map ID belongs to a standard decoration
|
||||||
static bool isDecoration(int objId) {
|
static bool isDecoration(int objId) {
|
||||||
if (objId == 124) return true; // Dead guard
|
// ID 124 is a dead guard in WL1, but an SS guard in WL6.
|
||||||
|
// However, for spawning purposes, if the SS trySpawn fails,
|
||||||
|
// we only want to treat it as a decoration if it's not a live actor.
|
||||||
|
if (objId == 124 || objId == 125) return true;
|
||||||
|
|
||||||
if (objId >= 23 && objId <= 70) {
|
if (objId >= 23 && objId <= 70) {
|
||||||
// Exclude the collectibles!
|
// Exclude collectibles defined in MapObject
|
||||||
if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) {
|
if ((objId >= 43 && objId <= 44) || (objId >= 47 && objId <= 56)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -23,9 +27,11 @@ class Decorative extends Entity {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safely calculates the VSWAP sprite index for standard decorations
|
|
||||||
static int getSpriteIndex(int objId) {
|
static int getSpriteIndex(int objId) {
|
||||||
if (objId == 124) return 95; // Dead guard
|
if (objId == 124) return 95; // Dead guard sprite index
|
||||||
|
if (objId == 125) return 96; // Dead Aardwolf/Other body
|
||||||
|
|
||||||
|
// Standard decorations are typically offset by 21 in the VSWAP
|
||||||
return objId - 21;
|
return objId - 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,17 @@ class Dog extends Enemy {
|
|||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Dog? trySpawn(int objId, double x, double y, Difficulty difficulty) {
|
static Dog? trySpawn(int objId, double x, double y, Difficulty _) {
|
||||||
// The renderer already checked difficulty, so we just check the ID block!
|
// Dogs span 216 to 251.
|
||||||
if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 17) {
|
if (objId >= MapObject.dogStart && objId <= MapObject.dogStart + 35) {
|
||||||
|
bool isPatrolling = objId >= MapObject.dogStart + 18;
|
||||||
|
|
||||||
return Dog(
|
return Dog(
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
angle: MapObject.getAngle(objId),
|
angle: MapObject.getAngle(objId),
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
);
|
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -61,8 +63,12 @@ class Dog extends Enemy {
|
|||||||
|
|
||||||
double diff = angleToPlayer - newAngle;
|
double diff = angleToPlayer - newAngle;
|
||||||
|
|
||||||
while (diff <= -math.pi) diff += 2 * math.pi;
|
while (diff <= -math.pi) {
|
||||||
while (diff > math.pi) diff -= 2 * 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;
|
||||||
@@ -92,13 +98,13 @@ class Dog extends Enemy {
|
|||||||
spriteIndex = 107 + (walkFrame * 8) + octant;
|
spriteIndex = 107 + (walkFrame * 8) + octant;
|
||||||
|
|
||||||
if (distance < 1.0 && elapsedMs - lastActionTime > 1000) {
|
if (distance < 1.0 && elapsedMs - lastActionTime > 1000) {
|
||||||
state = EntityState.shooting;
|
state = EntityState.attacking;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
_hasBittenThisCycle = false;
|
_hasBittenThisCycle = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EntityState.shooting:
|
case EntityState.attacking:
|
||||||
int timeAttacking = elapsedMs - lastActionTime;
|
int timeAttacking = elapsedMs - lastActionTime;
|
||||||
if (timeAttacking < 200) {
|
if (timeAttacking < 200) {
|
||||||
spriteIndex = 139;
|
spriteIndex = 139;
|
||||||
|
|||||||
@@ -38,23 +38,6 @@ abstract class Enemy extends Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static double getInitialAngle(int objId) {
|
|
||||||
int normalizedId = (objId - 108) % 36;
|
|
||||||
int direction = normalizedId % 4;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void checkWakeUp({
|
void checkWakeUp({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
required Coordinate2D playerPosition,
|
required Coordinate2D playerPosition,
|
||||||
@@ -187,7 +170,7 @@ abstract class Enemy extends Entity {
|
|||||||
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 x, int y) tryOpenDoor,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,35 +6,35 @@ 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/map_objects.dart';
|
import 'package:wolf_dart/features/entities/map_objects.dart';
|
||||||
|
|
||||||
class BrownGuard extends Enemy {
|
class Guard extends Enemy {
|
||||||
static const double speed = 0.03;
|
static const double speed = 0.03;
|
||||||
bool _hasFiredThisCycle = false;
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
BrownGuard({
|
Guard({
|
||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
}) : super(
|
}) : super(
|
||||||
// Default front-facing idle
|
|
||||||
spriteIndex: 50,
|
spriteIndex: 50,
|
||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
);
|
);
|
||||||
|
|
||||||
static BrownGuard? trySpawn(
|
static Guard? trySpawn(int objId, double x, double y, Difficulty difficulty) {
|
||||||
int objId,
|
// Guards span 108 to 143. (124 and 125 are decorative dead bodies).
|
||||||
double x,
|
if (objId >= MapObject.guardStart &&
|
||||||
double y,
|
objId <= MapObject.guardStart + 35 &&
|
||||||
Difficulty difficulty,
|
objId != 124 &&
|
||||||
) {
|
objId != 125) {
|
||||||
// Use the range constants we defined in MapObject!
|
// If the ID is in the second half of the block, it's a Patrolling guard
|
||||||
if (objId >= MapObject.guardStart && objId <= MapObject.guardStart + 17) {
|
bool isPatrolling = objId >= MapObject.guardStart + 18;
|
||||||
return BrownGuard(
|
|
||||||
|
return Guard(
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
angle: MapObject.getAngle(objId),
|
angle: MapObject.getAngle(objId),
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
);
|
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,12 @@ class BrownGuard extends Enemy {
|
|||||||
// Octant logic (Directional sprites)
|
// Octant logic (Directional sprites)
|
||||||
double diff = angleToPlayer - newAngle;
|
double diff = angleToPlayer - newAngle;
|
||||||
|
|
||||||
while (diff <= -math.pi) diff += 2 * math.pi;
|
while (diff <= -math.pi) {
|
||||||
while (diff > math.pi) diff -= 2 * 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;
|
||||||
@@ -96,14 +100,14 @@ class BrownGuard extends Enemy {
|
|||||||
|
|
||||||
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
|
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
|
||||||
if (hasLineOfSight(playerPosition, isWalkable)) {
|
if (hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
state = EntityState.shooting;
|
state = EntityState.attacking;
|
||||||
lastActionTime = elapsedMs;
|
lastActionTime = elapsedMs;
|
||||||
_hasFiredThisCycle = false;
|
_hasFiredThisCycle = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EntityState.shooting:
|
case EntityState.attacking:
|
||||||
int timeShooting = elapsedMs - lastActionTime;
|
int timeShooting = elapsedMs - lastActionTime;
|
||||||
if (timeShooting < 150) {
|
if (timeShooting < 150) {
|
||||||
spriteIndex = 96; // Aiming
|
spriteIndex = 96; // Aiming
|
||||||
156
lib/features/entities/enemies/mutant.dart
Normal file
156
lib/features/entities/enemies/mutant.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||||
|
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/entity.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/map_objects.dart';
|
||||||
|
|
||||||
|
class Mutant extends Enemy {
|
||||||
|
static const double speed = 0.045;
|
||||||
|
static const int _baseSprite = 187;
|
||||||
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
|
Mutant({
|
||||||
|
required super.x,
|
||||||
|
required super.y,
|
||||||
|
required super.angle,
|
||||||
|
required super.mapId,
|
||||||
|
}) : super(
|
||||||
|
spriteIndex: _baseSprite,
|
||||||
|
state: EntityState.idle,
|
||||||
|
) {
|
||||||
|
health = 45;
|
||||||
|
damage = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Mutant? trySpawn(
|
||||||
|
int objId,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
Difficulty difficulty,
|
||||||
|
) {
|
||||||
|
// Mutants span 252 to 287
|
||||||
|
if (objId >= MapObject.mutantStart && objId <= MapObject.mutantStart + 35) {
|
||||||
|
bool isPatrolling = objId >= MapObject.mutantStart + 18;
|
||||||
|
|
||||||
|
return Mutant(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
angle: MapObject.getAngle(objId),
|
||||||
|
mapId: objId,
|
||||||
|
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
|
required int elapsedMs,
|
||||||
|
required Coordinate2D playerPosition,
|
||||||
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required void Function(int damage) onDamagePlayer,
|
||||||
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
|
}) {
|
||||||
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
|
double newAngle = angle;
|
||||||
|
|
||||||
|
// Mutants don't make wake-up noises in the original game!
|
||||||
|
checkWakeUp(
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
playerPosition: playerPosition,
|
||||||
|
isWalkable: isWalkable,
|
||||||
|
);
|
||||||
|
|
||||||
|
double distance = position.distanceTo(playerPosition);
|
||||||
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
|
|
||||||
|
if (state != EntityState.idle && state != EntityState.dead) {
|
||||||
|
newAngle = angleToPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
double diff = angleToPlayer - newAngle;
|
||||||
|
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;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case EntityState.idle:
|
||||||
|
spriteIndex = _baseSprite + octant;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.patrolling:
|
||||||
|
if (distance > 0.8) {
|
||||||
|
double moveX = math.cos(angleToPlayer) * speed;
|
||||||
|
double moveY = math.sin(angleToPlayer) * speed;
|
||||||
|
movement = getValidMovement(
|
||||||
|
Coordinate2D(moveX, moveY),
|
||||||
|
isWalkable,
|
||||||
|
tryOpenDoor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int walkFrame = (elapsedMs ~/ 150) % 4;
|
||||||
|
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
|
||||||
|
|
||||||
|
if (distance < 6.0 && elapsedMs - lastActionTime > 1000) {
|
||||||
|
if (hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
|
state = EntityState.attacking;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
_hasFiredThisCycle = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.attacking:
|
||||||
|
int timeShooting = elapsedMs - lastActionTime;
|
||||||
|
if (timeShooting < 150) {
|
||||||
|
spriteIndex = _baseSprite + 46; // Aiming
|
||||||
|
} else if (timeShooting < 300) {
|
||||||
|
spriteIndex = _baseSprite + 47; // Firing
|
||||||
|
if (!_hasFiredThisCycle) {
|
||||||
|
onDamagePlayer(damage);
|
||||||
|
_hasFiredThisCycle = true;
|
||||||
|
}
|
||||||
|
} else if (timeShooting < 450) {
|
||||||
|
spriteIndex = _baseSprite + 48; // Recoil
|
||||||
|
} else {
|
||||||
|
state = EntityState.patrolling;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.pain:
|
||||||
|
spriteIndex = _baseSprite + 44;
|
||||||
|
if (elapsedMs - lastActionTime > 250) {
|
||||||
|
state = EntityState.patrolling;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.dead:
|
||||||
|
if (isDying) {
|
||||||
|
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
|
||||||
|
if (deathFrame < 4) {
|
||||||
|
spriteIndex = (_baseSprite + 40) + deathFrame;
|
||||||
|
} else {
|
||||||
|
spriteIndex = _baseSprite + 45;
|
||||||
|
isDying = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spriteIndex = _baseSprite + 45;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (movement: movement, newAngle: newAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
156
lib/features/entities/enemies/officer.dart
Normal file
156
lib/features/entities/enemies/officer.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||||
|
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/entity.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/map_objects.dart';
|
||||||
|
|
||||||
|
class Officer extends Enemy {
|
||||||
|
static const double speed = 0.055;
|
||||||
|
static const int _baseSprite = 50;
|
||||||
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
|
Officer({
|
||||||
|
required super.x,
|
||||||
|
required super.y,
|
||||||
|
required super.angle,
|
||||||
|
required super.mapId,
|
||||||
|
}) : super(
|
||||||
|
spriteIndex: _baseSprite,
|
||||||
|
state: EntityState.idle,
|
||||||
|
) {
|
||||||
|
health = 50;
|
||||||
|
damage = 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Officer? trySpawn(
|
||||||
|
int objId,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
Difficulty difficulty,
|
||||||
|
) {
|
||||||
|
if (objId >= MapObject.officerStart &&
|
||||||
|
objId <= MapObject.officerStart + 35) {
|
||||||
|
bool isPatrolling = objId >= MapObject.officerStart + 18;
|
||||||
|
|
||||||
|
return Officer(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
angle: MapObject.getAngle(objId),
|
||||||
|
mapId: objId,
|
||||||
|
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
|
required int elapsedMs,
|
||||||
|
required Coordinate2D playerPosition,
|
||||||
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required void Function(int damage) onDamagePlayer,
|
||||||
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
|
}) {
|
||||||
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
|
double newAngle = angle;
|
||||||
|
|
||||||
|
checkWakeUp(
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
playerPosition: playerPosition,
|
||||||
|
isWalkable: isWalkable,
|
||||||
|
);
|
||||||
|
|
||||||
|
double distance = position.distanceTo(playerPosition);
|
||||||
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
|
|
||||||
|
if (state != EntityState.idle && state != EntityState.dead) {
|
||||||
|
newAngle = angleToPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
double diff = angleToPlayer - newAngle;
|
||||||
|
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;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case EntityState.idle:
|
||||||
|
spriteIndex = _baseSprite + octant;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.patrolling:
|
||||||
|
if (distance > 0.8) {
|
||||||
|
double moveX = math.cos(angleToPlayer) * speed;
|
||||||
|
double moveY = math.sin(angleToPlayer) * speed;
|
||||||
|
movement = getValidMovement(
|
||||||
|
Coordinate2D(moveX, moveY),
|
||||||
|
isWalkable,
|
||||||
|
tryOpenDoor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int walkFrame = (elapsedMs ~/ 150) % 4;
|
||||||
|
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
|
||||||
|
|
||||||
|
if (distance < 6.0 && elapsedMs - lastActionTime > 1000) {
|
||||||
|
if (hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
|
state = EntityState.attacking;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
_hasFiredThisCycle = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.attacking:
|
||||||
|
int timeShooting = elapsedMs - lastActionTime;
|
||||||
|
if (timeShooting < 150) {
|
||||||
|
spriteIndex = _baseSprite + 46; // Aiming
|
||||||
|
} else if (timeShooting < 300) {
|
||||||
|
spriteIndex = _baseSprite + 47; // Firing
|
||||||
|
if (!_hasFiredThisCycle) {
|
||||||
|
onDamagePlayer(damage);
|
||||||
|
_hasFiredThisCycle = true;
|
||||||
|
}
|
||||||
|
} else if (timeShooting < 450) {
|
||||||
|
spriteIndex = _baseSprite + 48; // Recoil
|
||||||
|
} else {
|
||||||
|
state = EntityState.patrolling;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.pain:
|
||||||
|
spriteIndex = _baseSprite + 44;
|
||||||
|
if (elapsedMs - lastActionTime > 250) {
|
||||||
|
state = EntityState.patrolling;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.dead:
|
||||||
|
if (isDying) {
|
||||||
|
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
|
||||||
|
if (deathFrame < 4) {
|
||||||
|
spriteIndex = (_baseSprite + 40) + deathFrame;
|
||||||
|
} else {
|
||||||
|
spriteIndex = _baseSprite + 45;
|
||||||
|
isDying = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spriteIndex = _baseSprite + 45;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (movement: movement, newAngle: newAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
151
lib/features/entities/enemies/ss.dart
Normal file
151
lib/features/entities/enemies/ss.dart
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||||
|
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/enemy.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/entity.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/map_objects.dart';
|
||||||
|
|
||||||
|
class SS extends Enemy {
|
||||||
|
static const double speed = 0.04;
|
||||||
|
static const int _baseSprite = 138;
|
||||||
|
bool _hasFiredThisCycle = false;
|
||||||
|
|
||||||
|
SS({
|
||||||
|
required super.x,
|
||||||
|
required super.y,
|
||||||
|
required super.angle,
|
||||||
|
required super.mapId,
|
||||||
|
}) : super(
|
||||||
|
spriteIndex: _baseSprite,
|
||||||
|
state: EntityState.idle,
|
||||||
|
) {
|
||||||
|
health = 100;
|
||||||
|
damage = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SS? trySpawn(int objId, double x, double y, Difficulty difficulty) {
|
||||||
|
if (objId >= MapObject.ssStart && objId <= MapObject.ssStart + 35) {
|
||||||
|
bool isPatrolling = objId >= MapObject.ssStart + 18;
|
||||||
|
|
||||||
|
return SS(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
angle: MapObject.getAngle(objId),
|
||||||
|
mapId: objId,
|
||||||
|
)..state = isPatrolling ? EntityState.patrolling : EntityState.idle;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
|
required int elapsedMs,
|
||||||
|
required Coordinate2D playerPosition,
|
||||||
|
required bool Function(int x, int y) isWalkable,
|
||||||
|
required void Function(int damage) onDamagePlayer,
|
||||||
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
|
}) {
|
||||||
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
|
double newAngle = angle;
|
||||||
|
|
||||||
|
checkWakeUp(
|
||||||
|
elapsedMs: elapsedMs,
|
||||||
|
playerPosition: playerPosition,
|
||||||
|
isWalkable: isWalkable,
|
||||||
|
);
|
||||||
|
|
||||||
|
double distance = position.distanceTo(playerPosition);
|
||||||
|
double angleToPlayer = position.angleTo(playerPosition);
|
||||||
|
|
||||||
|
if (state != EntityState.idle && state != EntityState.dead) {
|
||||||
|
newAngle = angleToPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
double diff = angleToPlayer - newAngle;
|
||||||
|
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;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case EntityState.idle:
|
||||||
|
spriteIndex = _baseSprite + octant;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.patrolling:
|
||||||
|
if (distance > 0.8) {
|
||||||
|
double moveX = math.cos(angleToPlayer) * speed;
|
||||||
|
double moveY = math.sin(angleToPlayer) * speed;
|
||||||
|
movement = getValidMovement(
|
||||||
|
Coordinate2D(moveX, moveY),
|
||||||
|
isWalkable,
|
||||||
|
tryOpenDoor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int walkFrame = (elapsedMs ~/ 150) % 4;
|
||||||
|
spriteIndex = (_baseSprite + 8) + (walkFrame * 8) + octant;
|
||||||
|
|
||||||
|
if (distance < 6.0 && elapsedMs - lastActionTime > 1500) {
|
||||||
|
if (hasLineOfSight(playerPosition, isWalkable)) {
|
||||||
|
state = EntityState.attacking;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
_hasFiredThisCycle = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.attacking:
|
||||||
|
// SS machine gun fires much faster than a standard pistol!
|
||||||
|
int timeShooting = elapsedMs - lastActionTime;
|
||||||
|
if (timeShooting < 100) {
|
||||||
|
spriteIndex = _baseSprite + 46; // Aiming
|
||||||
|
} else if (timeShooting < 200) {
|
||||||
|
spriteIndex = _baseSprite + 47; // Firing
|
||||||
|
if (!_hasFiredThisCycle) {
|
||||||
|
onDamagePlayer(damage);
|
||||||
|
_hasFiredThisCycle = true;
|
||||||
|
}
|
||||||
|
} else if (timeShooting < 300) {
|
||||||
|
spriteIndex = _baseSprite + 48; // Recoil
|
||||||
|
} else {
|
||||||
|
state = EntityState.patrolling;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.pain:
|
||||||
|
spriteIndex = _baseSprite + 44;
|
||||||
|
if (elapsedMs - lastActionTime > 250) {
|
||||||
|
state = EntityState.patrolling;
|
||||||
|
lastActionTime = elapsedMs;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EntityState.dead:
|
||||||
|
if (isDying) {
|
||||||
|
int deathFrame = (elapsedMs - lastActionTime) ~/ 150;
|
||||||
|
if (deathFrame < 4) {
|
||||||
|
spriteIndex = (_baseSprite + 40) + deathFrame;
|
||||||
|
} else {
|
||||||
|
spriteIndex = _baseSprite + 45;
|
||||||
|
isDying = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spriteIndex = _baseSprite + 45;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (movement: movement, newAngle: newAngle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
import 'package:wolf_dart/classes/coordinate_2d.dart';
|
||||||
|
|
||||||
enum EntityState { staticObj, idle, patrolling, shooting, pain, dead }
|
enum EntityState { staticObj, idle, patrolling, attacking, pain, dead }
|
||||||
|
|
||||||
abstract class Entity<T> {
|
abstract class Entity<T> {
|
||||||
double x;
|
double x;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
import 'package:wolf_dart/features/difficulty/difficulty.dart';
|
||||||
import 'package:wolf_dart/features/entities/collectible.dart';
|
import 'package:wolf_dart/features/entities/collectible.dart';
|
||||||
import 'package:wolf_dart/features/entities/decorative.dart';
|
import 'package:wolf_dart/features/entities/decorative.dart';
|
||||||
import 'package:wolf_dart/features/entities/enemies/brown_guard.dart';
|
|
||||||
import 'package:wolf_dart/features/entities/enemies/dog.dart';
|
import 'package:wolf_dart/features/entities/enemies/dog.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/guard.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/mutant.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/officer.dart';
|
||||||
|
import 'package:wolf_dart/features/entities/enemies/ss.dart';
|
||||||
import 'package:wolf_dart/features/entities/entity.dart';
|
import 'package:wolf_dart/features/entities/entity.dart';
|
||||||
import 'package:wolf_dart/features/entities/map_objects.dart';
|
import 'package:wolf_dart/features/entities/map_objects.dart';
|
||||||
|
|
||||||
@@ -16,8 +19,14 @@ typedef EntitySpawner =
|
|||||||
|
|
||||||
abstract class EntityRegistry {
|
abstract class EntityRegistry {
|
||||||
static final List<EntitySpawner> _spawners = [
|
static final List<EntitySpawner> _spawners = [
|
||||||
BrownGuard.trySpawn,
|
// Enemies need to try to spawn first
|
||||||
|
Guard.trySpawn,
|
||||||
|
Officer.trySpawn,
|
||||||
|
SS.trySpawn,
|
||||||
|
Mutant.trySpawn,
|
||||||
Dog.trySpawn,
|
Dog.trySpawn,
|
||||||
|
|
||||||
|
// Everything else
|
||||||
Collectible.trySpawn,
|
Collectible.trySpawn,
|
||||||
Decorative.trySpawn,
|
Decorative.trySpawn,
|
||||||
];
|
];
|
||||||
@@ -32,17 +41,22 @@ abstract class EntityRegistry {
|
|||||||
// 1. Difficulty check before even looking for a spawner
|
// 1. Difficulty check before even looking for a spawner
|
||||||
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
|
if (!MapObject.shouldSpawn(objId, difficulty)) return null;
|
||||||
|
|
||||||
|
if (objId == 0) return null;
|
||||||
|
|
||||||
for (final spawner in _spawners) {
|
for (final spawner in _spawners) {
|
||||||
Entity? entity = spawner(objId, x, y, difficulty);
|
Entity? entity = spawner(objId, x, y, difficulty);
|
||||||
|
|
||||||
if (entity != null) {
|
if (entity != null) {
|
||||||
// Safety bounds check for the VSWAP array
|
// Safety bounds check for the VSWAP array
|
||||||
if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) {
|
if (entity.spriteIndex >= 0 && entity.spriteIndex < maxSprites) {
|
||||||
|
print("Spawned entity with objId $objId");
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
print("VSWAP doesn't have this sprite! objId $objId");
|
||||||
return null; // VSWAP doesn't have this sprite!
|
return null; // VSWAP doesn't have this sprite!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print("No class claimed this Map ID > objId $objId");
|
||||||
return null; // No class claimed this Map ID
|
return null; // No class claimed this Map ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,13 +65,6 @@ abstract class MapObject {
|
|||||||
static const int secretExitTrigger = 99;
|
static const int secretExitTrigger = 99;
|
||||||
static const int normalExitTrigger = 100;
|
static const int normalExitTrigger = 100;
|
||||||
|
|
||||||
// --- Enemy Base IDs (Easy, North) ---
|
|
||||||
static const int _guardBase = 108;
|
|
||||||
static const int _officerBase = 126; // WL6 only
|
|
||||||
static const int _ssBase = 144; // WL6 only
|
|
||||||
static const int _dogBase = 162;
|
|
||||||
static const int _mutantBase = 180; // Episode 2+
|
|
||||||
|
|
||||||
// Bosses (Shared between WL1 and WL6)
|
// Bosses (Shared between WL1 and WL6)
|
||||||
static const int bossHansGrosse = 214;
|
static const int bossHansGrosse = 214;
|
||||||
|
|
||||||
@@ -87,55 +80,38 @@ abstract class MapObject {
|
|||||||
static const int bossFettgesicht = 223;
|
static const int bossFettgesicht = 223;
|
||||||
|
|
||||||
// --- Enemy Range Constants ---
|
// --- Enemy Range Constants ---
|
||||||
static const int guardStart = 108;
|
static const int guardStart = 108; // 108-143
|
||||||
static const int officerStart = 126;
|
static const int officerStart = 144; // 144-179 (WL6)
|
||||||
static const int ssStart = 144;
|
static const int ssStart = 180; // 180-215 (WL6)
|
||||||
static const int dogStart = 162;
|
static const int dogStart = 216; // 216-251
|
||||||
static const int mutantStart = 180;
|
static const int mutantStart = 252; // 252-287 (WL6)
|
||||||
|
|
||||||
|
// --- Missing Decorative Bodies ---
|
||||||
|
static const int deadGuard = 124; // Decorative only in WL1
|
||||||
|
static const int deadAardwolf = 125; // Decorative only in WL1
|
||||||
|
|
||||||
|
/// Returns true if the object ID exists in the Shareware version.
|
||||||
/// Returns true if the object ID exists in the Shareware version.
|
/// Returns true if the object ID exists in the Shareware version.
|
||||||
static bool isSharewareCompatible(int id) {
|
static bool isSharewareCompatible(int id) {
|
||||||
// WL1 only had Guards (108-125), Dogs (162-179), and Hans Grosse (214)
|
// Standard Decorations & Collectibles
|
||||||
if (id >= 126 && id < 162) return false; // No Officers or SS
|
if (id <= vines) return true;
|
||||||
if (id >= 180 && id < 214) return false; // No Mutants
|
|
||||||
if (id > 214) return false; // No other bosses
|
// Logic Triggers (Exits/Pushwalls)
|
||||||
return true;
|
if (id >= pushwallTrigger && id <= normalExitTrigger) return true;
|
||||||
|
|
||||||
|
// Guards (108-143 includes dead bodies at 124/125)
|
||||||
|
if (id >= guardStart && id < officerStart) return true;
|
||||||
|
|
||||||
|
// Dogs (216-251) - These ARE in Shareware!
|
||||||
|
if (id >= dogStart && id < mutantStart) return true;
|
||||||
|
|
||||||
|
// Episode 1 Boss
|
||||||
|
if (id == bossHansGrosse) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves which enemy type a map ID belongs to.
|
|
||||||
static String getEnemyType(int id) {
|
|
||||||
if (id >= 108 && id <= 125) return "Guard";
|
|
||||||
if (id >= 126 && id <= 143) return "Officer";
|
|
||||||
if (id >= 144 && id <= 161) return "SS";
|
|
||||||
if (id >= 162 && id <= 179) return "Dog";
|
|
||||||
if (id >= 180 && id <= 197) return "Mutant";
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if an object should be spawned based on chosen difficulty.
|
|
||||||
static bool shouldSpawn(int id, Difficulty selectedDifficulty) {
|
|
||||||
if (id < 108 || id > 213) return true; // Items/Players/Bosses always spawn
|
|
||||||
|
|
||||||
// Enemy blocks are 18 IDs wide (e.g., 108-125 for Guards)
|
|
||||||
int relativeId = (id - 108) % 18;
|
|
||||||
|
|
||||||
// 0-3: Easy, 4-7: Medium, 8-11: Hard
|
|
||||||
if (relativeId < 4) return true; // Easy spawns on everything
|
|
||||||
if (relativeId < 8) {
|
|
||||||
return selectedDifficulty != Difficulty.canIPlayDaddy; // Medium/Hard
|
|
||||||
}
|
|
||||||
if (relativeId < 12) {
|
|
||||||
return selectedDifficulty == Difficulty.iAmDeathIncarnate; // Hard only
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12-15 are typically "Ambush" versions of the Easy/Medium/Hard guards
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determines the spawn orientation of an enemy or player.
|
|
||||||
/// Determines the spawn orientation of an enemy or player.
|
|
||||||
static double getAngle(int id) {
|
static double getAngle(int id) {
|
||||||
// Player spawn angles
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case playerNorth:
|
case playerNorth:
|
||||||
return CardinalDirection.north.radians;
|
return CardinalDirection.north.radians;
|
||||||
@@ -147,9 +123,52 @@ abstract class MapObject {
|
|||||||
return CardinalDirection.west.radians;
|
return CardinalDirection.west.radians;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id < 108 || id > 213) return 0.0;
|
// FIX: Expand the boundary to include ALL enemies (Dogs and Mutants)
|
||||||
|
if (id < guardStart || id > (mutantStart + 35)) return 0.0;
|
||||||
|
|
||||||
// Enemy directions are mapped in groups of 4
|
int baseId;
|
||||||
return CardinalDirection.fromEnemyIndex(id - 108).radians;
|
if (id >= mutantStart) {
|
||||||
|
baseId = mutantStart;
|
||||||
|
} else if (id >= dogStart) {
|
||||||
|
baseId = dogStart;
|
||||||
|
} else if (id >= ssStart) {
|
||||||
|
baseId = ssStart;
|
||||||
|
} else if (id >= officerStart) {
|
||||||
|
baseId = officerStart;
|
||||||
|
} else {
|
||||||
|
baseId = guardStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIX: Normalize patrolling enemies back to the standing block, THEN get the 4-way angle
|
||||||
|
int directionIndex = ((id - baseId) % 18) % 4;
|
||||||
|
return CardinalDirection.fromEnemyIndex(directionIndex).radians;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool shouldSpawn(int id, Difficulty selectedDifficulty) {
|
||||||
|
// FIX: Expand the boundary so Dogs and Mutants aren't bypassing difficulty checks
|
||||||
|
if (id < guardStart || id > (mutantStart + 35)) return true;
|
||||||
|
|
||||||
|
int baseId;
|
||||||
|
if (id >= mutantStart) {
|
||||||
|
baseId = mutantStart;
|
||||||
|
} else if (id >= dogStart) {
|
||||||
|
baseId = dogStart;
|
||||||
|
} else if (id >= ssStart) {
|
||||||
|
baseId = ssStart;
|
||||||
|
} else if (id >= officerStart) {
|
||||||
|
baseId = officerStart;
|
||||||
|
} else {
|
||||||
|
baseId = guardStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
int relativeId = (id - baseId) % 18;
|
||||||
|
|
||||||
|
return switch (relativeId) {
|
||||||
|
< 4 => true,
|
||||||
|
< 8 => selectedDifficulty.level >= Difficulty.dontHurtMe.level,
|
||||||
|
< 12 => selectedDifficulty.level >= Difficulty.bringEmOn.level,
|
||||||
|
< 16 => selectedDifficulty.level >= Difficulty.iAmDeathIncarnate.level,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:wolf_dart/classes/matrix.dart';
|
import 'package:wolf_dart/classes/matrix.dart';
|
||||||
import 'package:wolf_dart/features/map/vswap_parser.dart';
|
|
||||||
import 'package:wolf_dart/features/map/wolf_level.dart';
|
import 'package:wolf_dart/features/map/wolf_level.dart';
|
||||||
import 'package:wolf_dart/features/map/wolf_map_parser.dart';
|
import 'package:wolf_dart/features/map/wolf_map_parser.dart';
|
||||||
|
import 'package:wolf_dart/vswap_parser.dart';
|
||||||
|
|
||||||
class WolfMap {
|
class WolfMap {
|
||||||
/// The fully parsed and decompressed levels from the game files.
|
/// The fully parsed and decompressed levels from the game files.
|
||||||
|
|||||||
@@ -101,4 +101,43 @@ class VswapParser {
|
|||||||
}
|
}
|
||||||
return sprites;
|
return sprites;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts digitized sound effects (PCM Audio) from VSWAP.WL1
|
||||||
|
static List<Uint8List> parseSounds(ByteData vswap) {
|
||||||
|
int chunks = vswap.getUint16(0, Endian.little);
|
||||||
|
int soundStart = vswap.getUint16(4, Endian.little);
|
||||||
|
|
||||||
|
List<int> offsets = [];
|
||||||
|
List<int> lengths = [];
|
||||||
|
|
||||||
|
// Offsets are 32-bit integers starting at byte 6
|
||||||
|
for (int i = 0; i < chunks; i++) {
|
||||||
|
offsets.add(vswap.getUint32(6 + (i * 4), Endian.little));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lengths are 16-bit integers immediately following the offset array
|
||||||
|
int lengthStart = 6 + (chunks * 4);
|
||||||
|
for (int i = 0; i < chunks; i++) {
|
||||||
|
lengths.add(vswap.getUint16(lengthStart + (i * 2), Endian.little));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Uint8List> sounds = [];
|
||||||
|
|
||||||
|
// Sounds start after the sprites and go to the end of the chunks
|
||||||
|
for (int i = soundStart; i < chunks; i++) {
|
||||||
|
int offset = offsets[i];
|
||||||
|
int length = lengths[i];
|
||||||
|
|
||||||
|
if (offset == 0 || length == 0) {
|
||||||
|
sounds.add(Uint8List(0)); // Empty placeholder
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the raw 8-bit PCM audio bytes
|
||||||
|
final soundData = vswap.buffer.asUint8List(offset, length);
|
||||||
|
sounds.add(soundData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sounds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user