311 lines
11 KiB
Java
311 lines
11 KiB
Java
package com.zombie.game.model;
|
|
|
|
import java.util.*;
|
|
|
|
public class Zombie {
|
|
private int id;
|
|
private float x, y;
|
|
private float angle;
|
|
private float health;
|
|
private float maxHealth;
|
|
private float speed;
|
|
private long lastAttackTime;
|
|
private boolean isElite;
|
|
private long lastRangedAttackTime;
|
|
private float targetX, targetY;
|
|
private boolean hasTarget;
|
|
private int reservedGridX, reservedGridY;
|
|
private boolean hasReservation;
|
|
|
|
public Zombie(int id, float x, float y, float health, float speed) {
|
|
this(id, x, y, health, speed, false);
|
|
}
|
|
|
|
public Zombie(int id, float x, float y, float health, float speed, boolean isElite) {
|
|
this.id = id;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.angle = 0;
|
|
this.health = health;
|
|
this.maxHealth = health;
|
|
this.speed = speed;
|
|
this.lastAttackTime = 0;
|
|
this.isElite = isElite;
|
|
this.lastRangedAttackTime = 0;
|
|
this.targetX = 0;
|
|
this.targetY = 0;
|
|
this.hasTarget = false;
|
|
this.reservedGridX = -1;
|
|
this.reservedGridY = -1;
|
|
this.hasReservation = false;
|
|
}
|
|
|
|
public int getId() { return id; }
|
|
public float getX() { return x; }
|
|
public float getY() { return y; }
|
|
public float getAngle() { return angle; }
|
|
public float getHealth() { return health; }
|
|
public float getMaxHealth() { return maxHealth; }
|
|
public boolean isElite() { return isElite; }
|
|
public int getReservedGridX() { return reservedGridX; }
|
|
public int getReservedGridY() { return reservedGridY; }
|
|
public boolean hasReservation() { return hasReservation; }
|
|
|
|
public void takeDamage(float damage) {
|
|
this.health -= damage;
|
|
if (this.health < 0) this.health = 0;
|
|
}
|
|
|
|
public boolean isAlive() {
|
|
return health > 0;
|
|
}
|
|
|
|
public void move(GameMap map, float dt, Collection<Zombie> otherZombies) {
|
|
if (!map.isFlowFieldValid()) return;
|
|
|
|
int currentGridX = (int) Math.floor(x);
|
|
int currentGridY = (int) Math.floor(y);
|
|
|
|
float centerDist = Float.MAX_VALUE;
|
|
if (hasTarget) {
|
|
float dx = targetX - x;
|
|
float dy = targetY - y;
|
|
centerDist = (float) Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
if (!hasTarget || centerDist < 0.15f) {
|
|
float[] flowDir = map.getFlowDirection(x, y);
|
|
float dirX = flowDir[0];
|
|
float dirY = flowDir[1];
|
|
|
|
if (dirX == 0 && dirY == 0) return;
|
|
|
|
float len = (float) Math.sqrt(dirX * dirX + dirY * dirY);
|
|
if (len > 0) {
|
|
dirX /= len;
|
|
dirY /= len;
|
|
}
|
|
|
|
int nextGridX = currentGridX + (int) Math.round(dirX);
|
|
int nextGridY = currentGridY + (int) Math.round(dirY);
|
|
|
|
if (map.isWall(nextGridX, nextGridY)) {
|
|
nextGridX = currentGridX + (int) Math.signum(dirX);
|
|
nextGridY = currentGridY + (int) Math.signum(dirY);
|
|
|
|
if (map.isWall(nextGridX, nextGridY)) {
|
|
if (!map.isWall(currentGridX + (int) Math.signum(dirX), currentGridY)) {
|
|
nextGridX = currentGridX + (int) Math.signum(dirX);
|
|
nextGridY = currentGridY;
|
|
} else if (!map.isWall(currentGridX, currentGridY + (int) Math.signum(dirY))) {
|
|
nextGridX = currentGridX;
|
|
nextGridY = currentGridY + (int) Math.signum(dirY);
|
|
} else {
|
|
hasReservation = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) {
|
|
int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies);
|
|
if (altDirs != null) {
|
|
nextGridX = altDirs[0];
|
|
nextGridY = altDirs[1];
|
|
} else {
|
|
hasReservation = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
reservedGridX = nextGridX;
|
|
reservedGridY = nextGridY;
|
|
hasReservation = true;
|
|
|
|
targetX = nextGridX + 0.5f;
|
|
targetY = nextGridY + 0.5f;
|
|
hasTarget = true;
|
|
}
|
|
|
|
float dx = targetX - x;
|
|
float dy = targetY - y;
|
|
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < 0.01f) {
|
|
hasTarget = false;
|
|
return;
|
|
}
|
|
|
|
float dirX = dx / dist;
|
|
float dirY = dy / dist;
|
|
|
|
float moveX = dirX * speed * dt;
|
|
float moveY = dirY * speed * dt;
|
|
|
|
float newX = x + moveX;
|
|
float newY = y + moveY;
|
|
|
|
boolean canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE);
|
|
boolean canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE);
|
|
boolean canMoveDiagonal = map.isWalkable(newX, newY, Constants.ZOMBIE_SIZE);
|
|
|
|
if (moveX != 0 && moveY != 0) {
|
|
int checkX = (int) Math.floor(newX);
|
|
int checkY = (int) Math.floor(newY);
|
|
int checkCurrentX = (int) Math.floor(x);
|
|
int checkCurrentY = (int) Math.floor(y);
|
|
|
|
boolean blockedByCorner = false;
|
|
if (checkX != checkCurrentX && checkY != checkCurrentY) {
|
|
boolean wallInX = map.isWall(checkX, checkCurrentY);
|
|
boolean wallInY = map.isWall(checkCurrentX, checkY);
|
|
|
|
if (wallInX || wallInY) {
|
|
blockedByCorner = true;
|
|
|
|
if (!wallInX && canMoveX) {
|
|
x = newX;
|
|
canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE);
|
|
} else if (!wallInY && canMoveY) {
|
|
y = newY;
|
|
canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!blockedByCorner && canMoveDiagonal) {
|
|
x = newX;
|
|
y = newY;
|
|
} else if (!blockedByCorner) {
|
|
if (canMoveX) x = newX;
|
|
if (canMoveY) y = newY;
|
|
}
|
|
} else {
|
|
if (canMoveX) x = newX;
|
|
if (canMoveY) y = newY;
|
|
}
|
|
|
|
float minSeparationDist = Constants.ZOMBIE_SIZE;
|
|
for (Zombie other : otherZombies) {
|
|
if (other.getId() == this.id) continue;
|
|
if (!other.isAlive()) continue;
|
|
|
|
float ox = other.getX();
|
|
float oy = other.getY();
|
|
float sepDx = x - ox;
|
|
float sepDy = y - oy;
|
|
float sepDist = (float) Math.sqrt(sepDx * sepDx + sepDy * sepDy);
|
|
|
|
if (sepDist < minSeparationDist && sepDist > 0.01f) {
|
|
float overlap = minSeparationDist - sepDist;
|
|
float pushX = (sepDx / sepDist) * overlap * 0.5f;
|
|
float pushY = (sepDy / sepDist) * overlap * 0.5f;
|
|
|
|
float pushedX = x + pushX;
|
|
float pushedY = y + pushY;
|
|
|
|
if (map.isWalkable(pushedX, pushedY, Constants.ZOMBIE_SIZE)) {
|
|
x = pushedX;
|
|
y = pushedY;
|
|
} else {
|
|
if (map.isWalkable(x + pushX, y, Constants.ZOMBIE_SIZE)) {
|
|
x = x + pushX;
|
|
}
|
|
if (map.isWalkable(x, y + pushY, Constants.ZOMBIE_SIZE)) {
|
|
y = y + pushY;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dirX != 0 || dirY != 0) {
|
|
angle = (float) Math.atan2(dirX, dirY);
|
|
}
|
|
}
|
|
|
|
private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> otherZombies) {
|
|
for (Zombie other : otherZombies) {
|
|
if (other.getId() == this.id) continue;
|
|
if (!other.isAlive()) continue;
|
|
|
|
int otherGridX = (int) Math.floor(other.getX());
|
|
int otherGridY = (int) Math.floor(other.getY());
|
|
|
|
if (otherGridX == gridX && otherGridY == gridY) {
|
|
return true;
|
|
}
|
|
|
|
if (other.hasReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
|
|
GameMap map, Collection<Zombie> otherZombies) {
|
|
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
|
|
|
java.util.List<int[]> candidates = new java.util.ArrayList<>();
|
|
|
|
for (int[] dir : allDirs) {
|
|
int nx = currentGridX + dir[0];
|
|
int ny = currentGridY + dir[1];
|
|
|
|
if (map.isWall(nx, ny)) continue;
|
|
|
|
if (dir[0] != 0 && dir[1] != 0) {
|
|
if (map.isWall(currentGridX + dir[0], currentGridY) ||
|
|
map.isWall(currentGridX, currentGridY + dir[1])) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue;
|
|
|
|
float dotProduct = dir[0] * dirX + dir[1] * dirY;
|
|
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
|
|
}
|
|
|
|
if (candidates.isEmpty()) return null;
|
|
|
|
candidates.sort((a, b) -> b[2] - a[2]);
|
|
|
|
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
|
|
}
|
|
|
|
public boolean canAttack(long now) {
|
|
return now - lastAttackTime >= Constants.ZOMBIE_ATTACK_RATE * 1000;
|
|
}
|
|
|
|
public void attack(long now) {
|
|
lastAttackTime = now;
|
|
}
|
|
|
|
public boolean canRangedAttack(long now) {
|
|
if (!isElite) return false;
|
|
return now - lastRangedAttackTime >= Constants.ELITE_ZOMBIE_ATTACK_RATE * 1000;
|
|
}
|
|
|
|
public void rangedAttack(long now) {
|
|
lastRangedAttackTime = now;
|
|
}
|
|
|
|
public float distanceTo(float px, float py) {
|
|
float dx = px - x;
|
|
float dy = py - y;
|
|
return (float) Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
public Map<String, Object> toStateMap() {
|
|
Map<String, Object> map = new LinkedHashMap<>();
|
|
map.put("id", id);
|
|
map.put("x", x);
|
|
map.put("y", y);
|
|
map.put("angle", angle);
|
|
map.put("health", health);
|
|
map.put("isElite", isElite);
|
|
return map;
|
|
}
|
|
}
|