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 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 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 otherZombies) { int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; java.util.List 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 toStateMap() { Map 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; } }