1
This commit is contained in:
35
backend/build-and-run.bat
Normal file
35
backend/build-and-run.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo Zombie Crisis 3 - Build and Run Script
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
set MAVEN_VERSION=3.9.6
|
||||
set MAVEN_DIR=%~dp0.maven
|
||||
|
||||
if not exist "%MAVEN_DIR%\apache-maven-%MAVEN_VERSION%" (
|
||||
echo Maven not found. Downloading Maven %MAVEN_VERSION%...
|
||||
powershell -Command "Invoke-WebRequest -Uri 'https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/%MAVEN_VERSION%/apache-maven-%MAVEN_VERSION%-bin.zip' -OutFile '%MAVEN_DIR%\maven.zip'"
|
||||
if not exist "%MAVEN_DIR%" mkdir "%MAVEN_DIR%"
|
||||
echo Extracting Maven...
|
||||
powershell -Command "Expand-Archive -Path '%MAVEN_DIR%\maven.zip' -DestinationPath '%MAVEN_DIR%' -Force"
|
||||
del "%MAVEN_DIR%\maven.zip"
|
||||
)
|
||||
|
||||
set MAVEN_HOME=%MAVEN_DIR%\apache-maven-%MAVEN_VERSION%
|
||||
set PATH=%MAVEN_HOME%\bin;%PATH%
|
||||
|
||||
echo Building server...
|
||||
call mvn clean package -DskipTests
|
||||
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo Build failed!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Starting server on port 8080...
|
||||
java -jar target\zombie-crisis-server-1.0.0.jar
|
||||
|
||||
pause
|
||||
39
backend/dependency-reduced-pom.xml
Normal file
39
backend/dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.zombie</groupId>
|
||||
<artifactId>zombie-crisis-server</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.zombie.game.GameServerMain</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<properties>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
61
backend/mvnw.cmd
vendored
Normal file
61
backend/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $proxy=''; [Net.WebRequest]::DefaultWebProxy.Credentials=[Net.CredentialCache]::DefaultCredentials; if('%MVNW_USERNAME%' -ne '' -and '%MVNW_PASSWORD%' -ne ''){$proxy=[Net.WebRequest]::GetSystemWebProxy(); $proxy.Credentials=[Net.NetworkCredential]::new('%MVNW_USERNAME%','%MVNW_PASSWORD%');} try{Invoke-WebRequest -Uri ('https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar') -Proxy $proxy -ProxyUseDefaultCredentials -OutFile ('%__MVNW_JAR__%')}catch{Write-Error $_}}" 2>&1`) DO @(
|
||||
IF "%%A"=="MVNW_ERROR" (
|
||||
SET __MVNW_ERROR__=%%B
|
||||
)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
|
||||
@SET MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
@IF NOT "%MAVEN_PROJECTBASEDIR%"=="" GOTO endDetectBaseDir
|
||||
@SET MAVEN_PROJECTBASEDIR=%~dp0..
|
||||
:endDetectBaseDir
|
||||
|
||||
@IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" (
|
||||
powershell -noprofile "& {Invoke-WebRequest -Uri 'https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar' -OutFile '%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar'}"
|
||||
)
|
||||
|
||||
@SET MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
@REM Find Java
|
||||
@SET JAVA_EXE=java.exe
|
||||
@IF DEFINED JAVA_HOME (
|
||||
@SET JAVA_HOME=%JAVA_HOME:"=%
|
||||
@SET JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
)
|
||||
|
||||
@REM Execute Maven wrapper
|
||||
"%JAVA_EXE%" ^
|
||||
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||
%MAVEN_OPTS% ^
|
||||
%MAVEN_DEBUG_OPTS% ^
|
||||
-classpath "%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" ^
|
||||
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||
org.apache.maven.wrapper.MavenWrapperMain %MAVEN_CMD_LINE_ARGS%
|
||||
65
backend/pom.xml
Normal file
65
backend/pom.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.zombie</groupId>
|
||||
<artifactId>zombie-crisis-server</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.5.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.zombie.game.GameServerMain</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
31
backend/src/main/java/com/zombie/game/GameServerMain.java
Normal file
31
backend/src/main/java/com/zombie/game/GameServerMain.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.zombie.game;
|
||||
|
||||
import com.zombie.game.server.GameWebSocketServer;
|
||||
|
||||
public class GameServerMain {
|
||||
public static void main(String[] args) {
|
||||
int port = 8080;
|
||||
if (args.length > 0) {
|
||||
try {
|
||||
port = Integer.parseInt(args[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Invalid port number, using default: 8080");
|
||||
}
|
||||
}
|
||||
|
||||
GameWebSocketServer server = new GameWebSocketServer(port);
|
||||
server.start();
|
||||
|
||||
System.out.println("Zombie Crisis 3 Server started on port " + port);
|
||||
System.out.println("Press Ctrl+C to stop the server");
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
System.out.println("Shutting down server...");
|
||||
try {
|
||||
server.stop();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
150
backend/src/main/java/com/zombie/game/model/Bullet.java
Normal file
150
backend/src/main/java/com/zombie/game/model/Bullet.java
Normal file
@@ -0,0 +1,150 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Bullet {
|
||||
private int id;
|
||||
private float x, y;
|
||||
private float z;
|
||||
private float vx, vy, vz;
|
||||
private float speed;
|
||||
private int damage;
|
||||
private String ownerId;
|
||||
private String weapon;
|
||||
private float range;
|
||||
private float distanceTraveled;
|
||||
private boolean explosive;
|
||||
private float explosionRadius;
|
||||
private float flightTime;
|
||||
private float maxFlightTime;
|
||||
private float targetX, targetY;
|
||||
private boolean isGrenade;
|
||||
|
||||
public Bullet(int id, float x, float y, float angle, float speed, int damage,
|
||||
String ownerId, String weapon, float range) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = 0.5f;
|
||||
this.speed = speed;
|
||||
this.vx = (float) Math.sin(angle) * speed;
|
||||
this.vy = (float) Math.cos(angle) * speed;
|
||||
this.vz = 0;
|
||||
this.damage = damage;
|
||||
this.ownerId = ownerId;
|
||||
this.weapon = weapon;
|
||||
this.range = range;
|
||||
this.distanceTraveled = 0;
|
||||
this.explosive = weapon.equals(Constants.WEAPON_GRENADE);
|
||||
this.explosionRadius = explosive ? 3.0f : 0;
|
||||
this.flightTime = 0;
|
||||
this.maxFlightTime = 0;
|
||||
this.isGrenade = weapon.equals(Constants.WEAPON_GRENADE);
|
||||
this.targetX = x + (float) Math.sin(angle) * range;
|
||||
this.targetY = y + (float) Math.cos(angle) * range;
|
||||
}
|
||||
|
||||
public Bullet(int id, float startX, float startY, float targetX, float targetY,
|
||||
float flightDuration, int damage, String ownerId, float explosionRadius) {
|
||||
this.id = id;
|
||||
this.x = startX;
|
||||
this.y = startY;
|
||||
this.z = 0.5f;
|
||||
this.targetX = targetX;
|
||||
this.targetY = targetY;
|
||||
this.damage = damage;
|
||||
this.ownerId = ownerId;
|
||||
this.weapon = Constants.WEAPON_GRENADE;
|
||||
this.range = 0;
|
||||
this.distanceTraveled = 0;
|
||||
this.explosive = true;
|
||||
this.explosionRadius = explosionRadius;
|
||||
this.flightTime = 0;
|
||||
this.maxFlightTime = flightDuration;
|
||||
this.isGrenade = true;
|
||||
this.speed = 0;
|
||||
|
||||
float dx = targetX - startX;
|
||||
float dy = targetY - startY;
|
||||
this.vx = dx / flightDuration;
|
||||
this.vy = dy / flightDuration;
|
||||
this.vz = 3.0f;
|
||||
}
|
||||
|
||||
public int getId() { return id; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public float getZ() { return z; }
|
||||
public float getVx() { return vx; }
|
||||
public float getVy() { return vy; }
|
||||
public int getDamage() { return damage; }
|
||||
public String getOwnerId() { return ownerId; }
|
||||
public String getWeapon() { return weapon; }
|
||||
public boolean isExplosive() { return explosive; }
|
||||
public float getExplosionRadius() { return explosionRadius; }
|
||||
public float getTargetX() { return targetX; }
|
||||
public float getTargetY() { return targetY; }
|
||||
public boolean isGrenade() { return isGrenade; }
|
||||
|
||||
public boolean update(float dt, GameMap map) {
|
||||
if (isGrenade) {
|
||||
flightTime += dt;
|
||||
|
||||
x += vx * dt;
|
||||
y += vy * dt;
|
||||
|
||||
float progress = flightTime / maxFlightTime;
|
||||
z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
|
||||
|
||||
if (flightTime >= maxFlightTime || z <= 0.5f && progress > 0.5f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < 0 || x >= Constants.GRID_SIZE || y < 0 || y >= Constants.GRID_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
float moveX = vx * dt;
|
||||
float moveY = vy * dt;
|
||||
x += moveX;
|
||||
y += moveY;
|
||||
distanceTraveled += (float) Math.sqrt(moveX * moveX + moveY * moveY);
|
||||
|
||||
if (distanceTraveled >= range) return false;
|
||||
if (x < 0 || x >= Constants.GRID_SIZE || y < 0 || y >= Constants.GRID_SIZE) return false;
|
||||
|
||||
int gx = (int) Math.floor(x);
|
||||
int gy = (int) Math.floor(y);
|
||||
if (map.isWall(gx, gy)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hitsEntity(float ex, float ey, float size) {
|
||||
float dx = x - ex;
|
||||
float dy = y - ey;
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
return dist < size / 2 + 0.1f;
|
||||
}
|
||||
|
||||
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("z", z);
|
||||
if (isGrenade) {
|
||||
map.put("angle", (float) Math.atan2(vx, vy));
|
||||
map.put("targetX", targetX);
|
||||
map.put("targetY", targetY);
|
||||
} else {
|
||||
map.put("angle", (float) Math.atan2(vx, vy));
|
||||
}
|
||||
map.put("weapon", weapon);
|
||||
map.put("ownerId", ownerId);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
56
backend/src/main/java/com/zombie/game/model/Constants.java
Normal file
56
backend/src/main/java/com/zombie/game/model/Constants.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
public class Constants {
|
||||
public static final int GRID_SIZE = 32;
|
||||
public static final float PLAYER_SIZE = 0.8f;
|
||||
public static final float ZOMBIE_SIZE = 0.8f;
|
||||
public static final int TICK_RATE = 30;
|
||||
public static final float TICK_INTERVAL = 1.0f / TICK_RATE;
|
||||
|
||||
public static final float PLAYER_SPEED = 5.0f;
|
||||
public static final float PLAYER_MAX_HEALTH = 100;
|
||||
public static final float PLAYER_INVULNERABLE_TIME = 0.5f;
|
||||
|
||||
public static final float ZOMBIE_BASE_HEALTH = 100;
|
||||
public static final float ZOMBIE_BASE_SPEED = 2.0f;
|
||||
public static final float ZOMBIE_DAMAGE = 10;
|
||||
public static final float ZOMBIE_ATTACK_RATE = 1.0f;
|
||||
public static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
|
||||
public static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
|
||||
public static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.8f;
|
||||
public static final float ZOMBIE_HEALTH_INCREASE = 20;
|
||||
public static final float ZOMBIE_SPEED_INCREASE = 0.1f;
|
||||
public static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
|
||||
|
||||
public static final float ELITE_ZOMBIE_HEALTH = 800;
|
||||
public static final float ELITE_ZOMBIE_SPEED = 1.5f;
|
||||
public static final float ELITE_ZOMBIE_DAMAGE = 20;
|
||||
public static final float ELITE_ZOMBIE_ATTACK_RANGE = 8.0f;
|
||||
public static final float ELITE_ZOMBIE_ATTACK_RATE = 2.0f;
|
||||
public static final int ELITE_ZOMBIE_BULLET_DAMAGE = 30;
|
||||
public static final float ELITE_ZOMBIE_BULLET_SPEED = 6.0f;
|
||||
public static final float ELITE_ZOMBIE_SPAWN_CHANCE = 0.05f;
|
||||
|
||||
public static final String WEAPON_PISTOL = "pistol";
|
||||
public static final String WEAPON_MACHINE_GUN = "machine_gun";
|
||||
public static final String WEAPON_SHOTGUN = "shotgun";
|
||||
public static final String WEAPON_GRENADE = "grenade";
|
||||
|
||||
public static final String LOOT_TYPE_AMMO = "ammo";
|
||||
public static final String LOOT_TYPE_HEALTH = "health";
|
||||
public static final float LOOT_HEALTH_AMOUNT = 30;
|
||||
|
||||
public static final String MSG_CREATE_ROOM = "create_room";
|
||||
public static final String MSG_JOIN_ROOM = "join_room";
|
||||
public static final String MSG_LEAVE_ROOM = "leave_room";
|
||||
public static final String MSG_ROOM_LIST = "room_list";
|
||||
public static final String MSG_ROOM_STATE = "room_state";
|
||||
public static final String MSG_READY = "ready";
|
||||
public static final String MSG_START_GAME = "start_game";
|
||||
public static final String MSG_GAME_STARTED = "game_started";
|
||||
public static final String MSG_PLAYER_INPUT = "player_input";
|
||||
public static final String MSG_GAME_STATE = "game_state";
|
||||
public static final String MSG_PLAYER_JOIN = "player_join";
|
||||
public static final String MSG_PLAYER_LEAVE = "player_leave";
|
||||
public static final String MSG_ERROR = "error";
|
||||
}
|
||||
216
backend/src/main/java/com/zombie/game/model/GameMap.java
Normal file
216
backend/src/main/java/com/zombie/game/model/GameMap.java
Normal file
@@ -0,0 +1,216 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class GameMap {
|
||||
private final int[][] cells;
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
public GameMap() {
|
||||
this.width = Constants.GRID_SIZE;
|
||||
this.height = Constants.GRID_SIZE;
|
||||
this.cells = new int[height][width];
|
||||
generateDefaultMap();
|
||||
}
|
||||
|
||||
private void generateDefaultMap() {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) {
|
||||
cells[y][x] = 1;
|
||||
} else {
|
||||
cells[y][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[][] wallSegments = {
|
||||
{5, 5, 5, 10}, {10, 3, 15, 3}, {20, 5, 20, 12},
|
||||
{8, 15, 14, 15}, {25, 10, 25, 18}, {3, 20, 8, 20},
|
||||
{15, 20, 15, 26}, {22, 22, 28, 22}, {5, 25, 10, 25},
|
||||
{12, 8, 12, 12}, {18, 16, 22, 16}, {27, 5, 27, 9},
|
||||
{8, 27, 13, 27}, {18, 26, 18, 30}
|
||||
};
|
||||
|
||||
for (int[] seg : wallSegments) {
|
||||
if (seg[0] == seg[2]) {
|
||||
for (int y = seg[1]; y <= seg[3]; y++) {
|
||||
if (seg[0] > 0 && seg[0] < width - 1 && y > 0 && y < height - 1) {
|
||||
cells[y][seg[0]] = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int x = seg[0]; x <= seg[2]; x++) {
|
||||
if (x > 0 && x < width - 1 && seg[1] > 0 && seg[1] < height - 1) {
|
||||
cells[seg[1]][x] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[][] spawnPoints = {{2, 2}, {29, 2}, {2, 29}, {29, 29}};
|
||||
for (int[] sp : spawnPoints) {
|
||||
cells[sp[1]][sp[0]] = 2;
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
||||
if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) {
|
||||
if (cells[ny][nx] == 1) cells[ny][nx] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[][] zombieSpawnAreas = {{16, 2}, {2, 16}, {29, 16}, {16, 29}, {16, 16}};
|
||||
for (int[] sp : zombieSpawnAreas) {
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
||||
if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) {
|
||||
if (cells[ny][nx] == 1) cells[ny][nx] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[][] zombieSpawnPoints = {{8, 8}, {24, 24}};
|
||||
for (int[] sp : zombieSpawnPoints) {
|
||||
cells[sp[1]][sp[0]] = 3;
|
||||
for (int dy = -1; dy <= 1; dy++) {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
||||
if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) {
|
||||
if (cells[ny][nx] == 1) cells[ny][nx] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isWall(int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
|
||||
return cells[gy][gx] == 1;
|
||||
}
|
||||
|
||||
public boolean isWalkable(float wx, float wy, float size) {
|
||||
float half = size / 2;
|
||||
float[][] corners = {
|
||||
{wx - half, wy - half}, {wx + half, wy - half},
|
||||
{wx - half, wy + half}, {wx + half, wy + half}
|
||||
};
|
||||
for (float[] c : corners) {
|
||||
int gx = (int) Math.floor(c[0]);
|
||||
int gy = (int) Math.floor(c[1]);
|
||||
if (isWall(gx, gy)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int[][] getCells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
public List<int[]> getSpawnPoints() {
|
||||
List<int[]> points = new ArrayList<>();
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (cells[y][x] == 2) {
|
||||
points.add(new int[]{x, y});
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
public List<int[]> getZombieSpawnPoints() {
|
||||
List<int[]> points = new ArrayList<>();
|
||||
int[][] zombieSpawns = {{8, 8}, {24, 24}};
|
||||
for (int[] sp : zombieSpawns) {
|
||||
points.add(sp);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
public List<float[]> findPath(float startX, float startY, float endX, float endY) {
|
||||
int sgx = (int) Math.floor(startX);
|
||||
int sgy = (int) Math.floor(startY);
|
||||
int egx = (int) Math.floor(endX);
|
||||
int egy = (int) Math.floor(endY);
|
||||
|
||||
if (isWall(egx, egy)) return null;
|
||||
|
||||
PriorityQueue<PathNode> openSet = new PriorityQueue<>(Comparator.comparingDouble(n -> n.f));
|
||||
Set<String> closedSet = new HashSet<>();
|
||||
Map<String, String> cameFrom = new HashMap<>();
|
||||
|
||||
String startKey = sgx + "," + sgy;
|
||||
double h = Math.abs(sgx - egx) + Math.abs(sgy - egy);
|
||||
openSet.add(new PathNode(sgx, sgy, 0, h));
|
||||
|
||||
int[][] dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
int iterations = 0;
|
||||
|
||||
while (!openSet.isEmpty() && iterations < 2000) {
|
||||
iterations++;
|
||||
PathNode current = openSet.poll();
|
||||
String currentKey = current.x + "," + current.y;
|
||||
|
||||
if (current.x == egx && current.y == egy) {
|
||||
List<float[]> path = new ArrayList<>();
|
||||
String key = currentKey;
|
||||
while (cameFrom.containsKey(key)) {
|
||||
String[] parts = key.split(",");
|
||||
int cx = Integer.parseInt(parts[0]);
|
||||
int cy = Integer.parseInt(parts[1]);
|
||||
path.add(0, new float[]{cx + 0.5f, cy + 0.5f});
|
||||
key = cameFrom.get(key);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
closedSet.add(currentKey);
|
||||
|
||||
for (int[] dir : dirs) {
|
||||
int nx = current.x + dir[0];
|
||||
int ny = current.y + dir[1];
|
||||
String nKey = nx + "," + ny;
|
||||
|
||||
if (closedSet.contains(nKey)) continue;
|
||||
if (isWall(nx, ny)) continue;
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isWall(current.x + dir[0], current.y) || isWall(current.x, current.y + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
boolean diagonal = dir[0] != 0 && dir[1] != 0;
|
||||
double moveCost = diagonal ? 1.414 : 1.0;
|
||||
double g = current.g + moveCost;
|
||||
|
||||
double nh = Math.abs(nx - egx) + Math.abs(ny - egy);
|
||||
openSet.add(new PathNode(nx, ny, g, nh));
|
||||
if (!cameFrom.containsKey(nKey)) {
|
||||
cameFrom.put(nKey, currentKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class PathNode {
|
||||
int x, y;
|
||||
double g, h, f;
|
||||
|
||||
PathNode(int x, int y, double g, double h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.g = g;
|
||||
this.h = h;
|
||||
this.f = g + h;
|
||||
}
|
||||
}
|
||||
}
|
||||
448
backend/src/main/java/com/zombie/game/model/GameWorld.java
Normal file
448
backend/src/main/java/com/zombie/game/model/GameWorld.java
Normal file
@@ -0,0 +1,448 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class GameWorld {
|
||||
private GameMap map;
|
||||
private Map<String, Player> players;
|
||||
private Map<Integer, Zombie> zombies;
|
||||
private Map<Integer, Bullet> bullets;
|
||||
private Map<Integer, Bullet> zombieBullets;
|
||||
private Map<Integer, Loot> loots;
|
||||
private int nextBulletId;
|
||||
private int nextZombieId;
|
||||
private int nextLootId;
|
||||
private int nextZombieBulletId;
|
||||
private float gameTime;
|
||||
private int waveNumber;
|
||||
private int score;
|
||||
private float spawnTimer;
|
||||
private float difficultyTimer;
|
||||
private float zombieHealth;
|
||||
private float zombieSpeed;
|
||||
private float spawnInterval;
|
||||
private Random random;
|
||||
private List<Map<String, Object>> explosions;
|
||||
private List<Integer> removedBullets;
|
||||
private List<Integer> removedZombieBullets;
|
||||
|
||||
public GameWorld() {
|
||||
this.map = new GameMap();
|
||||
this.players = new LinkedHashMap<>();
|
||||
this.zombies = new LinkedHashMap<>();
|
||||
this.bullets = new LinkedHashMap<>();
|
||||
this.zombieBullets = new LinkedHashMap<>();
|
||||
this.loots = new LinkedHashMap<>();
|
||||
this.nextBulletId = 0;
|
||||
this.nextZombieId = 0;
|
||||
this.nextLootId = 0;
|
||||
this.nextZombieBulletId = 0;
|
||||
this.gameTime = 0;
|
||||
this.waveNumber = 0;
|
||||
this.score = 0;
|
||||
this.spawnTimer = 0;
|
||||
this.difficultyTimer = 0;
|
||||
this.zombieHealth = Constants.ZOMBIE_BASE_HEALTH;
|
||||
this.zombieSpeed = Constants.ZOMBIE_BASE_SPEED;
|
||||
this.spawnInterval = Constants.ZOMBIE_SPAWN_INTERVAL_BASE;
|
||||
this.random = new Random();
|
||||
this.explosions = new ArrayList<>();
|
||||
this.removedBullets = new ArrayList<>();
|
||||
this.removedZombieBullets = new ArrayList<>();
|
||||
}
|
||||
|
||||
public GameMap getMap() { return map; }
|
||||
|
||||
public void addPlayer(Player player) {
|
||||
List<int[]> spawnPoints = map.getSpawnPoints();
|
||||
int idx = players.size() % spawnPoints.size();
|
||||
int[] sp = spawnPoints.get(idx);
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
player.applyMovement(0, 0, map);
|
||||
players.put(player.getId(), player);
|
||||
}
|
||||
|
||||
public void removePlayer(String playerId) {
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
public Player getPlayer(String id) { return players.get(id); }
|
||||
public Collection<Player> getPlayers() { return players.values(); }
|
||||
public Collection<Zombie> getZombies() { return zombies.values(); }
|
||||
public Collection<Bullet> getBullets() { return bullets.values(); }
|
||||
public Collection<Loot> getLoots() { return loots.values(); }
|
||||
|
||||
public void update(float dt) {
|
||||
explosions.clear();
|
||||
removedBullets.clear();
|
||||
removedZombieBullets.clear();
|
||||
|
||||
gameTime += dt;
|
||||
spawnTimer += dt;
|
||||
difficultyTimer += dt;
|
||||
|
||||
if (difficultyTimer >= Constants.DIFFICULTY_INCREASE_INTERVAL) {
|
||||
difficultyTimer -= Constants.DIFFICULTY_INCREASE_INTERVAL;
|
||||
waveNumber++;
|
||||
spawnInterval = Math.max(Constants.ZOMBIE_SPAWN_INTERVAL_MIN,
|
||||
spawnInterval - 0.3f);
|
||||
}
|
||||
|
||||
if (spawnTimer >= spawnInterval) {
|
||||
spawnTimer -= spawnInterval;
|
||||
spawnZombie();
|
||||
}
|
||||
|
||||
updateZombies(dt);
|
||||
updateBullets(dt);
|
||||
updateZombieBullets(dt);
|
||||
checkBulletCollisions();
|
||||
checkZombieBulletCollisions();
|
||||
checkZombieAttacks();
|
||||
checkLootCollection();
|
||||
}
|
||||
|
||||
private void spawnZombie() {
|
||||
List<int[]> spawnPoints = map.getZombieSpawnPoints();
|
||||
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
|
||||
Player nearest = findNearestPlayer(wx, wy);
|
||||
if (nearest == null) return;
|
||||
|
||||
boolean isElite = random.nextFloat() < Constants.ELITE_ZOMBIE_SPAWN_CHANCE;
|
||||
Zombie zombie;
|
||||
if (isElite) {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, Constants.ELITE_ZOMBIE_HEALTH, Constants.ELITE_ZOMBIE_SPEED, true);
|
||||
} else {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, zombieHealth, zombieSpeed, false);
|
||||
}
|
||||
zombie.updatePath(map, nearest.getX(), nearest.getY());
|
||||
zombies.put(zombie.getId(), zombie);
|
||||
}
|
||||
|
||||
private Player findNearestPlayer(float x, float y) {
|
||||
Player nearest = null;
|
||||
float minDist = Float.MAX_VALUE;
|
||||
for (Player p : players.values()) {
|
||||
if (!p.isAlive()) continue;
|
||||
float dist = p.distanceTo(x, y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = p;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private void updateZombies(float dt) {
|
||||
long now = System.currentTimeMillis();
|
||||
for (Zombie z : new ArrayList<>(zombies.values())) {
|
||||
if (!z.isAlive()) {
|
||||
onZombieKilled(z);
|
||||
zombies.remove(z.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
Player nearest = findNearestPlayer(z.getX(), z.getY());
|
||||
if (nearest != null) {
|
||||
if (z.getPath() == null || z.getPathIndex() >= (z.getPath() != null ? z.getPath().size() : 0)
|
||||
|| random.nextFloat() < 0.02f) {
|
||||
z.updatePath(map, nearest.getX(), nearest.getY());
|
||||
}
|
||||
|
||||
if (z.isElite()) {
|
||||
float dist = z.distanceTo(nearest.getX(), nearest.getY());
|
||||
if (dist <= Constants.ELITE_ZOMBIE_ATTACK_RANGE && z.canRangedAttack(now)) {
|
||||
fireZombieBullet(z, nearest);
|
||||
z.rangedAttack(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
z.move(map, dt);
|
||||
}
|
||||
}
|
||||
|
||||
private void fireZombieBullet(Zombie zombie, Player target) {
|
||||
float dx = target.getX() - zombie.getX();
|
||||
float dy = target.getY() - zombie.getY();
|
||||
float angle = (float) Math.atan2(dx, dy);
|
||||
|
||||
float startX = zombie.getX() + (float) Math.sin(angle) * 0.5f;
|
||||
float startY = zombie.getY() + (float) Math.cos(angle) * 0.5f;
|
||||
|
||||
Bullet bullet = new Bullet(nextZombieBulletId++, startX, startY, angle,
|
||||
Constants.ELITE_ZOMBIE_BULLET_SPEED, Constants.ELITE_ZOMBIE_BULLET_DAMAGE,
|
||||
"zombie_" + zombie.getId(), "zombie_bullet", 15);
|
||||
zombieBullets.put(bullet.getId(), bullet);
|
||||
}
|
||||
|
||||
private void updateZombieBullets(float dt) {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Bullet b : zombieBullets.values()) {
|
||||
if (!b.update(dt, map)) {
|
||||
toRemove.add(b.getId());
|
||||
}
|
||||
}
|
||||
for (int id : toRemove) {
|
||||
zombieBullets.remove(id);
|
||||
removedZombieBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkZombieBulletCollisions() {
|
||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||
for (Bullet b : new ArrayList<>(zombieBullets.values())) {
|
||||
for (Player p : new ArrayList<>(players.values())) {
|
||||
if (!p.isAlive()) continue;
|
||||
if (b.hitsEntity(p.getX(), p.getY(), Constants.PLAYER_SIZE)) {
|
||||
p.takeDamage(b.getDamage());
|
||||
bulletsToRemove.add(b.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int id : bulletsToRemove) {
|
||||
zombieBullets.remove(id);
|
||||
removedZombieBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void onZombieKilled(Zombie z) {
|
||||
score += z.isElite() ? 50 : 10;
|
||||
if (random.nextFloat() < Constants.ZOMBIE_LOOT_DROP_CHANCE) {
|
||||
String lootType = random.nextFloat() < 0.5f ? Constants.LOOT_TYPE_AMMO : Constants.LOOT_TYPE_HEALTH;
|
||||
Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType);
|
||||
loots.put(loot.getId(), loot);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBullets(float dt) {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Bullet b : bullets.values()) {
|
||||
if (!b.update(dt, map)) {
|
||||
if (b.isExplosive()) {
|
||||
createExplosion(b.getX(), b.getY(), b.getExplosionRadius(), b.getOwnerId());
|
||||
}
|
||||
toRemove.add(b.getId());
|
||||
}
|
||||
}
|
||||
for (int id : toRemove) {
|
||||
bullets.remove(id);
|
||||
removedBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkBulletCollisions() {
|
||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||
for (Bullet b : new ArrayList<>(bullets.values())) {
|
||||
if (b.isGrenade()) continue;
|
||||
for (Zombie z : new ArrayList<>(zombies.values())) {
|
||||
if (!z.isAlive()) continue;
|
||||
if (b.hitsEntity(z.getX(), z.getY(), Constants.ZOMBIE_SIZE)) {
|
||||
z.takeDamage(b.getDamage());
|
||||
bulletsToRemove.add(b.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int id : bulletsToRemove) {
|
||||
bullets.remove(id);
|
||||
removedBullets.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void createExplosion(float x, float y, float radius, String ownerId) {
|
||||
Map<String, Object> exp = new LinkedHashMap<>();
|
||||
exp.put("x", x);
|
||||
exp.put("y", y);
|
||||
exp.put("radius", radius);
|
||||
explosions.add(exp);
|
||||
|
||||
for (Zombie z : new ArrayList<>(zombies.values())) {
|
||||
float dist = z.distanceTo(x, y);
|
||||
if (dist < radius) {
|
||||
z.takeDamage(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkZombieAttacks() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (Zombie z : zombies.values()) {
|
||||
if (!z.isAlive()) continue;
|
||||
for (Player p : players.values()) {
|
||||
if (!p.isAlive()) continue;
|
||||
float dist = z.distanceTo(p.getX(), p.getY());
|
||||
if (dist < 1.0f && z.canAttack(now)) {
|
||||
p.takeDamage(Constants.ZOMBIE_DAMAGE);
|
||||
z.attack(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLootCollection() {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Loot loot : loots.values()) {
|
||||
for (Player p : players.values()) {
|
||||
if (!p.isAlive()) continue;
|
||||
if (loot.isCollectedBy(p.getX(), p.getY())) {
|
||||
if (loot.getType().equals(Constants.LOOT_TYPE_HEALTH)) {
|
||||
p.heal(Constants.LOOT_HEALTH_AMOUNT);
|
||||
} else {
|
||||
p.refillRandomWeapon();
|
||||
}
|
||||
toRemove.add(loot.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int id : toRemove) {
|
||||
loots.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Integer> fireWeapon(Player player, float aimX, float aimY) {
|
||||
return fireWeapon(player, aimX, aimY, 0);
|
||||
}
|
||||
|
||||
public List<Integer> fireWeapon(Player player, float aimX, float aimY, float chargePercent) {
|
||||
List<Integer> newBulletIds = new ArrayList<>();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (!player.canFire(now) || !player.hasAmmo()) return newBulletIds;
|
||||
|
||||
player.setAngle(aimX, aimY);
|
||||
player.fire(now);
|
||||
|
||||
String weapon;
|
||||
switch (player.getWeaponIndex()) {
|
||||
case 1: weapon = Constants.WEAPON_MACHINE_GUN; break;
|
||||
case 2: weapon = Constants.WEAPON_SHOTGUN; break;
|
||||
case 3: weapon = Constants.WEAPON_GRENADE; break;
|
||||
default: weapon = Constants.WEAPON_PISTOL;
|
||||
}
|
||||
|
||||
if (weapon.equals(Constants.WEAPON_GRENADE)) {
|
||||
float startX = player.getX();
|
||||
float startY = player.getY();
|
||||
|
||||
float minDist = 3.0f;
|
||||
float maxDist = 15.0f;
|
||||
float dist = minDist + (maxDist - minDist) * chargePercent;
|
||||
|
||||
float dx = aimX - startX;
|
||||
float dy = aimY - startY;
|
||||
float targetDist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
float targetX, targetY;
|
||||
if (targetDist < 0.1f) {
|
||||
targetX = startX + minDist;
|
||||
targetY = startY;
|
||||
} else {
|
||||
float scale = Math.min(dist, targetDist) / targetDist;
|
||||
targetX = startX + dx * scale;
|
||||
targetY = startY + dy * scale;
|
||||
}
|
||||
|
||||
targetX = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, targetX));
|
||||
targetY = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, targetY));
|
||||
|
||||
float flightDuration = 0.8f + chargePercent * 0.7f;
|
||||
|
||||
Bullet bullet = new Bullet(nextBulletId++, startX, startY, targetX, targetY,
|
||||
flightDuration, player.getDamage(), player.getId(), 3.0f);
|
||||
bullets.put(bullet.getId(), bullet);
|
||||
newBulletIds.add(bullet.getId());
|
||||
} else {
|
||||
int pellets = player.getPelletCount();
|
||||
float spread = player.getSpread();
|
||||
|
||||
for (int i = 0; i < pellets; i++) {
|
||||
float angle = player.getAngle();
|
||||
if (spread > 0) {
|
||||
angle += (random.nextFloat() - 0.5f) * spread * 2;
|
||||
}
|
||||
|
||||
float startX = player.getX() + (float) Math.sin(angle) * 0.5f;
|
||||
float startY = player.getY() + (float) Math.cos(angle) * 0.5f;
|
||||
|
||||
float speed = player.getBulletSpeed();
|
||||
int damage = player.getDamage();
|
||||
float range;
|
||||
switch (weapon) {
|
||||
case Constants.WEAPON_MACHINE_GUN: range = 25; break;
|
||||
case Constants.WEAPON_SHOTGUN: range = 12; break;
|
||||
default: range = 30;
|
||||
}
|
||||
|
||||
Bullet bullet = new Bullet(nextBulletId++, startX, startY, angle,
|
||||
speed, damage, player.getId(), weapon, range);
|
||||
bullets.put(bullet.getId(), bullet);
|
||||
newBulletIds.add(bullet.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return newBulletIds;
|
||||
}
|
||||
|
||||
public int[][] getMapData() {
|
||||
return map.getCells();
|
||||
}
|
||||
|
||||
public Map<String, Object> buildGameState(String forPlayerId) {
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
|
||||
List<Map<String, Object>> playerStates = new ArrayList<>();
|
||||
for (Player p : players.values()) {
|
||||
playerStates.add(p.toStateMap());
|
||||
}
|
||||
state.put("players", playerStates);
|
||||
|
||||
List<Map<String, Object>> zombieStates = new ArrayList<>();
|
||||
for (Zombie z : zombies.values()) {
|
||||
zombieStates.add(z.toStateMap());
|
||||
}
|
||||
state.put("zombies", zombieStates);
|
||||
|
||||
List<Map<String, Object>> bulletStates = new ArrayList<>();
|
||||
for (Bullet b : bullets.values()) {
|
||||
bulletStates.add(b.toStateMap());
|
||||
}
|
||||
state.put("bullets", bulletStates);
|
||||
|
||||
List<Map<String, Object>> zombieBulletStates = new ArrayList<>();
|
||||
for (Bullet b : zombieBullets.values()) {
|
||||
zombieBulletStates.add(b.toStateMap());
|
||||
}
|
||||
state.put("zombieBullets", zombieBulletStates);
|
||||
|
||||
List<Map<String, Object>> lootStates = new ArrayList<>();
|
||||
for (Loot l : loots.values()) {
|
||||
lootStates.add(l.toStateMap());
|
||||
}
|
||||
state.put("loots", lootStates);
|
||||
|
||||
state.put("explosions", new ArrayList<>(explosions));
|
||||
state.put("removedBullets", new ArrayList<>(removedBullets));
|
||||
state.put("gameTime", gameTime);
|
||||
state.put("waveNumber", waveNumber);
|
||||
state.put("score", score);
|
||||
|
||||
Player forPlayer = players.get(forPlayerId);
|
||||
if (forPlayer != null) {
|
||||
Map<String, Object> ammoMap = new LinkedHashMap<>();
|
||||
ammoMap.put(Constants.WEAPON_PISTOL, forPlayer.getAmmo()[0] == Integer.MAX_VALUE ? -1 : forPlayer.getAmmo()[0]);
|
||||
ammoMap.put(Constants.WEAPON_MACHINE_GUN, (int) forPlayer.getAmmo()[1]);
|
||||
ammoMap.put(Constants.WEAPON_SHOTGUN, (int) forPlayer.getAmmo()[2]);
|
||||
ammoMap.put(Constants.WEAPON_GRENADE, (int) forPlayer.getAmmo()[3]);
|
||||
state.put("ammo", ammoMap);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
42
backend/src/main/java/com/zombie/game/model/Loot.java
Normal file
42
backend/src/main/java/com/zombie/game/model/Loot.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Loot {
|
||||
private int id;
|
||||
private float x, y;
|
||||
private String type;
|
||||
private long spawnTime;
|
||||
|
||||
public Loot(int id, float x, float y) {
|
||||
this(id, x, y, Constants.LOOT_TYPE_AMMO);
|
||||
}
|
||||
|
||||
public Loot(int id, float x, float y, String type) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.type = type;
|
||||
this.spawnTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getId() { return id; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public String getType() { return type; }
|
||||
|
||||
public boolean isCollectedBy(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return Math.sqrt(dx * dx + dy * dy) < 0.8f;
|
||||
}
|
||||
|
||||
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("type", type);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
203
backend/src/main/java/com/zombie/game/model/Player.java
Normal file
203
backend/src/main/java/com/zombie/game/model/Player.java
Normal file
@@ -0,0 +1,203 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Player {
|
||||
private String id;
|
||||
private String name;
|
||||
private float x, y;
|
||||
private float angle;
|
||||
private float health;
|
||||
private int weaponIndex;
|
||||
private boolean ready;
|
||||
private long lastAttackTime;
|
||||
private long lastDamageTime;
|
||||
private float[] ammo;
|
||||
private boolean firing;
|
||||
private float grenadeChargeStart;
|
||||
private boolean chargingGrenade;
|
||||
private int lastProcessedSeq;
|
||||
|
||||
private static final String[] WEAPONS = {
|
||||
Constants.WEAPON_PISTOL, Constants.WEAPON_MACHINE_GUN,
|
||||
Constants.WEAPON_SHOTGUN, Constants.WEAPON_GRENADE
|
||||
};
|
||||
|
||||
private static final int[] MAX_AMMO = {Integer.MAX_VALUE, 100, 20, 10};
|
||||
|
||||
public Player(String id, String name, float x, float y) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.angle = 0;
|
||||
this.health = Constants.PLAYER_MAX_HEALTH;
|
||||
this.weaponIndex = 0;
|
||||
this.ready = false;
|
||||
this.lastAttackTime = 0;
|
||||
this.lastDamageTime = 0;
|
||||
this.ammo = new float[]{Integer.MAX_VALUE, 100, 20, 10};
|
||||
this.firing = false;
|
||||
this.grenadeChargeStart = 0;
|
||||
this.chargingGrenade = false;
|
||||
this.lastProcessedSeq = 0;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public float getAngle() { return angle; }
|
||||
public float getHealth() { return health; }
|
||||
public int getWeaponIndex() { return weaponIndex; }
|
||||
public boolean isReady() { return ready; }
|
||||
public float[] getAmmo() { return ammo; }
|
||||
public boolean isFiring() { return firing; }
|
||||
public int getLastProcessedSeq() { return lastProcessedSeq; }
|
||||
|
||||
public void setReady(boolean ready) { this.ready = ready; }
|
||||
public void setWeaponIndex(int idx) { this.weaponIndex = Math.max(0, Math.min(3, idx)); }
|
||||
|
||||
public void applyMovement(float dx, float dy, GameMap map) {
|
||||
float speed = Constants.PLAYER_SPEED * Constants.TICK_INTERVAL;
|
||||
float newX = x + dx * speed;
|
||||
float newY = y + dy * speed;
|
||||
|
||||
if (map.isWalkable(newX, y, Constants.PLAYER_SIZE)) {
|
||||
x = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, newX));
|
||||
}
|
||||
if (map.isWalkable(x, newY, Constants.PLAYER_SIZE)) {
|
||||
y = Math.max(0.5f, Math.min(Constants.GRID_SIZE - 0.5f, newY));
|
||||
}
|
||||
}
|
||||
|
||||
public void setAngle(float aimX, float aimY) {
|
||||
this.angle = (float) Math.atan2(aimX - x, aimY - y);
|
||||
}
|
||||
|
||||
public void takeDamage(float damage) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastDamageTime < Constants.PLAYER_INVULNERABLE_TIME * 1000) return;
|
||||
this.health -= damage;
|
||||
this.lastDamageTime = now;
|
||||
if (this.health < 0) this.health = 0;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
public float distanceTo(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
public boolean canFire(long now) {
|
||||
String weapon = WEAPONS[weaponIndex];
|
||||
long fireRate = getFireRate(weapon);
|
||||
return now - lastAttackTime >= fireRate;
|
||||
}
|
||||
|
||||
public void fire(long now) {
|
||||
lastAttackTime = now;
|
||||
String weapon = WEAPONS[weaponIndex];
|
||||
int idx = weaponIndex;
|
||||
if (idx != 0 && ammo[idx] > 0) {
|
||||
ammo[idx]--;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAmmo() {
|
||||
if (weaponIndex == 0) return true;
|
||||
return ammo[weaponIndex] > 0;
|
||||
}
|
||||
|
||||
public void refillRandomWeapon() {
|
||||
Random rand = new Random();
|
||||
int idx = rand.nextInt(3) + 1;
|
||||
ammo[idx] = MAX_AMMO[idx];
|
||||
}
|
||||
|
||||
public void heal(float amount) {
|
||||
this.health = Math.min(Constants.PLAYER_MAX_HEALTH, this.health + amount);
|
||||
}
|
||||
|
||||
public void setFiring(boolean firing) { this.firing = firing; }
|
||||
public void setLastProcessedSeq(int seq) { this.lastProcessedSeq = seq; }
|
||||
|
||||
public void startGrenadeCharge() {
|
||||
if (!chargingGrenade) {
|
||||
chargingGrenade = true;
|
||||
grenadeChargeStart = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
public float getGrenadeChargePercent() {
|
||||
if (!chargingGrenade) return 0;
|
||||
float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f;
|
||||
return Math.min(1.0f, elapsed);
|
||||
}
|
||||
|
||||
public void stopGrenadeCharge() {
|
||||
chargingGrenade = false;
|
||||
}
|
||||
|
||||
private long getFireRate(String weapon) {
|
||||
switch (weapon) {
|
||||
case Constants.WEAPON_PISTOL: return 400;
|
||||
case Constants.WEAPON_MACHINE_GUN: return 100;
|
||||
case Constants.WEAPON_SHOTGUN: return 800;
|
||||
case Constants.WEAPON_GRENADE: return 1500;
|
||||
default: return 400;
|
||||
}
|
||||
}
|
||||
|
||||
public int getDamage() {
|
||||
switch (WEAPONS[weaponIndex]) {
|
||||
case Constants.WEAPON_PISTOL: return 50;
|
||||
case Constants.WEAPON_MACHINE_GUN: return 50;
|
||||
case Constants.WEAPON_SHOTGUN: return 50;
|
||||
case Constants.WEAPON_GRENADE: return 120;
|
||||
default: return 50;
|
||||
}
|
||||
}
|
||||
|
||||
public float getBulletSpeed() {
|
||||
switch (WEAPONS[weaponIndex]) {
|
||||
case Constants.WEAPON_PISTOL: return 20;
|
||||
case Constants.WEAPON_MACHINE_GUN: return 25;
|
||||
case Constants.WEAPON_SHOTGUN: return 18;
|
||||
case Constants.WEAPON_GRENADE: return 12;
|
||||
default: return 20;
|
||||
}
|
||||
}
|
||||
|
||||
public int getPelletCount() {
|
||||
return WEAPONS[weaponIndex].equals(Constants.WEAPON_SHOTGUN) ? 10 : 1;
|
||||
}
|
||||
|
||||
public float getSpread() {
|
||||
switch (WEAPONS[weaponIndex]) {
|
||||
case Constants.WEAPON_MACHINE_GUN: return 0.05f;
|
||||
case Constants.WEAPON_SHOTGUN: return 0.15f;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isChargeable() {
|
||||
return WEAPONS[weaponIndex].equals(Constants.WEAPON_GRENADE);
|
||||
}
|
||||
|
||||
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("weaponIndex", weaponIndex);
|
||||
map.put("lastProcessedSeq", lastProcessedSeq);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
88
backend/src/main/java/com/zombie/game/model/Room.java
Normal file
88
backend/src/main/java/com/zombie/game/model/Room.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Room {
|
||||
private String id;
|
||||
private String hostId;
|
||||
private Map<String, Player> players;
|
||||
private boolean gameStarted;
|
||||
private int maxPlayers = 4;
|
||||
|
||||
public Room(String id, String hostId, String hostName) {
|
||||
this.id = id;
|
||||
this.hostId = hostId;
|
||||
this.players = new LinkedHashMap<>();
|
||||
Player host = new Player(hostId, hostName, 0, 0);
|
||||
players.put(hostId, host);
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getHostId() { return hostId; }
|
||||
public boolean isGameStarted() { return gameStarted; }
|
||||
public void setGameStarted(boolean started) { this.gameStarted = started; }
|
||||
|
||||
public boolean addPlayer(String playerId, String playerName) {
|
||||
if (players.size() >= maxPlayers) return false;
|
||||
if (players.containsKey(playerId)) return false;
|
||||
Player player = new Player(playerId, playerName, 0, 0);
|
||||
players.put(playerId, player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void removePlayer(String playerId) {
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
public Player getPlayer(String playerId) {
|
||||
return players.get(playerId);
|
||||
}
|
||||
|
||||
public Collection<Player> getPlayers() {
|
||||
return players.values();
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
public boolean isHost(String playerId) {
|
||||
return hostId.equals(playerId);
|
||||
}
|
||||
|
||||
public boolean allReady() {
|
||||
for (Player p : players.values()) {
|
||||
if (!p.getId().equals(hostId) && !p.isReady()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Map<String, Object> toStateMap(String playerId) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("roomId", id);
|
||||
map.put("hostId", hostId);
|
||||
map.put("isHost", hostId.equals(playerId));
|
||||
map.put("playerId", playerId);
|
||||
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (Player p : players.values()) {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
pm.put("ready", p.isReady());
|
||||
pm.put("index", index++);
|
||||
playerList.add(pm);
|
||||
}
|
||||
map.put("players", playerList);
|
||||
return map;
|
||||
}
|
||||
|
||||
public Map<String, Object> toRoomListMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
map.put("hostName", players.get(hostId) != null ? players.get(hostId).getName() : "Unknown");
|
||||
map.put("playerCount", players.size());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
129
backend/src/main/java/com/zombie/game/model/Zombie.java
Normal file
129
backend/src/main/java/com/zombie/game/model/Zombie.java
Normal file
@@ -0,0 +1,129 @@
|
||||
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 List<float[]> path;
|
||||
private int pathIndex;
|
||||
private float targetX, targetY;
|
||||
private boolean isElite;
|
||||
private long lastRangedAttackTime;
|
||||
|
||||
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.path = null;
|
||||
this.pathIndex = 0;
|
||||
this.isElite = isElite;
|
||||
this.lastRangedAttackTime = 0;
|
||||
}
|
||||
|
||||
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 List<float[]> getPath() { return path; }
|
||||
public int getPathIndex() { return pathIndex; }
|
||||
public boolean isElite() { return isElite; }
|
||||
|
||||
public void takeDamage(float damage) {
|
||||
this.health -= damage;
|
||||
if (this.health < 0) this.health = 0;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
public void updatePath(GameMap map, float targetX, float targetY) {
|
||||
this.targetX = targetX;
|
||||
this.targetY = targetY;
|
||||
this.path = map.findPath(x, y, targetX, targetY);
|
||||
this.pathIndex = 0;
|
||||
}
|
||||
|
||||
public void move(GameMap map, float dt) {
|
||||
if (path != null && pathIndex < path.size()) {
|
||||
float[] target = path.get(pathIndex);
|
||||
float dx = target[0] - x;
|
||||
float dy = target[1] - y;
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 0.2f) {
|
||||
pathIndex++;
|
||||
if (pathIndex >= path.size()) {
|
||||
path = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float moveX = (dx / dist) * speed * dt;
|
||||
float moveY = (dy / dist) * speed * dt;
|
||||
|
||||
float newX = x + moveX;
|
||||
float newY = y + moveY;
|
||||
|
||||
if (map.isWalkable(newX, y, Constants.ZOMBIE_SIZE)) {
|
||||
x = newX;
|
||||
}
|
||||
if (map.isWalkable(x, newY, Constants.ZOMBIE_SIZE)) {
|
||||
y = newY;
|
||||
}
|
||||
|
||||
angle = (float) Math.atan2(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
46
backend/src/main/java/com/zombie/game/server/GameLoop.java
Normal file
46
backend/src/main/java/com/zombie/game/server/GameLoop.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.model.GameWorld;
|
||||
|
||||
public class GameLoop implements Runnable {
|
||||
private String roomId;
|
||||
private GameWorld world;
|
||||
private GameWebSocketServer server;
|
||||
private volatile boolean running;
|
||||
private static final int TICK_RATE = 30;
|
||||
private static final long TICK_INTERVAL = 1000 / TICK_RATE;
|
||||
|
||||
public GameLoop(String roomId, GameWorld world, GameWebSocketServer server) {
|
||||
this.roomId = roomId;
|
||||
this.world = world;
|
||||
this.server = server;
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long lastTime = System.currentTimeMillis();
|
||||
|
||||
while (running) {
|
||||
long now = System.currentTimeMillis();
|
||||
long delta = now - lastTime;
|
||||
|
||||
if (delta >= TICK_INTERVAL) {
|
||||
float dt = delta / 1000.0f;
|
||||
world.update(dt);
|
||||
server.broadcastGameState(roomId, world);
|
||||
lastTime = now;
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.zombie.game.model.*;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class GameWebSocketServer extends WebSocketServer {
|
||||
private Gson gson;
|
||||
private Map<String, Room> rooms;
|
||||
private Map<WebSocket, String> connectionToPlayer;
|
||||
private Map<String, WebSocket> playerToConnection;
|
||||
private Map<String, GameWorld> activeGames;
|
||||
private Map<String, GameLoop> gameLoops;
|
||||
private Timer roomListTimer;
|
||||
|
||||
public GameWebSocketServer(int port) {
|
||||
super(new InetSocketAddress(port));
|
||||
this.gson = new Gson();
|
||||
this.rooms = new ConcurrentHashMap<>();
|
||||
this.connectionToPlayer = new ConcurrentHashMap<>();
|
||||
this.playerToConnection = new ConcurrentHashMap<>();
|
||||
this.activeGames = new ConcurrentHashMap<>();
|
||||
this.gameLoops = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||
System.out.println("New connection: " + conn.getRemoteSocketAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||
System.out.println("Connection closed: " + conn.getRemoteSocketAddress());
|
||||
String playerId = connectionToPlayer.remove(conn);
|
||||
if (playerId != null) {
|
||||
playerToConnection.remove(playerId);
|
||||
handleLeaveRoomByPlayerId(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket conn, String message) {
|
||||
try {
|
||||
JsonObject msg = gson.fromJson(message, JsonObject.class);
|
||||
String type = msg.get("type").getAsString();
|
||||
JsonObject data = msg.has("data") ? msg.getAsJsonObject("data") : new JsonObject();
|
||||
|
||||
switch (type) {
|
||||
case Constants.MSG_CREATE_ROOM:
|
||||
handleCreateRoom(conn, data);
|
||||
break;
|
||||
case Constants.MSG_JOIN_ROOM:
|
||||
handleJoinRoom(conn, data);
|
||||
break;
|
||||
case Constants.MSG_LEAVE_ROOM:
|
||||
handleLeaveRoomByConn(conn);
|
||||
break;
|
||||
case Constants.MSG_ROOM_LIST:
|
||||
handleRoomList(conn);
|
||||
break;
|
||||
case Constants.MSG_READY:
|
||||
handleReady(conn);
|
||||
break;
|
||||
case Constants.MSG_START_GAME:
|
||||
handleStartGame(conn);
|
||||
break;
|
||||
case Constants.MSG_PLAYER_INPUT:
|
||||
handlePlayerInput(conn, data);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
sendError(conn, "Invalid message format");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket conn, Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
System.out.println("Game WebSocket Server started on port " + getPort());
|
||||
roomListTimer = new Timer(true);
|
||||
roomListTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
broadcastRoomList();
|
||||
}
|
||||
}, 0, 2000);
|
||||
}
|
||||
|
||||
private void handleCreateRoom(WebSocket conn, JsonObject data) {
|
||||
String playerName = data.get("playerName").getAsString();
|
||||
String playerId = UUID.randomUUID().toString();
|
||||
String roomId = UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
connectionToPlayer.put(conn, playerId);
|
||||
playerToConnection.put(playerId, conn);
|
||||
|
||||
Room room = new Room(roomId, playerId, playerName);
|
||||
rooms.put(roomId, room);
|
||||
|
||||
sendToConnection(conn, Constants.MSG_ROOM_STATE, room.toStateMap(playerId));
|
||||
System.out.println("Room created: " + roomId + " by " + playerName);
|
||||
}
|
||||
|
||||
private void handleJoinRoom(WebSocket conn, JsonObject data) {
|
||||
String roomId = data.get("roomId").getAsString();
|
||||
String playerName = data.get("playerName").getAsString();
|
||||
|
||||
Room room = rooms.get(roomId);
|
||||
if (room == null) {
|
||||
sendError(conn, "Room not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.getPlayerCount() >= 4) {
|
||||
sendError(conn, "Room is full");
|
||||
return;
|
||||
}
|
||||
|
||||
String playerId = UUID.randomUUID().toString();
|
||||
connectionToPlayer.put(conn, playerId);
|
||||
playerToConnection.put(playerId, conn);
|
||||
|
||||
room.addPlayer(playerId, playerName);
|
||||
|
||||
broadcastRoomState(room);
|
||||
System.out.println("Player " + playerName + " joined room " + roomId);
|
||||
}
|
||||
|
||||
private void handleLeaveRoomByConn(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
handleLeaveRoomByPlayerId(playerId);
|
||||
}
|
||||
|
||||
private void handleLeaveRoomByPlayerId(String playerId) {
|
||||
for (Room room : new ArrayList<>(rooms.values())) {
|
||||
if (room.getPlayer(playerId) != null) {
|
||||
room.removePlayer(playerId);
|
||||
if (room.getPlayerCount() == 0) {
|
||||
stopGame(room.getId());
|
||||
rooms.remove(room.getId());
|
||||
} else {
|
||||
broadcastRoomState(room);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRoomList(WebSocket conn) {
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
for (Room room : rooms.values()) {
|
||||
if (!room.isGameStarted()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("rooms", roomList);
|
||||
sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
|
||||
}
|
||||
|
||||
private void handleReady(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
for (Room room : rooms.values()) {
|
||||
Player player = room.getPlayer(playerId);
|
||||
if (player != null) {
|
||||
player.setReady(!player.isReady());
|
||||
broadcastRoomState(room);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleStartGame(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
for (Room room : rooms.values()) {
|
||||
if (room.isHost(playerId) && !room.isGameStarted() && room.allReady()) {
|
||||
room.setGameStarted(true);
|
||||
startGame(room);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startGame(Room room) {
|
||||
GameWorld world = new GameWorld();
|
||||
int index = 0;
|
||||
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
int[] sp = spawnPoints.get(index % spawnPoints.size());
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
|
||||
try {
|
||||
java.lang.reflect.Field xField = Player.class.getDeclaredField("x");
|
||||
xField.setAccessible(true);
|
||||
xField.setFloat(player, wx);
|
||||
java.lang.reflect.Field yField = Player.class.getDeclaredField("y");
|
||||
yField.setAccessible(true);
|
||||
yField.setFloat(player, wy);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
world.addPlayer(player);
|
||||
index++;
|
||||
}
|
||||
|
||||
activeGames.put(room.getId(), world);
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("playerId", player.getId());
|
||||
data.put("mapData", serializeMapData(world.getMapData()));
|
||||
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int idx = 0;
|
||||
for (Player p : room.getPlayers()) {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
pm.put("x", p.getX());
|
||||
pm.put("y", p.getY());
|
||||
pm.put("index", idx++);
|
||||
playerList.add(pm);
|
||||
}
|
||||
data.put("players", playerList);
|
||||
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
if (pConn != null) {
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STARTED, data);
|
||||
}
|
||||
}
|
||||
|
||||
GameLoop loop = new GameLoop(room.getId(), world, this);
|
||||
gameLoops.put(room.getId(), loop);
|
||||
new Thread(loop).start();
|
||||
|
||||
System.out.println("Game started for room: " + room.getId());
|
||||
}
|
||||
|
||||
private void stopGame(String roomId) {
|
||||
GameLoop loop = gameLoops.remove(roomId);
|
||||
if (loop != null) {
|
||||
loop.stop();
|
||||
}
|
||||
activeGames.remove(roomId);
|
||||
}
|
||||
|
||||
private void handlePlayerInput(WebSocket conn, JsonObject data) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
for (Room room : rooms.values()) {
|
||||
if (room.isGameStarted()) {
|
||||
GameWorld world = activeGames.get(room.getId());
|
||||
if (world == null) continue;
|
||||
|
||||
Player player = world.getPlayer(playerId);
|
||||
if (player == null) continue;
|
||||
|
||||
float dx = data.has("dx") ? data.get("dx").getAsFloat() : 0;
|
||||
float dy = data.has("dy") ? data.get("dy").getAsFloat() : 0;
|
||||
float aimX = data.has("aimX") ? data.get("aimX").getAsFloat() : 0;
|
||||
float aimY = data.has("aimY") ? data.get("aimY").getAsFloat() : 0;
|
||||
boolean firing = data.has("firing") && data.get("firing").getAsBoolean();
|
||||
int weaponIndex = data.has("weaponIndex") ? data.get("weaponIndex").getAsInt() : -1;
|
||||
int seq = data.has("seq") ? data.get("seq").getAsInt() : 0;
|
||||
float grenadeCharge = data.has("grenadeCharge") ? data.get("grenadeCharge").getAsFloat() : 0;
|
||||
boolean grenadeReleased = data.has("grenadeReleased") && data.get("grenadeReleased").getAsBoolean();
|
||||
|
||||
if (weaponIndex >= 0 && weaponIndex <= 3) {
|
||||
player.setWeaponIndex(weaponIndex);
|
||||
}
|
||||
|
||||
player.applyMovement(dx, dy, world.getMap());
|
||||
player.setAngle(aimX, aimY);
|
||||
player.setLastProcessedSeq(seq);
|
||||
|
||||
if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) {
|
||||
world.fireWeapon(player, aimX, aimY, grenadeCharge);
|
||||
} else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) {
|
||||
world.fireWeapon(player, aimX, aimY);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastGameState(String roomId, GameWorld world) {
|
||||
Room room = rooms.get(roomId);
|
||||
if (room == null) return;
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
Map<String, Object> state = world.buildGameState(player.getId());
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
if (pConn != null && pConn.isOpen()) {
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastRoomState(Room room) {
|
||||
for (Player player : room.getPlayers()) {
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
if (pConn != null && pConn.isOpen()) {
|
||||
sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastRoomList() {
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
for (Room room : rooms.values()) {
|
||||
if (!room.isGameStarted()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("rooms", roomList);
|
||||
|
||||
for (WebSocket conn : getConnections()) {
|
||||
if (connectionToPlayer.get(conn) == null) {
|
||||
sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<List<Integer>> serializeMapData(int[][] cells) {
|
||||
List<List<Integer>> result = new ArrayList<>();
|
||||
for (int[] row : cells) {
|
||||
List<Integer> rowList = new ArrayList<>();
|
||||
for (int cell : row) {
|
||||
rowList.add(cell);
|
||||
}
|
||||
result.add(rowList);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void sendToConnection(WebSocket conn, String type, Object data) {
|
||||
if (conn != null && conn.isOpen()) {
|
||||
Map<String, Object> msg = new LinkedHashMap<>();
|
||||
msg.put("type", type);
|
||||
msg.put("data", data);
|
||||
conn.send(gson.toJson(msg));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(WebSocket conn, String message) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("message", message);
|
||||
sendToConnection(conn, Constants.MSG_ERROR, data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user