|
// public/game.js |
|
|
|
// --- Game Configuration --- |
|
const config = { |
|
type: Phaser.AUTO, // Use WebGL if available, otherwise Canvas |
|
parent: 'game-container', // ID of the div to contain the game |
|
width: 800, |
|
height: 600, |
|
backgroundColor: '#cccccc', |
|
physics: { |
|
default: 'arcade', // Simple arcade physics |
|
arcade: { |
|
gravity: { y: 0 }, // No gravity in a top-down tank game |
|
debug: false // Set to true to see physics bodies |
|
} |
|
}, |
|
scene: { |
|
preload: preload, |
|
create: create, |
|
update: update |
|
} |
|
}; |
|
|
|
// --- Global Variables for the Client --- |
|
let game = new Phaser.Game(config); // The main Phaser game instance |
|
let socket; // For WebSocket communication with the server |
|
let selfClientId = null; // This client's player ID, given by the server |
|
let playerTank; // This client's tank sprite |
|
let otherPlayers = {}; // Sprites for other players' tanks |
|
let bullets; // A Phaser group for bullets |
|
let cursors; // For keyboard input |
|
let lastMoveTime = 0; // To limit how often we send movement updates |
|
|
|
// --- Phaser Scene Functions --- |
|
|
|
function preload() { |
|
// Load assets here (images, sounds, etc.) |
|
// For simplicity, we'll draw tanks and bullets with Phaser graphics, |
|
// so no external images are strictly needed for this basic version. |
|
// If you have tank.png and bullet.png: |
|
// this.load.image('tank', 'assets/tank.png'); |
|
// this.load.image('bullet', 'assets/bullet.png'); |
|
} |
|
|
|
function create() { |
|
// --- Setup WebSocket Connection --- |
|
// Use 'ws://localhost:3000' for local development |
|
// For a deployed game, use wss://your-deployed-server-address |
|
socket = new WebSocket(`ws://${window.location.hostname}:3000`); |
|
|
|
socket.onopen = () => { |
|
console.log('Connected to server'); |
|
}; |
|
|
|
socket.onmessage = (event) => { |
|
const message = JSON.parse(event.data); |
|
// console.log('Received message:', message); |
|
|
|
switch (message.type) { |
|
case 'your_id': |
|
selfClientId = message.id; |
|
console.log('My player ID is:', selfClientId); |
|
break; |
|
|
|
case 'current_players': |
|
// Add all players already in the game |
|
for (const id in message.players) { |
|
if (message.players.hasOwnProperty(id)) { |
|
const playerData = message.players[id]; |
|
if (playerData.id === selfClientId) { |
|
addPlayerTank(this, playerData); // 'this' refers to the Phaser Scene |
|
} else { |
|
addOtherPlayerTank(this, playerData); |
|
} |
|
} |
|
} |
|
break; |
|
|
|
case 'player_joined': |
|
if (message.player.id !== selfClientId) { |
|
addOtherPlayerTank(this, message.player); |
|
} |
|
break; |
|
|
|
case 'player_update': |
|
if (otherPlayers[message.id]) { |
|
otherPlayers[message.id].setPosition(message.x, message.y); |
|
otherPlayers[message.id].setAngle(message.angle); |
|
} |
|
break; |
|
|
|
case 'player_left': |
|
if (otherPlayers[message.id]) { |
|
otherPlayers[message.id].destroy(); // Remove tank sprite |
|
delete otherPlayers[message.id]; // Remove from our list |
|
} |
|
break; |
|
|
|
case 'bullet_fired': |
|
fireBullet(this, message.x, message.y, message.angle, message.playerId); |
|
break; |
|
|
|
case 'player_hit': |
|
if (message.targetId === selfClientId && playerTank) { |
|
playerTank.health = message.health; |
|
// Add visual feedback for being hit (e.g., flash red) |
|
playerTank.setTint(0xff0000); |
|
this.time.delayedCall(100, () => { |
|
if (playerTank) playerTank.clearTint(); |
|
}); |
|
console.log(`I was hit! My health: ${playerTank.health}`); |
|
} else if (otherPlayers[message.targetId]) { |
|
otherPlayers[message.targetId].health = message.health; |
|
otherPlayers[message.targetId].setTint(0xff0000); // Flash red |
|
this.time.delayedCall(100, () => { |
|
if(otherPlayers[message.targetId]) otherPlayers[message.targetId].clearTint(); |
|
}); |
|
} |
|
// You could update a health bar UI here |
|
break; |
|
|
|
case 'player_defeated': |
|
if (message.playerId === selfClientId && playerTank) { |
|
console.log("I've been defeated!"); |
|
playerTank.setActive(false).setVisible(false); // Hide tank |
|
// You could show a "Game Over" message or respawn logic |
|
} else if (otherPlayers[message.playerId]) { |
|
console.log(`Player ${message.playerId} defeated`); |
|
otherPlayers[message.playerId].setActive(false).setVisible(false); |
|
// Show an explosion or something |
|
this.time.delayedCall(1000, () => { // Remove after a delay |
|
if (otherPlayers[message.playerId]) { |
|
otherPlayers[message.playerId].destroy(); |
|
delete otherPlayers[message.playerId]; |
|
} |
|
}); |
|
} |
|
break; |
|
} |
|
}; |
|
|
|
socket.onerror = (error) => { |
|
console.error('WebSocket Error:', error); |
|
}; |
|
|
|
socket.onclose = () => { |
|
console.log('Disconnected from server'); |
|
// Maybe show a "disconnected" message to the player |
|
}; |
|
|
|
// --- Setup Game Elements --- |
|
// Create a group for bullets to manage them easily |
|
bullets = this.physics.add.group({ |
|
defaultKey: 'bullet_sprite', // If using a loaded image |
|
maxSize: 30, // Max bullets on screen at once |
|
runChildUpdate: true // Bullets will update themselves |
|
}); |
|
|
|
// Input |
|
cursors = this.input.keyboard.createCursorKeys(); // For arrow key movement |
|
this.input.keyboard.on('keydown-SPACE', () => { // For shooting with Spacebar |
|
if (playerTank && playerTank.active && socket && socket.readyState === WebSocket.OPEN) { |
|
// Tell the server we are shooting |
|
socket.send(JSON.stringify({ |
|
type: 'shoot', |
|
x: playerTank.x, |
|
y: playerTank.y, |
|
angle: playerTank.angle |
|
})); |
|
} |
|
}); |
|
|
|
// Add collision detection between bullets and tanks |
|
// Note: For an MMO, authoritative hit detection should be on the server. |
|
// Here, the client that fired the bullet can detect a hit and tell the server. |
|
this.physics.add.overlap(bullets, null, handleBulletTankCollision, null, this); |
|
// The 'null' for colliders means bullets will collide with *all* physics bodies |
|
// in the scene unless specified. We'll filter in `handleBulletTankCollision`. |
|
|
|
} // --- End of create() --- |
|
|
|
function update(time, delta) { |
|
if (!playerTank || !playerTank.active) return; // Don't do anything if our tank isn't ready or active |
|
|
|
let inputChanged = false; |
|
const moveSpeed = 200; // Pixels per second |
|
const angularSpeed = 150; // Degrees per second |
|
|
|
// --- Handle Player Input and Movement --- |
|
// Stop movement by default |
|
playerTank.body.setVelocity(0, 0); // FIXED: gemini missed ".body" |
|
playerTank.body.setAngularVelocity(0); // FIXED: gemini missed ".body" |
|
|
|
if (cursors.left.isDown) { |
|
playerTank.body.setAngularVelocity(-angularSpeed); // FIXED: gemini missed ".body" |
|
inputChanged = true; |
|
} else if (cursors.right.isDown) { |
|
playerTank.body.setAngularVelocity(angularSpeed); // FIXED: gemini missed ".body" |
|
inputChanged = true; |
|
} |
|
|
|
if (cursors.up.isDown) { |
|
// Move in the direction the tank is facing |
|
this.physics.velocityFromAngle(playerTank.angle - 90, moveSpeed, playerTank.body.velocity); |
|
inputChanged = true; |
|
} else if (cursors.down.isDown) { |
|
this.physics.velocityFromAngle(playerTank.angle - 90, -moveSpeed / 2, playerTank.body.velocity); // Slower reverse |
|
inputChanged = true; |
|
} |
|
|
|
// Keep tank within game boundaries |
|
playerTank.x = Phaser.Math.Clamp(playerTank.x, 25, config.width - 25); // 25 is half tank size (approx) |
|
playerTank.y = Phaser.Math.Clamp(playerTank.y, 25, config.height - 25); |
|
|
|
// Send movement updates to the server periodically |
|
if (inputChanged && time > lastMoveTime + 50) { // Send update every 50ms if moving |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
socket.send(JSON.stringify({ |
|
type: 'player_move', |
|
x: playerTank.x, |
|
y: playerTank.y, |
|
angle: playerTank.angle |
|
})); |
|
} |
|
lastMoveTime = time; |
|
} |
|
|
|
// Update bullets (basic client-side movement for immediate feedback) |
|
// Server will be the authority for hit detection ultimately |
|
bullets.getChildren().forEach(bullet => { |
|
if (bullet.active) { |
|
// Check bounds and deactivate if out of screen |
|
if (bullet.x < 0 || bullet.x > config.width || bullet.y < 0 || bullet.y > config.height) { |
|
bullet.setActive(false).setVisible(false); |
|
bullet.destroy(); // Or return to a pool |
|
} |
|
} |
|
}); |
|
|
|
} // --- End of update() --- |
|
|
|
|
|
// --- Helper Functions for the Client --- |
|
|
|
function addPlayerTank(scene, playerData) { |
|
// Create the tank sprite using Phaser's graphics for simplicity |
|
// A real game would use playerTank = scene.physics.add.sprite(playerData.x, playerData.y, 'tank_image'); |
|
playerTank = scene.add.graphics({ x: playerData.x, y: playerData.y }); |
|
drawTank(playerTank, playerData.color); // Custom function to draw the tank |
|
|
|
// Add physics body to the graphics object |
|
scene.physics.world.enableBody(playerTank, Phaser.Physics.Arcade.DYNAMIC_BODY); |
|
playerTank.body.setSize(40, 50); // Approximate size of our drawn tank |
|
playerTank.body.setCollideWorldBounds(true); // Don't go off screen |
|
|
|
playerTank.id = playerData.id; |
|
playerTank.angle = playerData.angle; // Initial angle from server |
|
playerTank.health = playerData.health; |
|
playerTank.setDepth(1); // Ensure player's tank is usually above others if overlapping briefly |
|
|
|
// Store the graphics object itself if you need to reference it by its physics body later |
|
playerTank.setData('isPlayer', true); |
|
playerTank.setData('id', playerData.id); |
|
} |
|
|
|
function addOtherPlayerTank(scene, playerData) { |
|
// otherTank = scene.physics.add.sprite(playerData.x, playerData.y, 'tank_image'); |
|
const otherTank = scene.add.graphics({ x: playerData.x, y: playerData.y }); |
|
drawTank(otherTank, playerData.color); |
|
scene.physics.world.enableBody(otherTank, Phaser.Physics.Arcade.STATIC_BODY); // Other tanks are static from this client's physics perspective |
|
otherTank.body.setSize(40, 50); |
|
|
|
otherTank.id = playerData.id; |
|
otherTank.angle = playerData.angle; |
|
otherTank.health = playerData.health; |
|
otherPlayers[playerData.id] = otherTank; // Store it |
|
|
|
otherTank.setData('isPlayer', false); |
|
otherTank.setData('id', playerData.id); |
|
} |
|
|
|
function drawTank(graphics, colorHex) { |
|
const color = Phaser.Display.Color.HexStringToColor(colorHex).color; |
|
graphics.clear(); // Clear previous drawing if any (for updates, though we recreate on join) |
|
// Tank Body |
|
graphics.fillStyle(color, 1); |
|
graphics.fillRect(-20, -25, 40, 50); // Tank body (width 40, height 50) |
|
// Tank Turret |
|
graphics.fillStyle(0x666666, 1); // Darker grey for turret |
|
graphics.fillRect(-5, -15, 10, 30); // Turret base |
|
// Tank Barrel |
|
graphics.fillStyle(0x444444, 1); // Even darker for barrel |
|
graphics.fillRect(-2.5, -40, 5, 25); // Barrel (width 5, length 25, pointing "up" relative to tank body) |
|
|
|
// The graphics origin (0,0) is the center of the tank for rotation. |
|
// The fillRect positions are relative to this origin. |
|
// Angle will be applied to the graphics container. |
|
} |
|
|
|
function fireBullet(scene, x, y, angle, ownerId) { |
|
let bullet = bullets.get(); // Get a bullet from the pool/group |
|
|
|
if (bullet) { |
|
// If not using a sprite, create graphics for bullet |
|
if (!bullet.texture || bullet.texture.key === '__MISSING') { |
|
// Create a simple circular graphic for the bullet if no sprite is set |
|
const bulletGraphics = scene.add.graphics(); |
|
bulletGraphics.fillStyle(0x333333, 1); // Dark grey bullet |
|
bulletGraphics.fillCircle(0, 0, 5); // 5px radius circle |
|
// Create a texture from the graphics |
|
const textureKey = `bullet_gfx_${Date.now()}_${Math.random()}`; // Unique key |
|
bulletGraphics.generateTexture(textureKey, 10, 10); |
|
bulletGraphics.destroy(); // Clean up graphics object, we only need the texture |
|
|
|
bullet.setTexture(textureKey); // Apply the new texture |
|
} |
|
|
|
bullet.setActive(true).setVisible(true); |
|
bullet.setPosition(x, y); |
|
bullet.setAngle(angle); |
|
bullet.ownerId = ownerId; // Whose bullet is this? |
|
bullet.body.setSize(10, 10); // Physics body for collision |
|
|
|
// Calculate velocity based on angle |
|
const speed = 600; // Bullet speed |
|
scene.physics.velocityFromAngle(angle - 90, speed, bullet.body.velocity); // Phaser angles start from right, -90 for "up" |
|
|
|
// Optional: Make bullets disappear after some time or distance |
|
bullet.lifespan = 2000; // Milliseconds |
|
scene.time.delayedCall(bullet.lifespan, () => { |
|
bullet.setActive(false).setVisible(false); |
|
bullet.destroy(); // Or return to pool if using one properly |
|
}); |
|
} |
|
} |
|
|
|
function handleBulletTankCollision(bullet, tankGraphics) { |
|
// This function is called by Phaser when a bullet overlaps *any* physics body. |
|
// We need to check if 'tankGraphics' is actually one of our tanks. |
|
// And make sure the bullet doesn't hit its own owner immediately. |
|
|
|
if (!bullet.active || !tankGraphics.active) return; |
|
|
|
const tankId = tankGraphics.getData('id'); |
|
const isPlayerTank = tankGraphics.getData('isPlayer'); // Custom data we set on tank graphics |
|
|
|
if (typeof tankId === 'undefined') return; // Not a tank we track |
|
|
|
// Prevent bullet from hitting its owner immediately after firing |
|
if (bullet.ownerId === tankId) { |
|
// Optional: Allow self-hit after a very short delay if desired, |
|
// but for now, just ignore if owner is the target. |
|
// This check is simple. A more robust way is to ignore collisions for a few frames. |
|
return; |
|
} |
|
|
|
// If this client is the one who fired the bullet, tell the server about the hit |
|
if (bullet.ownerId === selfClientId) { |
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
socket.send(JSON.stringify({ |
|
type: 'hit_player', |
|
targetId: tankId, |
|
damage: 10 // Example damage |
|
})); |
|
} |
|
} |
|
|
|
// Deactivate bullet on hit (client-side prediction) |
|
bullet.setActive(false).setVisible(false); |
|
bullet.destroy(); // Or return to a pool |
|
|
|
// Client can show an impact effect immediately |
|
// The server will confirm the health change. |
|
// console.log(`Bullet from ${bullet.ownerId} hit tank ${tankId}`); |
|
} |