From 1f0daf82cb57155586bc11029f80552d1a1bcea4 Mon Sep 17 00:00:00 2001 From: RuanFernandes Date: Sun, 29 Jun 2025 10:55:10 -0300 Subject: [PATCH] . --- .gitignore | 9 + .prettierrc | 4 + package.json | 30 ++++ public/index.html | 28 +++ public/js/main.js | 344 ++++++++++++++++++++++++++++++++++++ src/callbacks/chat.ts | 25 +++ src/callbacks/connect.ts | 48 +++++ src/callbacks/disconnect.ts | 32 ++++ src/callbacks/move.ts | 36 ++++ src/classes/player.ts | 6 + src/protocol/MessageType.ts | 16 ++ src/rooms/GameRoom.ts | 13 ++ src/rooms/WorldRoom.ts | 71 ++++++++ src/server.ts | 50 ++++++ tsconfig.json | 18 ++ 15 files changed, 730 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 package.json create mode 100644 public/index.html create mode 100644 public/js/main.js create mode 100644 src/callbacks/chat.ts create mode 100644 src/callbacks/connect.ts create mode 100644 src/callbacks/disconnect.ts create mode 100644 src/callbacks/move.ts create mode 100644 src/classes/player.ts create mode 100644 src/protocol/MessageType.ts create mode 100644 src/rooms/GameRoom.ts create mode 100644 src/rooms/WorldRoom.ts create mode 100644 src/server.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71a88ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +# Keep environment variables out of version control +.env + +# Ignore build output +package-lock.json + +# Ignore logs +logs diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9b0bb43 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "singleQuote": true +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b5c813 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "graalclone", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "node dist/server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "@colyseus/schema": "^3.0.42", + "colyseus": "^0.16.4", + "cors": "^2.8.5", + "dotenv": "^17.0.0", + "express": "^5.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/node": "^24.0.7", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..338fbfc --- /dev/null +++ b/public/index.html @@ -0,0 +1,28 @@ + + + + + + + FreneticalPVP + + + + + + + + + + \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..c71b763 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,344 @@ +const client = new Colyseus.Client('ws://localhost:3000'); +let room; +let player; +const otherPlayers = new Map(); +let scene; + +const config = { + type: Phaser.AUTO, + width: window.innerWidth, + height: window.innerHeight, + backgroundColor: '#1a1a1a', + physics: { + default: 'arcade', + arcade: { debug: false }, + }, + scale: { + mode: Phaser.Scale.RESIZE, + autoCenter: Phaser.Scale.CENTER_BOTH, + }, + scene: { + preload, + create, + update, + }, +}; + +const game = new Phaser.Game(config); + +const MessageType = { + SERVER_CONNECT: 1, + SERVER_DISCONNECT: 2, + SERVER_MOVE: 3, + SERVER_CHAT: 4, + + CLIENT_WELCOME: 1001, + CLIENT_PLAYER_JOINED: 1002, + CLIENT_PLAYER_LEFT: 1003, + CLIENT_UPDATE_PLAYER: 1004, + CLIENT_CHAT_MESSAGE: 1005, + CLIENT_ERROR: 1404, + CLIENT_PLAYERS_LIST: 1006, // Novo tipo para lista de jogadores +}; + +// Variáveis para movimento fluido +const keys = {}; +let lastMovementTime = 0; +const MOVEMENT_INTERVAL = 16; // ~60fps +const SPEED = 500; // pixels por segundo + +function preload() { + console.log('🎮 Preload phase started'); +} + +function create() { + scene = this; + console.log('🎯 Create phase started'); + + // Criar o player visual + player = createPlayerObject(this, 400, 300, 0x00ff00, 'Você'); + + // Configurar input contínuo + setupContinuousInput(this); + + // Conectar ao servidor + console.log('🔗 Tentando conectar ao servidor...'); + + client + .joinOrCreate('world', { nickname: prompt('Nickname:') }) + .then((r) => { + room = r; + console.log('✅ Conectado com sucesso:', room.sessionId); + + // Configurar handlers de mensagem + setupMessageHandlers(scene); + }) + .catch((error) => { + console.error('❌ Erro ao conectar:', error); + }); +} + +function createPlayerObject(scene, x, y, color, nickname) { + const playerContainer = scene.add.container(x, y); + + // Corpo do jogador + const body = scene.add.rectangle(0, 0, 32, 32, color); + + // Nome do jogador + const nameText = scene.add + .text(0, -25, nickname, { + fontSize: '12px', + fill: '#ffffff', + stroke: '#000000', + strokeThickness: 2, + }) + .setOrigin(0.5); + + // Texto de chat (inicialmente oculto) + const chatText = scene.add + .text(0, -50, '', { + fontSize: '10px', + fill: '#ffffff', + backgroundColor: '#000000aa', + padding: { x: 4, y: 2 }, + }) + .setOrigin(0.5) + .setVisible(false); + + playerContainer.add([body, nameText, chatText]); + + // Adicionar propriedades customizadas + playerContainer.body = body; + playerContainer.nameText = nameText; + playerContainer.chatText = chatText; + playerContainer.targetX = x; + playerContainer.targetY = y; + + return playerContainer; +} + +function setupContinuousInput(scene) { + // Configurar teclas + const cursors = scene.input.keyboard.createCursorKeys(); + const wasd = scene.input.keyboard.addKeys('W,S,A,D'); + + // Armazenar referências das teclas + keys.up = cursors.up; + keys.down = cursors.down; + keys.left = cursors.left; + keys.right = cursors.right; + keys.w = wasd.W; + keys.s = wasd.S; + keys.a = wasd.A; + keys.d = wasd.D; + + // Configurar tecla Enter para chat + scene.input.keyboard.on('keydown-ENTER', () => { + if (!room) return; + + const msg = prompt('Mensagem:'); + if (msg && msg.trim()) { + console.log('💬 Enviando mensagem:', msg); + room.send(MessageType.SERVER_CHAT, { message: msg.trim() }); + } + }); +} + +function setupMessageHandlers(scene) { + // Welcome message + room.onMessage(MessageType.CLIENT_WELCOME, (data) => { + console.log('👋 Welcome:', data.message); + }); + + // Lista de jogadores online (novo) + room.onMessage(MessageType.CLIENT_PLAYERS_LIST, (players) => { + console.log('📋 Players list:', players); + players.forEach((playerData) => { + if (playerData.sessionId !== room.sessionId) { + createOtherPlayer(playerData); + } + }); + }); + + // Player joined + room.onMessage( + MessageType.CLIENT_PLAYER_JOINED, + ({ sessionId, options }) => { + console.log('🧍 Player joined:', sessionId, options); + if (sessionId === room.sessionId) return; + + const newPlayer = createPlayerObject( + scene, + 400, + 300, + 0xff0000, + options?.nickname || 'Player' + ); + otherPlayers.set(sessionId, newPlayer); + } + ); + + // Player left + room.onMessage(MessageType.CLIENT_PLAYER_LEFT, ({ sessionId }) => { + console.log('👋 Player left:', sessionId); + const playerObj = otherPlayers.get(sessionId); + if (playerObj) { + playerObj.destroy(); + otherPlayers.delete(sessionId); + } + }); + + // Player update - com interpolação suave + room.onMessage( + MessageType.CLIENT_UPDATE_PLAYER, + ({ sessionId, position }) => { + if (sessionId === room.sessionId) { + return; // Não atualizar nosso próprio player + } + + const playerObj = otherPlayers.get(sessionId); + if (playerObj) { + // Definir posição alvo para interpolação suave + playerObj.targetX = position.x; + playerObj.targetY = position.y; + } + } + ); + + // Chat message - exibir acima da cabeça + room.onMessage( + MessageType.CLIENT_CHAT_MESSAGE, + ({ sessionId, message }) => { + console.log(`💬 [${sessionId}]:`, message); + + let playerObj; + if (sessionId === room.sessionId) { + playerObj = player; + } else { + playerObj = otherPlayers.get(sessionId); + } + + if (playerObj && playerObj.chatText) { + showChatMessage(playerObj, message); + } + } + ); + + // Error message + room.onMessage(MessageType.CLIENT_ERROR, ({ message }) => { + console.error('❌ Erro do servidor:', message); + }); + + // Connection events + room.onStateChange((state) => { + console.log('🔄 State changed:', state); + }); + + room.onError((code, message) => { + console.error('❌ Room error:', code, message); + }); + + room.onLeave((code) => { + console.log('👋 Left room with code:', code); + }); +} + +function createOtherPlayer(playerData) { + const newPlayer = createPlayerObject( + scene, + playerData.position.x, + playerData.position.y, + 0xff0000, + playerData.nickname || 'Player' + ); + otherPlayers.set(playerData.sessionId, newPlayer); +} + +function showChatMessage(playerObj, message) { + if (!playerObj.chatText) return; + + playerObj.chatText.setText(message); + playerObj.chatText.setVisible(true); + + // Ocultar mensagem após 3 segundos + setTimeout(() => { + if (playerObj.chatText) { + playerObj.chatText.setVisible(false); + } + }, 3000); +} + +function update(time, delta) { + // Movimento fluido baseado em input contínuo + if (room && player) { + handleContinuousMovement(delta); + } + + // Interpolação suave para outros jogadores + interpolateOtherPlayers(delta); +} + +function handleContinuousMovement(delta) { + const currentTime = Date.now(); + + // Verificar se é hora de enviar movimento + if (currentTime - lastMovementTime < MOVEMENT_INTERVAL) { + return; + } + + let isMoving = false; + const movement = { x: 0, y: 0 }; + const speed = (SPEED * delta) / 1000; // pixels por frame + + // Verificar input + if (keys.up.isDown || keys.w.isDown) { + movement.y = -speed; + isMoving = true; + } + if (keys.down.isDown || keys.s.isDown) { + movement.y = speed; + isMoving = true; + } + if (keys.left.isDown || keys.a.isDown) { + movement.x = -speed; + isMoving = true; + } + if (keys.right.isDown || keys.d.isDown) { + movement.x = speed; + isMoving = true; + } + + if (isMoving) { + // Atualizar posição local imediatamente + player.x += movement.x; + player.y += movement.y; + + // Enviar para o servidor + room.send(MessageType.SERVER_MOVE, { x: player.x, y: player.y }); + + lastMovementTime = currentTime; + } +} + +function interpolateOtherPlayers(delta) { + const lerpSpeed = 0.1; // Velocidade de interpolação (0-1) + + otherPlayers.forEach((playerObj) => { + if ( + playerObj.targetX !== undefined && + playerObj.targetY !== undefined + ) { + // Interpolação linear suave + const deltaX = playerObj.targetX - playerObj.x; + const deltaY = playerObj.targetY - playerObj.y; + + if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { + playerObj.x += deltaX * lerpSpeed; + playerObj.y += deltaY * lerpSpeed; + } else { + playerObj.x = playerObj.targetX; + playerObj.y = playerObj.targetY; + } + } + }); +} diff --git a/src/callbacks/chat.ts b/src/callbacks/chat.ts new file mode 100644 index 0000000..773a34b --- /dev/null +++ b/src/callbacks/chat.ts @@ -0,0 +1,25 @@ +import { Client } from 'colyseus'; +import { MessageType } from '../protocol/MessageType'; +import { GameRoom } from '../rooms/GameRoom'; + +export default function chatCallback( + client: Client, + options: { message: string }, + room: GameRoom +) { + if (!room || !room.getRoomPlayersKeys().includes(client.sessionId)) { + client.send(MessageType.CLIENT_ERROR, { + message: 'You are not connected to this room.', + }); + console.warn( + `Client ${client.sessionId} tried to chat but is not connected to the room.` + ); + return; + } + + // Enviar mensagem para todos os jogadores, incluindo o remetente + room.broadcast(MessageType.CLIENT_CHAT_MESSAGE, { + sessionId: client.sessionId, + message: options.message, + }); +} diff --git a/src/callbacks/connect.ts b/src/callbacks/connect.ts new file mode 100644 index 0000000..f81d94e --- /dev/null +++ b/src/callbacks/connect.ts @@ -0,0 +1,48 @@ +import { Client } from 'colyseus'; +import { MessageType } from '../protocol/MessageType'; +import { GameRoom } from '../rooms/GameRoom'; +import { Player } from '../classes/player'; + +export default function connectCallback( + client: Client, + options: any, + room: GameRoom +) { + console.log( + 'Client connected:', + client.sessionId, + 'with options:', + options + ); + + const player: Player = { + nickname: + options.nickname || 'Player ' + Math.floor(Math.random() * 10000), + sessionId: client.sessionId, + position: { x: 0, y: 0 }, + health: 100, + }; + + room.addPlayer(client.sessionId, player); + + client.send(MessageType.CLIENT_WELCOME, { + message: 'Welcome to the game!', + }); + + client.send( + MessageType.CLIENT_PLAYER_LIST, + room + .getAllPlayers() + .filter((p) => p.sessionId !== client.sessionId) + .map((p) => ({ + sessionId: p.sessionId, + nickname: p.nickname, + position: p.position, + })) + ); + + room.broadcast(MessageType.CLIENT_PLAYER_JOINED, { + sessionId: client.sessionId, + options: options, + }); +} diff --git a/src/callbacks/disconnect.ts b/src/callbacks/disconnect.ts new file mode 100644 index 0000000..c4f7e84 --- /dev/null +++ b/src/callbacks/disconnect.ts @@ -0,0 +1,32 @@ +import { Client } from 'colyseus'; +import { MessageType } from '../protocol/MessageType'; +import { GameRoom } from '../rooms/GameRoom'; + +export default function disconnectCallback( + client: Client, + consented: boolean, + room: GameRoom +) { + console.log( + 'Client disconnected:', + client.sessionId, + 'consented:', + consented + ); + + room.broadcast(MessageType.CLIENT_PLAYER_LEFT, { + sessionId: client.sessionId, + }); + + room.deletePlayer(client.sessionId); + + if (consented) { + client.send(MessageType.CLIENT_WELCOME, { + message: 'You have successfully disconnected.', + }); + } else { + client.send(MessageType.CLIENT_ERROR, { + message: 'You were disconnected unexpectedly.', + }); + } +} diff --git a/src/callbacks/move.ts b/src/callbacks/move.ts new file mode 100644 index 0000000..09df340 --- /dev/null +++ b/src/callbacks/move.ts @@ -0,0 +1,36 @@ +import { Client } from 'colyseus'; +import { MessageType } from '../protocol/MessageType'; +import { GameRoom } from '../rooms/GameRoom'; + +export default function moveCallback( + client: Client, + options: { x: number; y: number }, + room: GameRoom +) { + if (!room || !room.getRoomPlayersKeys().includes(client.sessionId)) { + client.send(MessageType.CLIENT_ERROR, { + message: 'You are not connected to this room.', + }); + console.warn( + `Client ${client.sessionId} tried to move but is not connected to the room.` + ); + return; + } + + room.movePlayer(client.sessionId, { + x: options.x, + y: options.y, + }); + + room.broadcast( + MessageType.CLIENT_UPDATE_PLAYER, + { + sessionId: client.sessionId, + position: { + x: options.x, + y: options.y, + }, + }, + { except: client } + ); +} diff --git a/src/classes/player.ts b/src/classes/player.ts new file mode 100644 index 0000000..be97ac9 --- /dev/null +++ b/src/classes/player.ts @@ -0,0 +1,6 @@ +export class Player { + nickname: string = 'Player ' + Math.floor(Math.random() * 10000); + sessionId: string = ''; + position: { x: number; y: number } = { x: 0, y: 0 }; + health: number = 100; +} diff --git a/src/protocol/MessageType.ts b/src/protocol/MessageType.ts new file mode 100644 index 0000000..8fbad1e --- /dev/null +++ b/src/protocol/MessageType.ts @@ -0,0 +1,16 @@ +export enum MessageType { + // Client to Server Messages + SERVER_CONNECT = 1, + SERVER_DISCONNECT = 2, + SERVER_MOVE = 3, + SERVER_CHAT = 4, + + // Server to Client Messages + CLIENT_WELCOME = 1001, + CLIENT_PLAYER_JOINED = 1002, + CLIENT_PLAYER_LEFT = 1003, + CLIENT_UPDATE_PLAYER = 1004, + CLIENT_CHAT_MESSAGE = 1005, + CLIENT_PLAYER_LIST = 1006, + CLIENT_ERROR = 1404, +} diff --git a/src/rooms/GameRoom.ts b/src/rooms/GameRoom.ts new file mode 100644 index 0000000..4201c9a --- /dev/null +++ b/src/rooms/GameRoom.ts @@ -0,0 +1,13 @@ +import { Room } from 'colyseus'; +import { Player } from '../classes/player'; + +export abstract class GameRoom extends Room { + abstract getRoomPlayersKeys(): string[]; + abstract movePlayer( + sessionId: string, + { x, y }: { x: number; y: number } + ): void; + abstract deletePlayer(sessionId: string): void; + abstract addPlayer(sessionId: string, playerData: Player): void; + abstract getAllPlayers(): Player[]; +} diff --git a/src/rooms/WorldRoom.ts b/src/rooms/WorldRoom.ts new file mode 100644 index 0000000..a7fccc3 --- /dev/null +++ b/src/rooms/WorldRoom.ts @@ -0,0 +1,71 @@ +import { Client } from 'colyseus'; +import { MessageType } from '../protocol/MessageType'; +import connectCallback from '../callbacks/connect'; +import disconnectCallback from '../callbacks/disconnect'; +import moveCallback from '../callbacks/move'; +import { GameRoom } from './GameRoom'; +import { Player } from '../classes/player'; +import chatCallback from '../callbacks/chat'; + +export class WorldRoom extends GameRoom { + maxClients = 32; + + players: Map = new Map(); + + onCreate(options: any) { + console.log('WorldRoom created with options:', options); + + this.setMessageHandlers(); + } + + onJoin(client: Client, options: any) { + connectCallback(client, options, this); + } + + onLeave(client: Client, consented: boolean) { + disconnectCallback(client, consented, this); + } + + onDispose() { + console.log('WorldRoom disposed'); + this.players.clear(); + } + + private setMessageHandlers() { + this.onMessage( + MessageType.SERVER_MOVE, + (client: Client, options: { x: number; y: number }) => { + moveCallback(client, options, this); + } + ); + this.onMessage( + MessageType.SERVER_CHAT, + (client: Client, options: { message: string }) => { + chatCallback(client, options, this); + } + ); + } + + public getRoomPlayersKeys() { + return Array.from(this.players.keys()); + } + + public movePlayer( + sessionId: string, + { x, y }: { x: number; y: number } + ): void { + this.players.get(sessionId)!.position = { x, y }; + } + + public deletePlayer(sessionId: string) { + this.players.delete(sessionId); + } + + public addPlayer(sessionId: string, playerData: Player) { + this.players.set(sessionId, playerData); + } + + public getAllPlayers(): Player[] { + return Array.from(this.players.values()); + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..eb194cf --- /dev/null +++ b/src/server.ts @@ -0,0 +1,50 @@ +import express from 'express'; +import { createServer } from 'http'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import 'dotenv/config'; +import { Server } from 'colyseus'; +import { WorldRoom } from './rooms/WorldRoom'; +import cors from 'cors'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +const server = createServer(app); + +app.use( + cors({ + origin: 'https://game.ruanfergui.com.br', + credentials: true, + }) +); +app.use(express.static(path.join(__dirname, '../public'))); +app.use(express.json()); + +const gameServer = new Server({ + server, +}); + +gameServer.define('world', WorldRoom); + +app.get('/api/status', (req, res) => { + res.json({ + status: 'online', + timestamp: new Date().toISOString(), + }); +}); + +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, '../public', 'index.html')); +}); + +const PORT = process.env.PORT || 3000; + +server.listen(PORT, () => { + console.log(`🚀 Server running on http://localhost:${PORT}`); + console.log(`🎮 Colyseus listening for game rooms...`); +}); + +export { app, server }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6d0c7d0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "allowImportingTsExtensions": false, + "noEmit": false, + "outDir": "./dist", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}