diff --git a/src/client/core/CommandManager.ts b/src/client/core/CommandManager.ts index 44344fe..9332526 100644 --- a/src/client/core/CommandManager.ts +++ b/src/client/core/CommandManager.ts @@ -1,6 +1,6 @@ -import { Player } from './Player.ts'; import { ChatOverlay } from '../ui/ChatOverlay.ts'; import { SettingsManager } from './SettingsManager.ts'; +import { Player } from '../../shared/Player.ts'; export class CommandManager { private localPlayer: Player; diff --git a/src/client/core/Game.ts b/src/client/core/Game.ts index aa449bf..0447f4c 100644 --- a/src/client/core/Game.ts +++ b/src/client/core/Game.ts @@ -1,4 +1,3 @@ -import { Player } from './Player.ts'; import { Renderer } from './Renderer.ts'; import { ChatOverlay } from '../ui/ChatOverlay.ts'; import { InputHandler } from '../input/InputHandler.ts'; @@ -9,6 +8,8 @@ import { HealthIndicator } from '../ui/HealthIndicator.ts'; import { MapLoader } from './MapLoader.ts'; import { RemoteItemRenderer } from './RemoteItemRenderer.ts'; import { TouchInputHandler } from '../input/TouchInputHandler.ts'; +import { SettingsManager } from './SettingsManager.ts'; +import { Player } from '../../shared/Player.ts'; export class Game { private localPlayer: Player; @@ -28,6 +29,7 @@ export class Game { constructor(container: HTMLElement) { this.gameIndex = Game.nextGameIndex++; this.localPlayer = new Player(); + this.localPlayer.name = SettingsManager.settings.name ?? this.localPlayer.name; this.chatOverlay = new ChatOverlay(container, this.localPlayer); this.networking = new Networking(this.localPlayer, this.chatOverlay); this.renderer = new Renderer(container, this.networking, this.localPlayer, this.chatOverlay); diff --git a/src/client/core/Inventory.ts b/src/client/core/Inventory.ts index 48a7471..4ab7fce 100644 --- a/src/client/core/Inventory.ts +++ b/src/client/core/Inventory.ts @@ -4,9 +4,9 @@ import { InputHandler } from '../input/InputHandler.ts'; import { BananaGun } from '../items/BananaGun.ts'; import { HeldItemInput } from '../input/HeldItemInput.ts'; import { Networking } from './Networking.ts'; -import { Player } from './Player.ts'; import { ItemBase, ItemType } from '../items/ItemBase.ts'; import { FishGun } from '../items/FishGun.ts'; +import { Player } from '../../shared/Player.ts'; export class Inventory { private inventoryItems: ItemBase[] = []; diff --git a/src/client/core/Networking.ts b/src/client/core/Networking.ts index 4b8e283..3ffefcb 100644 --- a/src/client/core/Networking.ts +++ b/src/client/core/Networking.ts @@ -1,26 +1,8 @@ -import { io, Socket } from 'socket.io-client'; -import { Player } from './Player.ts'; -import { ChatOverlay } from '../ui/ChatOverlay.ts'; import * as THREE from 'three'; - -export interface RemotePlayer { - idLastDamagedBy: number; - latency: number; - id: number; - position: { x: number; y: number; z: number }; - velocity: { x: number; y: number; z: number }; - lookQuaternion: [number, number, number, number]; - name: string; - gravity: number; - forced: boolean; - health: number; - inventory: number[]; - chatActive: boolean; - chatMsg: string; - playerSpectating: number; - gameMsgs: string[]; - gameMsgs2: string[]; -} +import { io } from 'socket.io-client'; +import { ChatOverlay } from '../ui/ChatOverlay.ts'; +import { CustomClientSocket } from '../../shared/messages.ts'; +import { Player, PlayerData } from '../../shared/Player.ts'; interface WorldItem { vector: { x: number; y: number; z: number }; @@ -48,9 +30,9 @@ interface LastUploadedLocalPlayer { } export class Networking { - private socket: Socket; + private socket: CustomClientSocket; private gameVersion: string = ''; - private remotePlayers: RemotePlayer[] = []; + private remotePlayers: PlayerData[] = []; private worldItems: WorldItem[] = []; private lastUploadedLocalPlayer: LastUploadedLocalPlayer | null = null; private lastUploadTime: number; @@ -107,21 +89,21 @@ export class Networking { this.lastLatencyTestGotResponse = true; }); - this.socket.on('remotePlayerData', (data: RemotePlayer[]) => { + this.socket.on('remotePlayerData', (data) => { this.remotePlayers = data; this.processRemotePlayerData(); }); - this.socket.on('worldItemData', (data: WorldItem[]) => { + this.socket.on('worldItemData', (data) => { this.worldItems = data; this.processWorldItemData(); }); - this.socket.on('chatMsg', (data: { id: number; name: string; message: string }) => { + this.socket.on('chatMsg', (data) => { if (data.id !== this.localPlayer.id) this.chatOverlay.addChatMessage(data); }); - this.socket.on('serverInfo', (data: ServerInfo) => { + this.socket.on('serverInfo', (data) => { this.serverInfo = data; this.onServerInfo(); }); @@ -180,7 +162,12 @@ export class Networking { if (remotePlayer.forced) { this.localPlayer.position.set(remotePlayer.position.x, remotePlayer.position.y, remotePlayer.position.z); this.localPlayer.velocity.set(remotePlayer.velocity.x, remotePlayer.velocity.y, remotePlayer.velocity.z); - this.localPlayer.lookQuaternion.set(...remotePlayer.lookQuaternion); + this.localPlayer.lookQuaternion.set( + remotePlayer.lookQuaternion.x, + remotePlayer.lookQuaternion.y, + remotePlayer.lookQuaternion.z, + remotePlayer.lookQuaternion.w, + ); //this.localPlayer.name = remotePlayer.name; this.localPlayer.gravity = remotePlayer.gravity; this.localPlayer.forcedAcknowledged = true; @@ -222,7 +209,7 @@ export class Networking { return this.messagesBeingTyped; } - public getRemotePlayerData(): RemotePlayer[] { + public getRemotePlayerData(): PlayerData[] { return this.remotePlayers; } @@ -244,7 +231,7 @@ export class Networking { } public applyDamage(id: number, damage: number) { - const player2 = this.remotePlayers.find((player) => player.id === id); + const player2 = this.remotePlayers.find((player) => player.id === id)!; const damageRequest = { localPlayer: this.localPlayer, targetPlayer: player2, diff --git a/src/client/core/Player.ts b/src/client/core/Player.ts deleted file mode 100644 index c88b55b..0000000 --- a/src/client/core/Player.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as THREE from 'three'; -import { SettingsManager } from './SettingsManager.ts'; - -export class Player { - public position: THREE.Vector3; - public velocity: THREE.Vector3; - public inputVelocity: THREE.Vector3; - public gravity: number; - public lookQuaternion: THREE.Quaternion; - public quaternion: THREE.Quaternion; - public id: number; - public gameVersion: string; - public name: string; - public speed: number; - public acceleration: number; - public chatActive: boolean; - public chatMsg: string; - public latency: number; - public health: number; - public forced: boolean; - public forcedAcknowledged: boolean; - public inventory: number[]; - public idLastDamagedBy: number; - public playerSpectating: number; - public gameMsgs: string[]; - public gameMsgs2: string[]; - - constructor() { - this.position = new THREE.Vector3(0, 100, 0); - this.velocity = new THREE.Vector3(0, 0, 0); - this.inputVelocity = new THREE.Vector3(); - this.gravity = 0; - this.lookQuaternion = new THREE.Quaternion(); - this.quaternion = new THREE.Quaternion(); - this.id = Math.floor(Math.random() * 1000000000); - this.gameVersion = ''; - this.name = ''; - this.speed = 5; - this.acceleration = 100; - this.chatActive = false; - this.chatMsg = ''; - this.latency = 1000; - this.health = 100; - this.forced = false; - this.forcedAcknowledged = false; - this.inventory = []; - this.idLastDamagedBy = -1; - this.playerSpectating = -1; - this.gameMsgs = []; - this.gameMsgs2 = []; - - const storedName = SettingsManager.settings.name; - if (storedName) this.name = storedName; - } -} diff --git a/src/client/core/RemotePlayerRenderer.ts b/src/client/core/RemotePlayerRenderer.ts index 437ffd0..34fd25f 100644 --- a/src/client/core/RemotePlayerRenderer.ts +++ b/src/client/core/RemotePlayerRenderer.ts @@ -2,27 +2,12 @@ import * as THREE from 'three'; import { Networking } from './Networking.ts'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; -import { Player } from './Player.ts'; import { acceleratedRaycast, computeBoundsTree } from 'three-mesh-bvh'; +import { Player, PlayerData } from '../../shared/Player.ts'; THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree; THREE.Mesh.prototype.raycast = acceleratedRaycast; -interface RemotePlayerData { - health: number; - id: number; - velocity: { x: number; y: number; z: number }; - position: { x: number; y: number; z: number }; - quaternion: [number, number, number, number]; // Add quaternion as required - forced: boolean; - name: string; - playerSpectating: number; -} - -interface RemotePlayer extends Omit { - quaternion?: [number, number, number, number]; // Optional in case it's missing -} - interface PlayerToRender { id: number; object: THREE.Object3D; @@ -122,7 +107,7 @@ export class RemotePlayerRenderer { private updateRemotePlayers(): void { if (!this.possumMesh) return; - const remotePlayerData: RemotePlayer[] = this.networking.getRemotePlayerData(); + const remotePlayerData: PlayerData[] = this.networking.getRemotePlayerData(); const localPlayerId = this.localPlayer.id; // First, remove all players that should be hidden @@ -161,7 +146,7 @@ export class RemotePlayerRenderer { return; } - const playerDataWithQuaternion: RemotePlayerData = { + const playerDataWithQuaternion: PlayerData = { ...remotePlayer, quaternion: remotePlayer.quaternion || [0, 0, 0, 1], }; @@ -178,7 +163,7 @@ export class RemotePlayerRenderer { private updatePlayerPosition( playerObject: THREE.Object3D, playerSphere: THREE.Object3D, - remotePlayerData: RemotePlayerData, + remotePlayerData: PlayerData, ): void { const velocity = Math.sqrt( Math.pow(remotePlayerData.velocity.x, 2) + @@ -265,10 +250,10 @@ export class RemotePlayerRenderer { // Apply quaternion slerp as before const targetQuaternion = new THREE.Quaternion( - remotePlayerData.quaternion[0], - remotePlayerData.quaternion[1], - remotePlayerData.quaternion[2], - remotePlayerData.quaternion[3], + remotePlayerData.quaternion.x, + remotePlayerData.quaternion.y, + remotePlayerData.quaternion.z, + remotePlayerData.quaternion.w, ); const rotationQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2); targetQuaternion.multiply(rotationQuaternion); @@ -302,7 +287,7 @@ export class RemotePlayerRenderer { } } - private addNewPlayer(remotePlayerData: RemotePlayerData): void { + private addNewPlayer(remotePlayerData: PlayerData): void { const object = this.possumMesh!.clone(); const sphere = this.sphere.clone(); @@ -331,7 +316,7 @@ export class RemotePlayerRenderer { ); } - private removeInactivePlayers(remotePlayerData: RemotePlayer[]): void { + private removeInactivePlayers(remotePlayerData: PlayerData[]): void { this.playersToRender = this.playersToRender.filter((player) => { const isActive = remotePlayerData.some((remotePlayer) => remotePlayer.id === player.id); if (!isActive) { diff --git a/src/client/core/Renderer.ts b/src/client/core/Renderer.ts index 6a46c16..256a9a0 100644 --- a/src/client/core/Renderer.ts +++ b/src/client/core/Renderer.ts @@ -1,11 +1,11 @@ import * as THREE from 'three'; -import { Networking, type RemotePlayer } from './Networking.ts'; -import { Player } from './Player.ts'; +import { Networking } from './Networking.ts'; import { ChatOverlay } from '../ui/ChatOverlay.ts'; import { RemotePlayerRenderer } from './RemotePlayerRenderer.ts'; import { InputHandler } from '../input/InputHandler.ts'; import { SettingsManager } from './SettingsManager.ts'; import { CollisionManager } from '../input/CollisionManager.ts'; +import { Player, PlayerData } from '../../shared/Player.ts'; export class Renderer { private clock: THREE.Clock; @@ -230,10 +230,10 @@ export class Renderer { // Simple quaternion slerp this.camera.quaternion.slerp( new THREE.Quaternion( - remotePlayer.lookQuaternion[0], - remotePlayer.lookQuaternion[1], - remotePlayer.lookQuaternion[2], - remotePlayer.lookQuaternion[3], + remotePlayer.lookQuaternion.x, + remotePlayer.lookQuaternion.y, + remotePlayer.lookQuaternion.z, + remotePlayer.lookQuaternion.w, ), 0.3 * this.deltaTime * 60, ); @@ -249,7 +249,7 @@ export class Renderer { this.knockbackVector.lerp(new THREE.Vector3(), 0.05 * this.deltaTime * 60); if (this.localPlayer.health < this.lastPlayerHealth) { - const remotePlayer: RemotePlayer | undefined = this.networking.getRemotePlayerData().find((player) => + const remotePlayer: PlayerData | undefined = this.networking.getRemotePlayerData().find((player) => player.id === this.localPlayer.idLastDamagedBy ); if (remotePlayer !== undefined) { diff --git a/src/client/input/CollisionManager.ts b/src/client/input/CollisionManager.ts index fe855d1..d431788 100644 --- a/src/client/input/CollisionManager.ts +++ b/src/client/input/CollisionManager.ts @@ -1,5 +1,4 @@ import * as THREE from 'three'; -import { Player } from '../core/Player.ts'; import { acceleratedRaycast, computeBoundsTree, @@ -9,6 +8,7 @@ import { } from 'three-mesh-bvh'; import { InputHandler } from './InputHandler.ts'; import { RemotePlayerRenderer } from '../core/RemotePlayerRenderer.ts'; +import { Player } from '../../shared/Player.ts'; THREE.Mesh.prototype.raycast = acceleratedRaycast; THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree; diff --git a/src/client/input/InputHandler.ts b/src/client/input/InputHandler.ts index f3fbba1..0028412 100644 --- a/src/client/input/InputHandler.ts +++ b/src/client/input/InputHandler.ts @@ -1,8 +1,8 @@ import * as THREE from 'three'; import { PointerLockControls } from './PointerLockControl.ts'; import { Renderer } from '../core/Renderer.ts'; -import { Player } from '../core/Player.ts'; import { SettingsManager } from '../core/SettingsManager.ts'; +import { Player } from '../../shared/Player.ts'; export class InputHandler { private readonly gameIndex: number; diff --git a/src/client/input/PointerLockControl.ts b/src/client/input/PointerLockControl.ts index b761c2b..7cba29b 100644 --- a/src/client/input/PointerLockControl.ts +++ b/src/client/input/PointerLockControl.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; -import { Player } from '../core/Player.ts'; import { SettingsManager } from '../core/SettingsManager.ts'; +import { Player } from '../../shared/Player.ts'; // Define a custom event map interface interface PointerLockControlEventMap { diff --git a/src/client/ui/ChatOverlay.ts b/src/client/ui/ChatOverlay.ts index 560c464..b820b34 100644 --- a/src/client/ui/ChatOverlay.ts +++ b/src/client/ui/ChatOverlay.ts @@ -1,10 +1,10 @@ -import { Player } from '../core/Player.ts'; import { Renderer } from '../core/Renderer.ts'; import { Networking } from '../core/Networking.ts'; import { InputHandler } from '../input/InputHandler.ts'; import { CommandManager } from '../core/CommandManager.ts'; import { SettingsManager } from '../core/SettingsManager.ts'; import { TouchInputHandler } from '../input/TouchInputHandler.ts'; +import { Player } from '../../shared/Player.ts'; interface ChatMessage { id: number; diff --git a/src/client/ui/HealthIndicator.ts b/src/client/ui/HealthIndicator.ts index ecc1885..4170647 100644 --- a/src/client/ui/HealthIndicator.ts +++ b/src/client/ui/HealthIndicator.ts @@ -2,8 +2,8 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; import * as THREE from 'three'; import { Renderer } from '../core/Renderer.ts'; -import { Player } from '../core/Player.ts'; import { Networking } from '../core/Networking.ts'; +import { Player } from '../../shared/Player.ts'; const clock = new THREE.Clock(); diff --git a/src/server/DataValidator.ts b/src/server/DataValidator.ts index 6219126..64f0c1a 100644 --- a/src/server/DataValidator.ts +++ b/src/server/DataValidator.ts @@ -1,7 +1,8 @@ +import * as THREE from 'three'; import { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts'; -import { Player } from './models/Player.ts'; import { ChatMessage } from './models/ChatMessage.ts'; import { DamageRequest } from './models/DamageRequest.ts'; +import { Player, PlayerData } from '../shared/Player.ts'; export class DataValidator { private static SERVER_VERSION = ''; @@ -17,13 +18,24 @@ export class DataValidator { return this.SERVER_VERSION; } - private static vector3Schema = z.object({ - x: z.number(), - y: z.number(), - z: z.number(), - }).strict(); + static vector3Schema = z + .object({ + x: z.number(), + y: z.number(), + z: z.number(), + }) + .transform(({ x, y, z }) => new THREE.Vector3(x, y, z)); + + static quaternionSchema = z + .object({ + x: z.number(), + y: z.number(), + z: z.number(), + w: z.number(), + }) + .transform(({ x, y, z, w }) => new THREE.Quaternion(x, y, z, w)); - private static playerDataSchema = z.object({ + static playerDataSchema = z.object({ id: z.number(), speed: z.number(), acceleration: z.number(), @@ -31,12 +43,12 @@ export class DataValidator { gameVersion: z.string().refine((val) => val === this.SERVER_VERSION, { message: `Game version must be ${this.SERVER_VERSION}`, }), - position: DataValidator.vector3Schema, - velocity: DataValidator.vector3Schema, - inputVelocity: DataValidator.vector3Schema, + position: this.vector3Schema, + velocity: this.vector3Schema, + inputVelocity: this.vector3Schema, gravity: z.number(), - lookQuaternion: z.array(z.number()).length(4), - quaternion: z.array(z.number()).length(4), + lookQuaternion: this.quaternionSchema, + quaternion: this.quaternionSchema, chatActive: z.boolean(), chatMsg: z.string().max(300), latency: z.number(), @@ -50,21 +62,21 @@ export class DataValidator { playerSpectating: z.number(), gameMsgs: z.array(z.string()), gameMsgs2: z.array(z.string()), - }).strict(); + }).strict().transform((data) => Player.fromObject(data as Player)); - private static chatMsgSchema = z.object({ + static chatMsgSchema = z.object({ id: z.number(), name: z.string().max(42), message: z.string().max(300), }).strict(); - private static damageRequestSchema = z.object({ + static damageRequestSchema = z.object({ localPlayer: DataValidator.playerDataSchema, targetPlayer: DataValidator.playerDataSchema, damage: z.number(), }).strict(); - static validatePlayerData(data: Player) { + static validatePlayerData(data: PlayerData) { return DataValidator.playerDataSchema.safeParse(data); } diff --git a/src/server/GameEngine.ts b/src/server/GameEngine.ts index 46ab37c..1cfb063 100644 --- a/src/server/GameEngine.ts +++ b/src/server/GameEngine.ts @@ -2,14 +2,14 @@ import { ChatManager } from './managers/ChatManager.ts'; import { DamageSystem } from './managers/DamageSystem.ts'; import { ItemManager } from './managers/ItemManager.ts'; import { PlayerManager } from './managers/PlayerManager.ts'; -import { Server } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; import config from './config.ts'; -import { Vector3 } from './models/Vector3.ts'; import { ServerInfo } from './models/ServerInfo.ts'; import { DataValidator } from './DataValidator.ts'; -import { Player } from './models/Player.ts'; import { Gamemode } from './gamemodes/Gamemode.ts'; import { FFAGamemode } from './gamemodes/FFAGamemode.ts'; +import { CustomServer } from '../shared/messages.ts'; +import { Player } from '../shared/Player.ts'; +import * as THREE from 'three'; export class GameEngine { private lastPlayerTickTimestamp: number = Date.now() / 1000; @@ -24,7 +24,7 @@ export class GameEngine { private itemManager: ItemManager, public chatManager: ChatManager, private damageSystem: DamageSystem, - private io: Server, + private io: CustomServer, ) {} start() { @@ -77,7 +77,7 @@ export class GameEngine { players.forEach((player) => { if (player.position.y < -150) { player.health = 0; - player.velocity = new Vector3(0, 0, 0); + player.velocity = new THREE.Vector3(0, 0, 0); this.chatManager.broadcastChat(`${player.name} fell off :'(`); console.log(`💔 ${player.name}(${player.id}) fell off the map`); } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 674639e..a57467d 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -1,5 +1,5 @@ import { Application, Router, send } from '@oak/oak'; -import { Server, Socket } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; +import { Server } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; import config from './config.ts'; import { serve } from 'https://deno.land/std@0.150.0/http/server.ts'; @@ -10,11 +10,12 @@ import { ChatManager } from './managers/ChatManager.ts'; import { DamageSystem } from './managers/DamageSystem.ts'; import { MapData } from './models/MapData.ts'; import { DataValidator } from './DataValidator.ts'; +import { CustomServer } from '../shared/messages.ts'; export class GameServer { router: Router = new Router(); app: Application = new Application(); - io: Server = new Server(); + io: CustomServer = new Server(); gameEngine: GameEngine; playerManager: PlayerManager; @@ -51,17 +52,13 @@ export class GameServer { } private setupSocketIO() { - this.io.on('connection', (socket: Socket) => { + this.io.on('connection', (socket) => { if (socket.connected) { - socket.on('error', (error) => { - console.error(`Socket error for ${socket.id}:`, error); - }); - // deno-lint-ignore require-await socket.on('playerData', async (data) => { try { const result = this.playerManager.addOrUpdatePlayer(data); - if (result.isNew && result.player) { + if (result.isNew) { if (this.gameEngine.gamemode) this.gameEngine.gamemode.onPlayerConnect(result.player); this.chatManager.broadcastChat(`${result.player.name} joined`); console.log(`🟢 ${result.player.name}(${result.player.id}) joined`); @@ -91,7 +88,7 @@ export class GameServer { socket.on('latencyTest', () => { try { - socket.emit('latencyTest', 'response :)'); + socket.emit('latencyTest'); } catch (err) { console.error(`Error handling latency test:`, err); } diff --git a/src/server/gamemodes/FFAGamemode.ts b/src/server/gamemodes/FFAGamemode.ts index aff8ff6..24c3a7d 100644 --- a/src/server/gamemodes/FFAGamemode.ts +++ b/src/server/gamemodes/FFAGamemode.ts @@ -1,7 +1,7 @@ import { GameEngine } from '../GameEngine.ts'; -import { Player } from '../models/Player.ts'; import { Gamemode } from './Gamemode.ts'; import config from '../config.ts'; +import { Player } from '../../shared/Player.ts'; export class FFAGamemode extends Gamemode { private spectateTimeouts: Map = new Map(); diff --git a/src/server/gamemodes/Gamemode.ts b/src/server/gamemodes/Gamemode.ts index 1032652..41d2aa9 100644 --- a/src/server/gamemodes/Gamemode.ts +++ b/src/server/gamemodes/Gamemode.ts @@ -1,5 +1,5 @@ import { GameEngine } from '../GameEngine.ts'; -import { Player } from '../models/Player.ts'; +import { Player } from '../../shared/Player.ts'; export abstract class Gamemode { constructor(protected gameEngine: GameEngine) {} diff --git a/src/server/managers/ChatManager.ts b/src/server/managers/ChatManager.ts index 825a80e..206ccd4 100644 --- a/src/server/managers/ChatManager.ts +++ b/src/server/managers/ChatManager.ts @@ -1,13 +1,13 @@ -import { Server, Socket } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; import { DataValidator } from '../DataValidator.ts'; import { ChatMessage } from '../models/ChatMessage.ts'; import { PlayerManager } from './PlayerManager.ts'; +import { CustomServer, CustomSocket } from '../../shared/messages.ts'; export class ChatManager { - constructor(private io: Server, private playerManager: PlayerManager) {} + constructor(private io: CustomServer, private playerManager: PlayerManager) {} - handleChatMessage(data: ChatMessage, socket: Socket) { - const { error } = DataValidator.validateChatMessage(data); + handleChatMessage(unparsedData: ChatMessage, socket: CustomSocket) { + const { data, error } = DataValidator.validateChatMessage(unparsedData); if (error) { console.warn(`Invalid chat message: ${error.message}`); return; @@ -21,7 +21,7 @@ export class ChatManager { } } - private parseCommand(message: string, socket: Socket, playerId: number): boolean { + private parseCommand(message: string, socket: CustomSocket, playerId: number): boolean { if (message.charAt(0) !== '/') return false; const args = message.slice(1).split(' '); @@ -76,7 +76,7 @@ export class ChatManager { this.io.emit('chatMsg', chatMessage); } - whisperChatMessage(message: string, socket: Socket) { + whisperChatMessage(message: string, socket: CustomSocket) { const chatMessage: ChatMessage = { id: -1, name: '', diff --git a/src/server/managers/DamageSystem.ts b/src/server/managers/DamageSystem.ts index 4f77d0b..bbb9f31 100644 --- a/src/server/managers/DamageSystem.ts +++ b/src/server/managers/DamageSystem.ts @@ -2,23 +2,24 @@ import { PlayerManager } from './PlayerManager.ts'; import { ChatManager } from './ChatManager.ts'; import { DamageRequest } from '../models/DamageRequest.ts'; import { DataValidator } from '../DataValidator.ts'; -import { Vector3 } from '../models/Vector3.ts'; import { GameEngine } from '../GameEngine.ts'; export class DamageSystem { + private gameEngine!: GameEngine; + constructor( private playerManager: PlayerManager, private chatManager: ChatManager, ) {} - private gameEngine!: GameEngine; + public setGameEngine(gameEngine: GameEngine) { this.gameEngine = gameEngine; } - handleDamageRequest(data: DamageRequest) { - const validationResult = DataValidator.validateDamageRequest(data); - if (!validationResult.success) { - console.warn(`Invalid damage request: ${validationResult.error?.message}`); + handleDamageRequest(unparsedData: DamageRequest) { + const { data, error } = DataValidator.validateDamageRequest(unparsedData); + if (error) { + console.warn(`Invalid damage request: ${error.message}`); return; } @@ -33,11 +34,11 @@ export class DamageSystem { // Validate positions const localPlayerSentPosition = data.localPlayer.position; const localPlayerServerPosition = localPlayer.position; - const localDistance = Vector3.distanceTo(localPlayerSentPosition, localPlayerServerPosition); + const localDistance = localPlayerSentPosition.distanceTo(localPlayerServerPosition); const targetPlayerSentPosition = data.targetPlayer.position; const targetPlayerServerPosition = targetPlayer.position; - const targetDistance = Vector3.distanceTo(targetPlayerSentPosition, targetPlayerServerPosition); + const targetDistance = targetPlayerSentPosition.distanceTo(targetPlayerServerPosition); const MAX_DESYNC_DISTANCE = 1; // Threshold for considering positions in sync diff --git a/src/server/managers/ItemManager.ts b/src/server/managers/ItemManager.ts index 78fcd2a..7ae5c3a 100644 --- a/src/server/managers/ItemManager.ts +++ b/src/server/managers/ItemManager.ts @@ -1,10 +1,10 @@ import { WorldItem } from '../models/WorldItem.ts'; import { MapData } from '../models/MapData.ts'; -import { Vector3 } from '../models/Vector3.ts'; import config from '../config.ts'; import { PlayerManager } from './PlayerManager.ts'; import { ChatManager } from './ChatManager.ts'; import { Gamemode } from '../gamemodes/Gamemode.ts'; +import * as THREE from 'three'; export class ItemManager { private worldItems: WorldItem[] = []; @@ -33,7 +33,7 @@ export class ItemManager { const randomIndex = Math.floor(Math.random() * this.mapData.itemRespawnPoints.length); const respawnPoint = this.mapData.itemRespawnPoints[randomIndex]; const newItem = new WorldItem( - new Vector3(respawnPoint.position.x, respawnPoint.position.y, respawnPoint.position.z), + new THREE.Vector3(respawnPoint.position.x, respawnPoint.position.y, respawnPoint.position.z), respawnPoint.itemId, ); @@ -55,7 +55,7 @@ export class ItemManager { for (const player of players) { if (player.playerSpectating !== -1) continue; if (player.health <= 0) continue; - const itemIndex = this.worldItems.findIndex((item) => Vector3.distanceTo(player.position, item.vector) < 0.5); + const itemIndex = this.worldItems.findIndex((item) => player.position.distanceTo(item.vector) < 0.5); if (itemIndex === -1) continue; @@ -95,8 +95,8 @@ export class ItemManager { } } - isItemCloseToPoint(vector: Vector3, distance: number): boolean { - return this.worldItems.some((item) => Vector3.distanceTo(item.vector, vector) < distance); + isItemCloseToPoint(vector: THREE.Vector3, distance: number): boolean { + return this.worldItems.some((item) => item.vector.distanceTo(vector) < distance); } getAllItems(): WorldItem[] { diff --git a/src/server/managers/PlayerManager.ts b/src/server/managers/PlayerManager.ts index 86b4fa3..3a1ad01 100644 --- a/src/server/managers/PlayerManager.ts +++ b/src/server/managers/PlayerManager.ts @@ -1,20 +1,19 @@ -import { Player } from '../models/Player.ts'; -import { Vector3 } from '../models/Vector3.ts'; -import { Quaternion } from '../models/Quaternion.ts'; +import { Player, PlayerData } from '../../shared/Player.ts'; import { DataValidator } from '../DataValidator.ts'; import { MapData } from '../models/MapData.ts'; import config from '../config.ts'; import { WorldItem } from '../models/WorldItem.ts'; import { ItemManager } from './ItemManager.ts'; import { PlayerExtras } from '../models/PlayerExtras.ts'; +import * as THREE from 'three'; -interface PlayerData { +interface PlayerWithExtras { player: Player; extras: PlayerExtras; } export class PlayerManager { - private players: Map = new Map(); + private players: Map = new Map(); private mapData: MapData; private itemManager!: ItemManager; @@ -26,67 +25,69 @@ export class PlayerManager { this.itemManager = itemManager; } - addOrUpdatePlayer(data: Player): { isNew: boolean; player?: Player } { - const { error } = DataValidator.validatePlayerData(data); + addOrUpdatePlayer(unparsedData: PlayerData): { isNew: false } | { isNew: true; player: Player } { + const { data: player, error } = DataValidator.validatePlayerData(unparsedData); if (error) { throw new Error(`⚠️ invalid player data `); } - const existingPlayerData = this.players.get(data.id); - if (data.name.length < 1) data.name = 'possum' + data.id.toString().substring(0, 3); - if (data.chatMsg.startsWith('/admin ')) data.chatMsg = '/admin ' + data.chatMsg.substring(7).replace(/./g, '*'); - if (data.chatMsg.startsWith('>')) data.chatMsg = '&2' + data.chatMsg; - if (!data.chatMsg.startsWith('&f')) data.chatMsg = '&f' + data.chatMsg; + const existingPlayerData = this.players.get(player.id); + if (player.name.length < 1) player.name = 'possum' + player.id.toString().substring(0, 3); + if (player.chatMsg.startsWith('/admin ')) { + player.chatMsg = '/admin ' + player.chatMsg.substring(7).replace(/./g, '*'); + } + if (player.chatMsg.startsWith('>')) player.chatMsg = '&2' + player.chatMsg; + if (!player.chatMsg.startsWith('&f')) player.chatMsg = '&f' + player.chatMsg; if (existingPlayerData) { // Handle forced acknowledgment - if (existingPlayerData.player.forced && !data.forcedAcknowledged) { + if (existingPlayerData.player.forced && !player.forcedAcknowledged) { return { isNew: false }; } - if (existingPlayerData.player.forced && data.forcedAcknowledged) { + if (existingPlayerData.player.forced && player.forcedAcknowledged) { existingPlayerData.player.forced = false; } // Update existing player, preserving certain fields - data.health = existingPlayerData.player.health; - data.inventory = existingPlayerData.player.inventory; - data.lastDamageTime = existingPlayerData.player.lastDamageTime; - data.gameMsgs = existingPlayerData.player.gameMsgs; - data.gameMsgs2 = existingPlayerData.player.gameMsgs2; - data.playerSpectating = existingPlayerData.player.playerSpectating; - data.updateTimestamp = Date.now() / 1000; - - const updatedData: PlayerData = { - player: data, + player.health = existingPlayerData.player.health; + player.inventory = existingPlayerData.player.inventory; + player.lastDamageTime = existingPlayerData.player.lastDamageTime; + player.gameMsgs = existingPlayerData.player.gameMsgs; + player.gameMsgs2 = existingPlayerData.player.gameMsgs2; + player.playerSpectating = existingPlayerData.player.playerSpectating; + player.updateTimestamp = Date.now() / 1000; + + const updatedData: PlayerWithExtras = { + player: player, extras: existingPlayerData.extras, }; - this.players.set(data.id, updatedData); + this.players.set(player.id, updatedData); return { isNew: false }; } else { // New player - data.inventory = [...config.player.baseInventory]; + player.inventory = [...config.player.baseInventory]; const spawnPoint = this.getRandomSpawnPoint(); - data.position = spawnPoint.vec; - data.health = config.player.maxHealth; - data.gameMsgs = []; - data.gameMsgs2 = []; - data.playerSpectating = -1; - data.lookQuaternion = [ + player.position = spawnPoint.vec; + player.health = config.player.maxHealth; + player.gameMsgs = []; + player.gameMsgs2 = []; + player.playerSpectating = -1; + player.lookQuaternion = new THREE.Quaternion( spawnPoint.quaternion.x, spawnPoint.quaternion.y, spawnPoint.quaternion.z, spawnPoint.quaternion.w, - ]; - data.forced = true; + ); + player.forced = true; - const newPlayerData: PlayerData = { - player: data, + const newPlayerData: PlayerWithExtras = { + player: player, extras: new PlayerExtras(), }; - this.players.set(data.id, newPlayerData); + this.players.set(player.id, newPlayerData); this.itemManager.triggerUpdateFlag(); - return { isNew: true, player: data }; + return { isNew: true, player: player }; } } @@ -95,7 +96,7 @@ export class PlayerManager { } getAllPlayers(): Player[] { - return Array.from(this.players.values()).map((playerData) => playerData.player); + return Array.from(this.players.values().map(({ player }) => player)); } getPlayerById(playerId: number): Player | undefined { @@ -103,7 +104,7 @@ export class PlayerManager { return playerData?.player; } - getPlayerDataById(playerId: number): PlayerData | undefined { + getPlayerDataById(playerId: number): PlayerWithExtras | undefined { return this.players.get(playerId); } @@ -112,7 +113,7 @@ export class PlayerManager { return playerData?.extras; } - getAllPlayerData(): PlayerData[] { + getAllPlayerData(): PlayerWithExtras[] { return Array.from(this.players.values()); } @@ -129,18 +130,18 @@ export class PlayerManager { const spawnPoint = this.getRandomSpawnPoint(); player.position = spawnPoint.vec; - player.lookQuaternion = [ + player.lookQuaternion = new THREE.Quaternion( spawnPoint.quaternion.x, spawnPoint.quaternion.y, spawnPoint.quaternion.z, spawnPoint.quaternion.w, - ]; + ); player.health = config.player.maxHealth; player.gravity = 0; - player.velocity = new Vector3(0, 0, 0); + player.velocity = new THREE.Vector3(0, 0, 0); player.forced = true; - const updatedPlayerData: PlayerData = { + const updatedPlayerData: PlayerWithExtras = { player: player, extras: playerData.extras, }; @@ -159,9 +160,9 @@ export class PlayerManager { } } - private getRandomSpawnPoint(): { vec: Vector3; quaternion: Quaternion } { + private getRandomSpawnPoint(): { vec: THREE.Vector3; quaternion: THREE.Quaternion } { if (!this.mapData) { - return { vec: new Vector3(2, 1, 0), quaternion: new Quaternion(0, 0, 0, 1) }; + return { vec: new THREE.Vector3(2, 1, 0), quaternion: new THREE.Quaternion(0, 0, 0, 1) }; } const randomIndex = Math.floor(Math.random() * this.mapData.respawnPoints.length); diff --git a/src/server/models/DamageRequest.ts b/src/server/models/DamageRequest.ts index 421b991..d937107 100644 --- a/src/server/models/DamageRequest.ts +++ b/src/server/models/DamageRequest.ts @@ -1,7 +1,7 @@ -import { Player } from './Player.ts'; +import { PlayerData } from '../../shared/Player.ts'; export interface DamageRequest { - localPlayer: Player; - targetPlayer: Player; + localPlayer: PlayerData; + targetPlayer: PlayerData; damage: number; } diff --git a/src/server/models/ItemRespawnPoint.ts b/src/server/models/ItemRespawnPoint.ts index 811fc3c..24a39c0 100644 --- a/src/server/models/ItemRespawnPoint.ts +++ b/src/server/models/ItemRespawnPoint.ts @@ -1,8 +1,8 @@ -import { Vector3 } from './Vector3.ts'; +import * as THREE from 'three'; export class ItemRespawnPoint { constructor( - public position: Vector3, + public position: THREE.Vector3, public itemId: number, public spawnChancePerTick: number, ) {} diff --git a/src/server/models/MapData.ts b/src/server/models/MapData.ts index ed2b665..b883730 100644 --- a/src/server/models/MapData.ts +++ b/src/server/models/MapData.ts @@ -1,7 +1,6 @@ import { RespawnPoint } from './RespawnPoint.ts'; import { ItemRespawnPoint } from './ItemRespawnPoint.ts'; -import { Vector3 } from './Vector3.ts'; -import { Quaternion } from './Quaternion.ts'; +import * as THREE from 'three'; export class MapData { constructor( @@ -16,15 +15,15 @@ export class MapData { const respawnPoints = json.respawnPoints.map( (rp) => new RespawnPoint( - new Vector3(rp.position.x, rp.position.y, rp.position.z), - new Quaternion(rp.quaternion.x, rp.quaternion.y, rp.quaternion.z, rp.quaternion.w), + new THREE.Vector3(rp.position.x, rp.position.y, rp.position.z), + new THREE.Quaternion(rp.quaternion.x, rp.quaternion.y, rp.quaternion.z, rp.quaternion.w), ), ); const itemRespawnPoints = json.itemRespawnPoints.map( (irp) => new ItemRespawnPoint( - new Vector3(irp.position.x, irp.position.y, irp.position.z), + new THREE.Vector3(irp.position.x, irp.position.y, irp.position.z), irp.itemId, irp.spawnChancePerTick, ), diff --git a/src/server/models/Player.ts b/src/server/models/Player.ts deleted file mode 100644 index de4ab1f..0000000 --- a/src/server/models/Player.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Vector3 } from './Vector3.ts'; - -export interface Player { - id: number; - speed: number; - acceleration: number; - name: string; - gameVersion: string; - position: Vector3; - velocity: Vector3; - inputVelocity: Vector3; - gravity: number; - lookQuaternion: number[]; - quaternion: number[]; - chatActive: boolean; - chatMsg: string; - latency: number; - health: number; - forced: boolean; - forcedAcknowledged: boolean; - updateTimestamp?: number; - lastDamageTime?: number; - inventory: number[]; - idLastDamagedBy?: number; - playerSpectating: number; - gameMsgs: string[]; - gameMsgs2: string[]; -} diff --git a/src/server/models/Quaternion.ts b/src/server/models/Quaternion.ts deleted file mode 100644 index dd75516..0000000 --- a/src/server/models/Quaternion.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class Quaternion { - constructor(public x: number, public y: number, public z: number, public w: number) {} -} diff --git a/src/server/models/RespawnPoint.ts b/src/server/models/RespawnPoint.ts index c0ad5ea..e93a38c 100644 --- a/src/server/models/RespawnPoint.ts +++ b/src/server/models/RespawnPoint.ts @@ -1,6 +1,5 @@ -import { Vector3 } from './Vector3.ts'; -import { Quaternion } from './Quaternion.ts'; +import * as THREE from 'three'; export class RespawnPoint { - constructor(public position: Vector3, public quaternion: Quaternion) {} + constructor(public position: THREE.Vector3, public quaternion: THREE.Quaternion) {} } diff --git a/src/server/models/Vector3.ts b/src/server/models/Vector3.ts deleted file mode 100644 index 253af65..0000000 --- a/src/server/models/Vector3.ts +++ /dev/null @@ -1,18 +0,0 @@ -export class Vector3 { - constructor(public x: number, public y: number, public z: number) {} - public distanceTo(other: Vector3): number { - return Math.sqrt( - Math.pow(this.x - other.x, 2) + - Math.pow(this.y - other.y, 2) + - Math.pow(this.z - other.z, 2), - ); - } - - public static distanceTo(v1: Vector3, v2: Vector3): number { - return Math.sqrt( - Math.pow(v1.x - v2.x, 2) + - Math.pow(v1.y - v2.y, 2) + - Math.pow(v1.z - v2.z, 2), - ); - } -} diff --git a/src/server/models/WorldItem.ts b/src/server/models/WorldItem.ts index 43c3bf3..6903958 100644 --- a/src/server/models/WorldItem.ts +++ b/src/server/models/WorldItem.ts @@ -1,9 +1,9 @@ -import { Vector3 } from './Vector3.ts'; +import * as THREE from 'three'; export class WorldItem { public id: number; - constructor(public vector: Vector3, public itemType: number) { + constructor(public vector: THREE.Vector3, public itemType: number) { this.id = Math.floor(Math.random() * 100000) + 1; } } diff --git a/src/shared/Player.ts b/src/shared/Player.ts new file mode 100644 index 0000000..a3cc79f --- /dev/null +++ b/src/shared/Player.ts @@ -0,0 +1,53 @@ +import type { z } from 'https://deno.land/x/zod@v3.23.8/mod.ts'; +import { DataValidator } from '../server/DataValidator.ts'; +import * as THREE from 'three'; + +export type PlayerData = z.input; + +export class Player { + public position = new THREE.Vector3(0, 100, 0); + public velocity = new THREE.Vector3(0, 0, 0); + public inputVelocity = new THREE.Vector3(); + public gravity = 0; + public lookQuaternion = new THREE.Quaternion(); + public quaternion = new THREE.Quaternion(); + public id = Math.floor(Math.random() * 1000000000); + public gameVersion = ''; + public name = ''; + public speed = 5; + public acceleration = 100; + public chatActive = false; + public chatMsg = ''; + public latency = 1000; + public health = 100; + public forced = false; + public forcedAcknowledged = false; + public inventory: number[] = []; + public idLastDamagedBy?: number = -1; + public playerSpectating = -1; + public gameMsgs: string[] = []; + public gameMsgs2: string[] = []; + public updateTimestamp?: number; + public lastDamageTime?: number; + + static fromObject(data: Player): Player { + const instance = new Player(); + Object.assign(instance, data); + return instance; + } + + // Gets called by Socket.IO during JSON.stringify, this will match our zod schema (PlayerData) + toJSON(): PlayerData { + const serializableVec3 = ({ x, y, z }: THREE.Vector3) => ({ x, y, z }); + const serializableQuaternion = ({ x, y, z, w }: THREE.Quaternion) => ({ x, y, z, w }); + + return { + ...this, + position: serializableVec3(this.position), + velocity: serializableVec3(this.velocity), + inputVelocity: serializableVec3(this.inputVelocity), + lookQuaternion: serializableQuaternion(this.lookQuaternion), + quaternion: serializableQuaternion(this.quaternion), + }; + } +} diff --git a/src/shared/messages.ts b/src/shared/messages.ts new file mode 100644 index 0000000..5f022e2 --- /dev/null +++ b/src/shared/messages.ts @@ -0,0 +1,30 @@ +import type { Server, Socket } from 'https://deno.land/x/socket_io@0.2.0/mod.ts'; +import type { Socket as ClientSocket } from 'socket.io-client'; +import type { ChatMessage } from '../server/models/ChatMessage.ts'; +import type { ServerInfo } from '../server/models/ServerInfo.ts'; +import type { WorldItem } from '../server/models/WorldItem.ts'; +import type { DamageRequest } from '../server/models/DamageRequest.ts'; +import type { PlayerData } from './Player.ts'; + +export type CustomServer = Server; +export type CustomSocket = Socket; + +export type CustomClientSocket = ClientSocket< + ServerToClientEvents, + ClientToServerEvents +>; + +interface ServerToClientEvents { + serverInfo: (info: ServerInfo) => void; + chatMsg: (message: ChatMessage) => void; + remotePlayerData: (players: PlayerData[]) => void; + worldItemData: (items: WorldItem[]) => void; + latencyTest: () => void; +} + +interface ClientToServerEvents { + playerData: (player: PlayerData) => void; + chatMsg: (message: ChatMessage) => void; + applyDamage: (damage: DamageRequest) => void; + latencyTest: () => void; +}