diff --git a/client/BridgeScene.js b/client/BridgeScene.js new file mode 100644 index 0000000..eca07df --- /dev/null +++ b/client/BridgeScene.js @@ -0,0 +1,394 @@ +import Phaser from "phaser"; +import Level from "./Level.js"; +import { PlayerManager } from './managers/PlayerManager' +export default class BridgeScene extends Phaser.Scene { + constructor() { + super("BridgeScene"); + this.player = null; + this.level = new Level(); + this.spike = null; + this.healthBar = null; + this.otherPlayers = {}; + this.canMove = false; + this.countdownText = null; + this.countdownStarted = false; + } + + preload() { + this.load.image("Bridge_Stone_Horizontal", "/assets/Bridge_Stone_Horizontal.png"); + this.load.image("Water_Tile", "/assets/Water_Tile.png"); + this.load.tilemapTiledJSON("bridge", "/assets/bridge.json"); + this.load.spritesheet("player", "/assets/Spearman.png", { + frameWidth: 48, + frameHeight: 48, + }); + } + + create() { + this.game.scale.resize(256,256) + const map = this.make.tilemap({ key: "bridge" }); + const floor = map.addTilesetImage("Water_Tile", "Water_Tile"); + + const floorLayer = map.createLayer("floor", [floor], 0, 0); + floorLayer.setScale(1, 1).setOrigin(0, 0) + .setCollisionByProperty({ collider: true }); + + const Bridge_Stone_Horizontal = map.addTilesetImage("Bridge_Stone_Horizontal", "Bridge_Stone_Horizontal"); + const objectLayer = map.createLayer("object", [Bridge_Stone_Horizontal], 0, 0); + objectLayer.setScale(1, 1).setOrigin(0, 0) + .setCollisionByProperty({ collider: true }); + + + this.player = this.physics.add + .sprite( + 256/2 - 50, + 256/2 - 35, + "player", + ) + .setScale(1); + + this.player.life = 100; + + this.player.setScale(1); // Scale the player sprite by 1.5 times + this.player.setBodySize(24, 28); + this.player.setOffset(10, 13); + this.cursors = this.input.keyboard.createCursorKeys(); + this.anims.create({ + key: "idleDown", + frames: this.anims.generateFrameNumbers("player", { start: 0, end: 5 }), + frameRate: 10, + repeat: -1, + }); + + this.physics.add.collider(this.player, objectLayer); + + this.anims.create({ + key: "idleRight", + frames: this.anims.generateFrameNumbers("player", { start: 6, end: 11 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "idleUp", + frames: this.anims.generateFrameNumbers("player", { start: 12, end: 17 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "walkDown", + frames: this.anims.generateFrameNumbers("player", { start: 18, end: 23 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "walkRight", + frames: this.anims.generateFrameNumbers("player", { start: 24, end: 29 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "walkUp", + frames: this.anims.generateFrameNumbers("player", { start: 30, end: 35 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "attackDown", + frames: this.anims.generateFrameNumbers("player", { start: 36, end: 49 }), + frameRate: 10, + repeat: 1, + }); + + this.anims.create({ + key: "attackRight", + frames: this.anims.generateFrameNumbers("player", { start: 42, end: 46 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "attackUp", + frames: this.anims.generateFrameNumbers("player", { start: 48, end: 52 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "die", + frames: this.anims.generateFrameNumbers("player", { start: 54, end: 56 }), + frameRate: 10, + repeat: 0, + }); + this.player.play("idleDown"); + + this.healthBar = this.createHealthBar(this.player.x, this.player.y, this.player); + + this.attackKey = this.input.keyboard.addKey( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); + + const leaveButton = this.add.text( + this.cameras.main.width - 2, + this.cameras.main.height - 2, + 'Quit Match', + { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '#000', + padding: { x: 5, y: 2 } + } + ) + .setOrigin(1, 1) + .setScrollFactor(0) + .setInteractive() + .setDepth(1000); + + leaveButton.on('pointerdown', () => { + console.log('Leave button clicked'); // Debug log + + if (this.socket) { + this.socket.emit('leaveScene', { + from: 'BridgeScene', + to: 'CommonScene' + }); + } + + // Instead of scene.start, reload the whole game + window.location.reload(); + }); + + this.countdownText = this.add.text( + this.cameras.main.width / 2, + this.cameras.main.height / 2, + 'In Queue...', + { + fontSize: '32px', + fill: '#fff', + stroke: '#000', + strokeThickness: 4 + } + ) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(1000); + + this.socket = io(socketId, { + withCredentials: false, + }); + + this.socket.on('currentPlayers', (players) => { + console.log('Received current players:', players); + Object.keys(players).forEach((id) => { + if (id !== this.socket.id) { + const playerInfo = players[id]; + const otherPlayer = this.physics.add.sprite( + playerInfo.x, + playerInfo.y, + 'player' + ).setScale(1); + this.otherPlayers[id] = otherPlayer; + } + }); + }); + + this.socket.on('newPlayer', (playerInfo) => { + console.log('New player joined:', playerInfo); + const otherPlayer = this.physics.add.sprite( + playerInfo.x, + playerInfo.y, + 'player' + ).setScale(1); + this.otherPlayers[playerInfo.playerId] = otherPlayer; + }); + + this.socket.on('playerDisconnected', (playerId) => { + console.log('Player disconnected:', playerId); + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } + }); + + this.socket.on('playerMoved', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + otherPlayer.play(playerInfo.anim, true); + otherPlayer.flipX = playerInfo.flipX; + } + }); + + this.socket.on('playerAttack', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.play(playerInfo.anim); + } + }); + + this.socket.on('playerDamaged', (data) => { + if (this.otherPlayers[data.playerId]) { + this.otherPlayers[data.playerId].life = data.newLife; + // Update health bar if you have one for other players + } + }); + + this.socket.emit('joinScene', 'BridgeScene'); + } + + checkPlayersAndStartCountdown() { + const playerCount = Object.keys(this.otherPlayers).length + 1; // +1 for local player + + if (playerCount >= 2 && !this.countdownStarted) { + this.countdownStarted = true; + this.startCountdown(); + } else if (playerCount < 2) { + // Reset if a player leaves during countdown + this.countdownStarted = false; + this.canMove = false; + if (this.countdownText) { + this.countdownText.setText('Waiting for players...'); + } + } + } + + startCountdown() { + let count = 3; + + this.countdownText.setText(count.toString()); + + const countdownTimer = this.time.addEvent({ + delay: 1000, + callback: () => { + count--; + if (count > 0) { + this.countdownText.setText(count.toString()); + } else if (count === 0) { + this.countdownText.setText('FIGHT!'); + this.canMove = true; + + this.time.delayedCall(500, () => { + this.countdownText.destroy(); + }); + } + }, + repeat: 3 + }); + } + + createHealthBar(x, y, player) { + const width = 40; + const height = 5; + + // White outline + const outline = this.add.rectangle(x, y - 20, width + 2, height + 2, 0xffffff); + + // Black background + const healthBarBackground = this.add.rectangle(x, y - 20, width, height, 0x000000); + + // Red health bar - set origin to left + const healthBar = this.add.rectangle(x - width/2, y - 20, width, height, 0xff0000) + .setOrigin(0, 0.5); + + return { + outline: outline, + background: healthBarBackground, + bar: healthBar + }; + } + + update() { + const speed = 80; + const prevVelocity = this.player.body.velocity.clone(); + let newX = this.player.x; + let newY = this.player.y; + + // this.input.keyboard.addListener("keydown-F", (e) => { + // console.log(e) + // this.player.play("attackRight"); + // }) + + // Stop any previous movement from the last frame + this.player.body.setVelocity(0); + + // Horizontal movement + if (this.cursors.left.isDown) { + newX -= speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + this.player.body.setVelocityX(-speed); + this.player.anims.play("walkRight", true); // Assuming you have a 'walkLeft' animation + this.player.flipX = true; // Flip the sprite to face left + } + } else if (this.cursors.right.isDown) { + newX += speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + this.player.body.setVelocityX(speed); + this.player.anims.play("walkRight", true); + this.player.flipX = false; // Ensure the sprite is facing right + } + } + + // Vertical movement + if (this.cursors.up.isDown) { + newY -= speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + this.player.body.setVelocityY(-speed); + this.player.anims.play("walkUp", true); + } + } else if (this.cursors.down.isDown) { + newY += speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + console.log("not"); + this.player.body.setVelocityY(speed); + this.player.anims.play("walkDown", true); + } + } + + // Normalize and scale the velocity so that player can't move faster along a diagonal + this.player.body.velocity.normalize().scale(speed); + + // If no movement keys are pressed, stop the animation + if ( + this.cursors.left.isUp && + this.cursors.right.isUp && + this.cursors.up.isUp && + this.cursors.down.isUp + ) { + this.player.anims.stop(); + + // Set idle animation based on the last direction + if (prevVelocity.x < 0) { + this.player.anims.play("idleRight", true); + this.player.flipX = true; + } else if (prevVelocity.x > 0) { + this.player.anims.play("idleRight", true); + this.player.flipX = false; + } else if (prevVelocity.y < 0) { + this.player.anims.play("idleUp", true); + } else if (prevVelocity.y > 0) { + this.player.anims.play("idleDown", true); + } + } + + // Update health bar position and width + if (this.healthBar) { + const yOffset = -20; + const width = 40; + + this.healthBar.outline.x = this.player.x; + this.healthBar.outline.y = this.player.y + yOffset; + this.healthBar.background.x = this.player.x; + this.healthBar.background.y = this.player.y + yOffset; + + // Update red bar position and width + this.healthBar.bar.x = this.player.x - width/2; + this.healthBar.bar.y = this.player.y + yOffset; + this.healthBar.bar.width = (this.player.life / 100) * width; + } + } +} diff --git a/client/CommonScene.js b/client/CommonScene.js new file mode 100644 index 0000000..e85cba6 --- /dev/null +++ b/client/CommonScene.js @@ -0,0 +1,590 @@ +import { AnimationManager } from './managers/AnimationManager' +import { PlayerManager } from './managers/PlayerManager' +import { CombatManager} from './managers/CombatManager' +import Level from './Level.js' +import { io } from 'socket.io-client' +import { XPBar } from './managers/XPBar' +const socketUrl = "https://latch.netlify.app/game"; + +export default class CommonScene extends Phaser.Scene { + constructor() { + super('CommonScene') + this.player = null + this.otherPlayers = {} + this.level = new Level() + this.spike = null + this.lastEmitTime = 0 + } + + preload() { + this.load.image('Apple_Tree', '/assets/Apple_Tree.png') + this.load.image('Barn', '/assets/Barn.png') + this.load.image('Beach_Decor_Tiles', '/assets/Beach_Decor_Tiles.png') + this.load.image('Beach_Tile', '/assets/Beach_Tile.png') + this.load.image('Birch_Tree', '/assets/Birch_Tree.png') + this.load.image('Boat', '/assets/Boat.png') + this.load.image('Cobble_Road_1', '/assets/Cobble_Road_1.png') + this.load.image('Cobble_Road_2', '/assets/Cobble_Road_2.png') + this.load.image('Fences', '/assets/Fences.png') + this.load.image('Fountain', '/assets/Fountain.png') + this.load.image('Grass_Middle', '/assets/Grass_Middle.png') + this.load.image('Grass_Tiles_1', '/assets/Grass_Tiles_1.png') + this.load.image('Water_Middle', '/assets/Water_Middle.png') + this.load.image('Water_Tile', '/assets/Water_Tile.png') + this.load.image('Well', '/assets/Well.png') + this.load.image('With_Hut', '/assets/With_Hut.png') + this.load.image('Cave_Floor', '/assets/Cave_Floor.png') + this.load.image('Water_Troughs', '/assets/Water_Troughs.png') + + this.load.tilemapTiledJSON('common', '/assets/common.json') + this.load.spritesheet('player', '/assets/Spearman.png', { + frameWidth: 48, + frameHeight: 48, + }) + } + + create() { + const spawnData = this.scene.settings.data; + if (spawnData && spawnData.x && spawnData.y) { + this.game.config.width = spawnData.x; + this.game.config.height = spawnData.y; + } + + const map = this.make.tilemap({ key: 'common' }) + const grass = map.addTilesetImage('Grass_Middle', 'Grass_Middle') + const water = map.addTilesetImage('Water_Tile', 'Water_Tile') + const waterMiddle = map.addTilesetImage('Water_Middle', 'Water_Middle') + const Cobble_Road_1 = map.addTilesetImage('Cobble_Road_1', 'Cobble_Road_1') + const Grass_Tiles_1 = map.addTilesetImage('Grass_Tiles_1', 'Grass_Tiles_1') + const Cave_Floor = map.addTilesetImage('Cave_Floor', 'Cave_Floor') + const beachDecorTiles = map.addTilesetImage( + 'Beach_Decor_Tiles', + 'Beach_Decor_Tiles' + ) + const layer1 = map.createLayer( + 'floor', + [ + grass, + waterMiddle, + water, + beachDecorTiles, + Cobble_Road_1, + Grass_Tiles_1, + Cave_Floor, + ], + 0, + 0 + ) + layer1 + .setScale(1, 1) + .setOrigin(0, 0) + .setCollisionByProperty({ collider: true }) + + const Fountain = map.addTilesetImage('Fountain', 'Fountain') + const Barn = map.addTilesetImage('Barn', 'Barn') + const Fences = map.addTilesetImage('Fences', 'Fences') + const Well = map.addTilesetImage('Well', 'Well') + const Boat = map.addTilesetImage('Boat', 'Boat') + const With_Hut = map.addTilesetImage('With_Hut', 'With_Hut') + const Birch_Tree = map.addTilesetImage('Birch_Tree', 'Birch_Tree') + const Apple_Tree = map.addTilesetImage('Apple_Tree', 'Apple_Tree') + const Water_Troughs = map.addTilesetImage('Water_Troughs', 'Water_Troughs') + const objectLayer = map.createLayer( + 'objects', + [ + Fountain, + Barn, + Fences, + Well, + Boat, + With_Hut, + Birch_Tree, + Apple_Tree, + Grass_Tiles_1, + Cave_Floor, + Water_Troughs, + ], + 0, + 0 + ) + objectLayer + .setScale(1, 1) + .setOrigin(0, 0) + .setCollisionByProperty({ collider: true }) + + // Clear any existing labels first + this.children.list + .filter(child => child.type === 'Text') + .forEach(label => label.destroy()); + + // Track positions where we've already placed labels + const labelPositions = new Set(); + + // Function to check if position is too close to existing labels + const isTooClose = (x, y) => { + for (let pos of labelPositions) { + const [existingX, existingY] = pos.split(',').map(Number); + const distance = Math.sqrt(Math.pow(existingX - x, 2) + Math.pow(existingY - y, 2)); + if (distance < 50) { // Adjust this number to change how close labels can be + return true; + } + } + return false; + }; + + // Add permanent scene transition labels + const dungeonTiles = objectLayer.filterTiles(tile => tile.properties.dungeon); + dungeonTiles.forEach(tile => { + const posKey = `${tile.pixelX},${tile.pixelY}`; + if (!isTooClose(tile.pixelX, tile.pixelY)) { + this.add.text(tile.pixelX, tile.pixelY - 20, 'Battle', { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '', + padding: { x: 5, y: 2 } + }).setOrigin(0.5); + labelPositions.add(posKey); + } + }); + + const bridgeTiles = objectLayer.filterTiles(tile => tile.properties.bridge); + bridgeTiles.forEach(tile => { + const posKey = `${tile.pixelX},${tile.pixelY}`; + if (!isTooClose(tile.pixelX, tile.pixelY)) { + this.add.text(tile.pixelX + 16, tile.pixelY - 20, 'Battle', { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '', + padding: { x: 5, y: 2 } + }).setOrigin(0.5); + labelPositions.add(posKey); + } + }); + + this.socket = io(socketId, { + withCredentials: false, + }) + this.player = this.physics.add + .sprite(this.game.config.width / 2, this.game.config.height / 2, 'player') + .setScale(1) + + this.player.setCollideWorldBounds(true) + this.player.life = 100 + this.player.attack = 0 + this.player.weapon = 'sword' + this.player.setScale(1) // Scale the player sprite by 1.5 times + this.player.setBodySize(24, 28) + this.player.setOffset(10, 13) + this.cursors = this.input.keyboard.createCursorKeys() + this.anims.create({ + key: 'idleDown', + frames: this.anims.generateFrameNumbers('player', { start: 0, end: 5 }), + frameRate: 10, + repeat: -1, + }) + let element = document.getElementById('input-box') + const yesButton = document.getElementById('yes') + const noButton = document.getElementById('no') + this.physics.add.collider(this.player, layer1) + this.physics.add.collider(this.player, objectLayer, (a, b) => { + if (b?.properties?.dungeon) { + element.style.display = 'block' + yesButton.addEventListener('click', () => { + this.socket.emit('changeScene', { + from: 'CommonScene', + to: 'DungeonScene' + }); + this.scene.start('DungeonScene') + element.style.display = 'none' + }) + noButton.addEventListener('click', () => { + element.style.display = 'none' + }) + } + if (b?.properties?.bridge) { + element.style.display = 'block' + yesButton.addEventListener('click', () => { + this.scene.start('BridgeScene') + element.style.display = 'none' + }) + noButton.addEventListener('click', () => { + element.style.display = 'none' + }) + } + }) + + // this.cameras.main.setBounds(0, 0, +this.game.config.width, +this.game.config.height); + this.cameras.main.startFollow(this.player, true) + this.cameras.main.setFollowOffset(-50, -50) + + this.anims.create({ + key: 'idleRight', + frames: this.anims.generateFrameNumbers('player', { start: 6, end: 11 }), + frameRate: 10, + repeat: -1, + }) + + this.anims.create({ + key: 'idleUp', + frames: this.anims.generateFrameNumbers('player', { start: 12, end: 17 }), + frameRate: 10, + repeat: -1, + }) + + this.anims.create({ + key: 'walkDown', + frames: this.anims.generateFrameNumbers('player', { start: 18, end: 23 }), + frameRate: 10, + repeat: -1, + }) + + this.anims.create({ + key: 'walkRight', + frames: this.anims.generateFrameNumbers('player', { start: 24, end: 29 }), + frameRate: 10, + repeat: -1, + }) + + this.anims.create({ + key: 'walkUp', + frames: this.anims.generateFrameNumbers('player', { start: 30, end: 35 }), + frameRate: 10, + repeat: -1, + }) + + + + AnimationManager.createAnimations(this) + this.player.play('idleDown') + this.handleSocketEvents() + + // Add a attack kek + this.attackKey = this.input.keyboard.addKey( + Phaser.Input.Keyboard.KeyCodes.SPACE + ) + + // Add inventory key + this.inventoryKey = this.input.keyboard.addKey( + Phaser.Input.Keyboard.KeyCodes.I + ) + + // Create inventory (initially hidden) + this.createInventory() + + // Add isAttacking flag + this.player.isAttacking = false + + // Create XP bar with debug logging + console.log('Creating XP bar...'); + this.xpBar = new XPBar(this); + + // Test if XP bar exists and has required methods + console.log('XP Bar created:', { + exists: !!this.xpBar, + background: !!this.xpBar?.background, + fillBar: !!this.xpBar?.fillBar, + levelText: !!this.xpBar?.levelText, + xpText: !!this.xpBar?.xpText + }); + + // Force an initial update + this.xpBar.update(50, 1); // Changed from setXP to update + + // Listen for XP updates + this.socket.on('xpUpdate', (data) => { + if (data.playerId === this.socket.id) { + this.xpBar.update(data.xp, data.level); // Changed from setXP to update + } + }); + + // Request initial XP data + this.socket.emit('requestXPData'); + } + + handleSocketEvents() { + const socket = this.socket + + socket.on('currentPlayers', (players) => { + Object.keys(players).forEach((id) => { + if (players[id].playerId === socket.id) { + this.player.setPosition(players[id].x, players[id].y) + this.player.lastDirection = players[id].lastDirection + } else { + this.addOtherPlayer(players[id]) + } + }) + }) + + socket.on('newPlayer', (playerInfo) => { + console.log('New player connected:', playerInfo) + this.addOtherPlayer(playerInfo) + }) + + socket.on('playerMoved', (playerInfo) => { + if (!playerInfo || !playerInfo.playerId) return; + + // Handle local player + if (playerInfo.playerId === this.socket.id) { + if (this.player && this.player.anims && !this.player.isAttacking) { + try { + this.player.anims.play(playerInfo.animation, true); + this.player.flipX = playerInfo.flipX; + this.player.lastDirection = playerInfo.lastDirection; + } catch (error) { + console.warn('Local player animation error:', error); + } + } + } + // Handle other players + else if (this.otherPlayers && this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + + // Update position + if (otherPlayer && typeof otherPlayer.setPosition === 'function') { + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + } + + // Update animation + if (otherPlayer && otherPlayer.anims && !otherPlayer.isAttacking) { + try { + if (playerInfo.animation && this.anims.exists(playerInfo.animation)) { + otherPlayer.anims.play(playerInfo.animation, true); + } + if (typeof playerInfo.flipX !== 'undefined') { + otherPlayer.flipX = playerInfo.flipX; + } + if (playerInfo.lastDirection) { + otherPlayer.lastDirection = playerInfo.lastDirection; + } + } catch (error) { + console.warn('Other player animation error:', error); + } + } + } + }); + + socket.on('playerAttacked', (data) => { + if (this.otherPlayers[data.target]) { + this.otherPlayers[data.target].life = data.life + console.log('Player attacked:', data) + } + }) + socket.on('playerAttackAnimation', (data) => { + console.log('Received attack animation:', data) + + if (data.attacker === this.socket.id) { + return + } + const otherPlayer = this.otherPlayers[data.attacker] + if (otherPlayer) { + PlayerManager.handleOtherPlayerAttack( + otherPlayer, + data.animation, + data.direction + ) + } + }) + + socket.on('playerDefeated', (playerId) => { + CombatManager.handlePlayerDeath(this, playerId) + console.log('Player defeated:', playerId) + + }) + + socket.on('playerDisconnected', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy() + delete this.otherPlayers[playerId] + } + }) + } + + addOtherPlayer(playerInfo) { + const otherPlayer = this.physics.add.sprite( + playerInfo.x, + playerInfo.y, + 'player' + ) + otherPlayer.playerId = playerInfo.playerId + otherPlayer.life = playerInfo.life + otherPlayer.attack = playerInfo.attack + otherPlayer.weapon = playerInfo.weapon + otherPlayer.lastDirection = playerInfo.lastDirection || 'Down' + otherPlayer.isAttacking = false + otherPlayer.setScale(1) + this.otherPlayers[playerInfo.playerId] = otherPlayer + console.log('Added other player:', playerInfo) + } + + createInventory() { + // Create inventory container + const padding = 10 + const cellSize = 40 + const rows = 4 + const cols = 6 + const width = cellSize * cols + padding * 2 + const height = cellSize * rows + padding * 2 + + // Position in center of screen + const x = this.cameras.main.centerX - width / 2 + const y = this.cameras.main.centerY - height / 2 + + // Create semi-transparent background + this.inventoryBg = this.add + .rectangle(x, y, width, height, 0x000000) + .setOrigin(0, 0) + .setAlpha(0.7) + .setScrollFactor(0) + .setDepth(1000); + + // Create grid cells + this.inventorySlots = [] + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const slotX = x + padding + col * cellSize + const slotY = y + padding + row * cellSize + + // Create slot background + const slot = this.add + .rectangle(slotX, slotY, cellSize - 2, cellSize - 2, 0x666666) + .setOrigin(0, 0) + .setAlpha(0.8) + .setScrollFactor(0) + .setDepth(1001); + + this.inventorySlots.push(slot) + } + } + + // Hide inventory initially + this.hideInventory() + } + + hideInventory() { + this.inventoryBg.setVisible(false) + this.inventorySlots.forEach((slot) => slot.setVisible(false)) + } + + showInventory() { + this.inventoryBg.setVisible(true) + this.inventorySlots.forEach((slot) => slot.setVisible(true)) + } + + update(time, delta) { + if (!this.player) return; + + // Update depths based on Y position, but keep lower than UI elements + this.player.setDepth(this.player.y + 100); // Base player depth on Y position + + Object.values(this.otherPlayers).forEach(otherPlayer => { + otherPlayer.setDepth(otherPlayer.y + 100); // Same for other players + }); + + const speed = 80; + let animation = this.lastDirection ? 'idle' + this.lastDirection : 'idleDown'; + + // Stop any previous movement + this.player.body.setVelocity(0); + + if (!this.player.isAttacking) { + // Handle movement and set last direction + if (this.cursors.left.isDown) { + this.player.body.setVelocityX(-speed); + animation = 'walkRight'; + this.lastDirection = 'Right'; + this.player.flipX = true; + } else if (this.cursors.right.isDown) { + this.player.body.setVelocityX(speed); + animation = 'walkRight'; + this.lastDirection = 'Right'; + this.player.flipX = false; + } + + if (this.cursors.up.isDown) { + this.player.body.setVelocityY(-speed); + animation = 'walkUp'; + this.lastDirection = 'Up'; + } else if (this.cursors.down.isDown) { + this.player.body.setVelocityY(speed); + animation = 'walkDown'; + this.lastDirection = 'Down'; + } + + // Normalize and scale the velocity + this.player.body.velocity.normalize().scale(speed); + + // Play the animation + this.player.anims.play(animation, true); + } + + // Emit movement to server + if (time - this.lastEmitTime > 16) { + this.socket.emit('playerInput', { + x: this.player.x, + y: this.player.y, + animation: animation, + flipX: this.player.flipX, + lastDirection: this.lastDirection, + }); + this.lastEmitTime = time; + } + + // Handle attack + if (Phaser.Input.Keyboard.JustDown(this.attackKey)) { + this.handleAttack(); + } + + // Handle inventory toggle + if (Phaser.Input.Keyboard.JustDown(this.inventoryKey)) { + if (this.inventoryBg.visible) { + this.hideInventory(); + } else { + this.showInventory(); + } + } + + // Update health bar + if (this.healthBar) { + const yOffset = -20; + const width = 40; + + this.healthBar.outline.x = this.player.x; + this.healthBar.outline.y = this.player.y + yOffset; + this.healthBar.background.x = this.player.x; + this.healthBar.background.y = this.player.y + yOffset; + + this.healthBar.bar.x = this.player.x - width/2; + this.healthBar.bar.y = this.player.y + yOffset; + this.healthBar.bar.width = (this.player.life / 100) * width; + } + } + + handleAttack() { + // Find the closest player to attack + let closestPlayer = null + let closestDistance = Infinity + + Object.keys(this.otherPlayers).forEach((id) => { + const otherPlayer = this.otherPlayers[id] + const distance = Phaser.Math.Distance.Between( + this.player.x, + this.player.y, + otherPlayer.x, + otherPlayer.y + ) + if (distance < closestDistance) { + closestDistance = distance + closestPlayer = otherPlayer + } + }) + + if (closestPlayer && closestDistance < 50) { + // Adjust attack range as needed + // this.socket.emit('attackPlayer', closestPlayer.playerId) + PlayerManager.handleAttack(this, this.player, closestPlayer.playerId) + console.log('Attacking player:', closestPlayer.playerId) + } + } +} diff --git a/client/DungeonScene.js b/client/DungeonScene.js new file mode 100644 index 0000000..cb6e284 --- /dev/null +++ b/client/DungeonScene.js @@ -0,0 +1,409 @@ +import Phaser from "phaser"; +import Level from "./Level.js"; +import { PlayerManager } from './managers/PlayerManager' + +export default class DungeonScene extends Phaser.Scene { + constructor() { + super("DungeonScene"); + this.player = null; + this.level = new Level(); + this.spike = null; + this.healthBar = null; + this.otherPlayers = {}; + this.canMove = false; + this.countdownText = null; + this.countdownStarted = false; + } + + preload() { + this.load.image("Dungeon_1", "/assets/Dungeon_1.png"); + this.load.image("windows", "/assets/Dungeon_2_Arch_small.png"); + this.load.image("pillars", "/assets/Dungeon_2_Pillars.png"); + this.load.image("objects", "/assets/Dungeon_Objects.png"); + this.load.image("spikes", "/assets/Floor_spikes_1.png"); + this.load.tilemapTiledJSON("dungeon", "assets/dmap.json"); + this.load.spritesheet("player", "assets/Spearman.png", { + frameWidth: 48, + frameHeight: 48, + }); + this.load.spritesheet("spike", "assets/spk1.png", { + frameWidth: 48, + frameHeight: 48, + }); + // this.load.image("spike", "assets/tile000.png"); + } + + create() { +this.game.scale.resize(256,256) + const map = this.make.tilemap({ key: "dungeon" }); + const floor = map.addTilesetImage("Dungeon_1", "Dungeon_1"); + const floorLayer = map.createLayer("Tile Layer 1", [floor], 0, -100); + floorLayer.setScale(1, 2).setOrigin(0, 0); + + const windows = map.addTilesetImage("windows", "windows"); + const windowsLayer = map.createLayer("windows", [windows], 0, 0); + windowsLayer.setScale(1, 1).setOrigin(0, 0); + + const pillars = map.addTilesetImage("pillars", "pillars"); + const pillarLayer = map.createLayer("pillars", pillars, 0, 0); + pillarLayer.setScale(1, 1).setOrigin(0, 0); + pillarLayer.setCollisionByProperty({ collider: true }); + + const objects = map.addTilesetImage("objects", "objects"); + const spikes = map.addTilesetImage("spikes", "spikes"); + const objectLayer = map.createLayer("objects", [objects, spikes], 0, 0); + objectLayer + .setScale(1, 1) + .setOrigin(0, 0) + .setCollisionByProperty({ collider: true }); + // this.spike = this.physics.add.image( + // this.game.config.width / 2 - 70, + // this.game.config.height / 2 - 25, + // "spike", + // ); + // + // this.anims.create({ + // key: "spike-anim", + // frames: this.anims.generateFrameNumbers("spike", { start: 0, end: 7 }), + // frameRate: 10, + // repeat: -1, + // }); + // this.spike.play("spike-anim", true); + + this.player = this.physics.add + .sprite( + 256/2 - 50, + 256/2 - 35, + "player", + ) + .setScale(1); + + this.player.life = 100; + + this.player.setScale(1); // Scale the player sprite by 1.5 times + this.player.setBodySize(24, 28); + this.player.setOffset(10, 13); + this.cursors = this.input.keyboard.createCursorKeys(); + this.anims.create({ + key: "idleDown", + frames: this.anims.generateFrameNumbers("player", { start: 0, end: 5 }), + frameRate: 10, + repeat: -1, + }); + + this.physics.add.collider(this.player, pillarLayer); + this.physics.add.collider(this.player, objectLayer); + + this.anims.create({ + key: "idleRight", + frames: this.anims.generateFrameNumbers("player", { start: 6, end: 11 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "idleUp", + frames: this.anims.generateFrameNumbers("player", { start: 12, end: 17 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "walkDown", + frames: this.anims.generateFrameNumbers("player", { start: 18, end: 23 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "walkRight", + frames: this.anims.generateFrameNumbers("player", { start: 24, end: 29 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "walkUp", + frames: this.anims.generateFrameNumbers("player", { start: 30, end: 35 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "attackDown", + frames: this.anims.generateFrameNumbers("player", { start: 36, end: 39 }), + frameRate: 10, + repeat: 1, + }); + + this.anims.create({ + key: "attackRight", + frames: this.anims.generateFrameNumbers("player", { start: 42, end: 46 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "attackUp", + frames: this.anims.generateFrameNumbers("player", { start: 48, end: 52 }), + frameRate: 10, + repeat: -1, + }); + + this.anims.create({ + key: "die", + frames: this.anims.generateFrameNumbers("player", { start: 54, end: 56 }), + frameRate: 10, + repeat: 0, + }); + this.player.play("idleDown"); + this.healthBar = this.createHealthBar(this.player.x, this.player.y, this.player); + + this.attackKey = this.input.keyboard.addKey( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); + + // Add leave button + const leaveButton = this.add.text( + this.cameras.main.width - 2, + this.cameras.main.height - 2, + 'Quit Match', + { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '#000', + padding: { x: 5, y: 2 } + } + ) + .setOrigin(1, 1) + .setScrollFactor(0) + .setInteractive() + .setDepth(1000); + + leaveButton.on('pointerdown', () => { + console.log('Leave button clicked'); // Debug log + + if (this.socket) { + this.socket.emit('leaveScene', { + from: 'DungeonScene', + to: 'CommonScene' + }); + } + + // Reload the whole game + window.location.reload(); + }); + + // Add waiting text instead of starting countdown immediately + this.countdownText = this.add.text( + this.cameras.main.width / 2, + this.cameras.main.height / 2, + 'In Queue...', + { + fontSize: '32px', + fill: '#fff', + stroke: '#000', + strokeThickness: 4 + } + ) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(1000); + + // Listen for player join/leave events + this.socket.on('playerJoined', () => this.checkPlayersAndStartCountdown()); + this.socket.on('playerLeft', () => this.checkPlayersAndStartCountdown()); + } + + createHealthBar(x, y, player) { + const width = 40; + const height = 5; + + // White outline + const outline = this.add.rectangle(x, y - 40, width + 2, height + 2, 0xffffff); + + // Black background + const healthBarBackground = this.add.rectangle(x, y - 40, width, height, 0x000000); + + // Red health bar - set origin to left + const healthBar = this.add.rectangle(x - width/2, y - 40, width, height, 0xff0000) + .setOrigin(0, 0.5); + + return { + outline: outline, + background: healthBarBackground, + bar: healthBar + }; + } + + update() { + const speed = 80; + const prevVelocity = this.player.body.velocity.clone(); + let newX = this.player.x; + let newY = this.player.y; + + // this.input.keyboard.addListener("keydown-F", (e) => { + // console.log(e) + // this.player.play("attackRight"); + // }) + + // Stop any previous movement from the last frame + this.player.body.setVelocity(0); + + // Horizontal movement + if (this.cursors.left.isDown) { + newX -= speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + this.player.body.setVelocityX(-speed); + this.player.anims.play("walkRight", true); // Assuming you have a 'walkLeft' animation + this.player.flipX = true; // Flip the sprite to face left + } + } else if (this.cursors.right.isDown) { + newX += speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + this.player.body.setVelocityX(speed); + this.player.anims.play("walkRight", true); + this.player.flipX = false; // Ensure the sprite is facing right + } + } + + // Vertical movement + if (this.cursors.up.isDown) { + newY -= speed * (1 / 60); + if (!this.level.isColliding(newX, newY)) { + this.player.body.setVelocityY(-speed); + this.player.anims.play("walkUp", true); + } + } else if (this.cursors.down.isDown) { + newY += speed * (1 / 60); + console.log(this.level.isColliding(newX, newY)); + if (!this.level.isColliding(newX, newY)) { + console.log("not"); + this.player.body.setVelocityY(speed); + this.player.anims.play("walkDown", true); + } + } + + // Normalize and scale the velocity so that player can't move faster along a diagonal + this.player.body.velocity.normalize().scale(speed); + + // If no movement keys are pressed, stop the animation + if ( + this.cursors.left.isUp && + this.cursors.right.isUp && + this.cursors.up.isUp && + this.cursors.down.isUp + ) { + this.player.anims.stop(); + + // Set idle animation based on the last direction + if (prevVelocity.x < 0) { + this.player.anims.play("idleRight", true); + this.player.flipX = true; + } else if (prevVelocity.x > 0) { + this.player.anims.play("idleRight", true); + this.player.flipX = false; + } else if (prevVelocity.y < 0) { + this.player.anims.play("idleUp", true); + } else if (prevVelocity.y > 0) { + this.player.anims.play("idleDown", true); + } + } + + // Update health bar position and width + if (this.healthBar) { + const yOffset = -20; + const width = 40; + + this.healthBar.outline.x = this.player.x; + this.healthBar.outline.y = this.player.y + yOffset; + this.healthBar.background.x = this.player.x; + this.healthBar.background.y = this.player.y + yOffset; + + // Update red bar position and width + this.healthBar.bar.x = this.player.x - width/2; + this.healthBar.bar.y = this.player.y + yOffset; + this.healthBar.bar.width = (this.player.life / 100) * width; + } + } + + handleSocketEvents() { + // ... keep existing socket events ... + + // Check if this handler exists and has flipX + this.socket.on('playerMoved', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + otherPlayer.anims.play(playerInfo.animation, true); + otherPlayer.flipX = playerInfo.flipX; // Make sure this is here + } + }); + } + + getSpawnPoint() { + // Define two specific spawn points + const spawnPoints = [ + { x: 50, y: 90 }, // First player spawn + { x: 200, y: 90 } // Second player spawn + ]; + + // Count existing players to determine spawn point + const playerCount = Object.keys(this.otherPlayers).length; + + // First player gets first spawn, second player gets second spawn + const spawnPoint = spawnPoints[playerCount] || spawnPoints[0]; + + // Validate the chosen point + if (!this.level.isColliding(spawnPoint.x, spawnPoint.y)) { + return spawnPoint; + } + + // Fallback to center if spawn point is invalid + return { + x: this.cameras.main.width / 2, + y: this.cameras.main.height / 2 + }; + } + + checkPlayersAndStartCountdown() { + const playerCount = Object.keys(this.otherPlayers).length + 1; // +1 for local player + + if (playerCount >= 2 && !this.countdownStarted) { + this.countdownStarted = true; + this.startCountdown(); + } else if (playerCount < 2) { + // Reset if a player leaves during countdown + this.countdownStarted = false; + this.canMove = false; + if (this.countdownText) { + this.countdownText.setText('In Queue...'); + } + } + } + + startCountdown() { + let count = 3; + + this.countdownText.setText(count.toString()); + + const countdownTimer = this.time.addEvent({ + delay: 1000, + callback: () => { + count--; + if (count > 0) { + this.countdownText.setText(count.toString()); + } else if (count === 0) { + this.countdownText.setText('FIGHT!'); + this.canMove = true; + + this.time.delayedCall(500, () => { + this.countdownText.destroy(); + }); + } + }, + repeat: 3 + }); + } +} diff --git a/client/dungeonmap/src/BridgeScene.js b/client/dungeonmap/src/BridgeScene.js index b001264..1dd9bf5 100644 --- a/client/dungeonmap/src/BridgeScene.js +++ b/client/dungeonmap/src/BridgeScene.js @@ -1,239 +1,1102 @@ import Phaser from "phaser"; import Level from "./Level.js"; +import BackgroundScene from './managers/backgroundscene' +import { PlayerManager } from './managers/PlayerManager' +import { AnimationManager } from './managers/AnimationManager' +import { CombatManager } from './managers/CombatManager' +import { io } from 'socket.io-client' + + export default class BridgeScene extends Phaser.Scene { - constructor() { - super("BridgeScene"); - this.player = null; - this.level = new Level(); - this.spike = null; - this.healthBar = null; - } - - preload() { - this.load.image("Bridge_Stone_Horizontal", "/assets/Bridge_Stone_Horizontal.png"); - this.load.image("Water_Tile", "/assets/Water_Tile.png"); - this.load.tilemapTiledJSON("bridge", "/assets/bridge.json"); - this.load.spritesheet("player", "/assets/Spearman.png", { - frameWidth: 48, - frameHeight: 48, - }); + constructor() { + super("BridgeScene"); + this.player = null; + this.level = new Level(); + this.spike = null; + this.healthBar = null; + this.otherPlayers = {}; + this.canMove = false; + this.countdownText = null; + this.countdownStarted = false; + this.socket = null; + this.backgroundScene = null; + this.animationsCreated = false; // Track if animations are created + this.otherPlayersGroup = null; + this.playerWorldPosition = {}; + this.playersGroup = null; + this.playerEventListeners = new Map(); + this.localPlayerId = null; // Track local player ID + this.hasJoinedScene = false; // Track if we've already joined + this.initializedPlayers = new Set(); // Track which players we've initialized + this.existingPlayers = new Set(); // Track existing player IDs + this.isAttacking = false; // Add attack state tracking + this.lastMovementUpdate = 0; + this.movementUpdateInterval = 50; // Update every 50ms + this.lastPosition = { x: 0, y: 0, animation: '', flipX: false }; + } + + init(data) { + console.log('BridgeScene init with data:', data); + // Only set socket once + if (!this.socket) { + if (data && data.socket) { + this.socket = data.socket; + } else { + this.backgroundScene = this.scene.get('BackgroundScene'); + this.socket = this.backgroundScene.getSocket(); + } + console.log('Socket initialized:', this.socket.id); } + } - create() { - this.game.scale.resize(256,256) - const map = this.make.tilemap({ key: "bridge" }); - const floor = map.addTilesetImage("Water_Tile", "Water_Tile"); - - const floorLayer = map.createLayer("floor", [floor], 0, 0); - floorLayer.setScale(1, 1).setOrigin(0, 0) - .setCollisionByProperty({ collider: true }); - - const Bridge_Stone_Horizontal = map.addTilesetImage("Bridge_Stone_Horizontal", "Bridge_Stone_Horizontal"); - const objectLayer = map.createLayer("object", [Bridge_Stone_Horizontal], 0, 0); - objectLayer.setScale(1, 1).setOrigin(0, 0) - .setCollisionByProperty({ collider: true }); - - - this.player = this.physics.add - .sprite( - 256/2 - 50, - 256/2 - 35, - "player", - ) - .setScale(1); - - this.player.life = 100; - - this.player.setScale(1); // Scale the player sprite by 1.5 times - this.player.setBodySize(24, 28); - this.player.setOffset(10, 13); - this.cursors = this.input.keyboard.createCursorKeys(); - this.anims.create({ - key: "idleDown", - frames: this.anims.generateFrameNumbers("player", { start: 0, end: 5 }), - frameRate: 10, - repeat: -1, - }); + preload() { + this.load.image("Bridge_Stone_Horizontal", "/assets/Bridge_Stone_Horizontal.png"); + this.load.image("Water_Tile", "/assets/Water_Tile.png"); + this.load.tilemapTiledJSON("bridge", "/assets/bridge.json"); + this.load.spritesheet("player", "/assets/Spearman.png", { + frameWidth: 48, + frameHeight: 48, + }); + } - this.physics.add.collider(this.player, objectLayer); + create() { + console.log('BridgeScene create starting'); + + // Initialize groups first, before any other creation logic + this.playersGroup = this.add.group(); + this.otherPlayersGroup = this.add.group(); + + this.game.scale.resize(256,256); + const map = this.make.tilemap({ key: "bridge" }); + const floor = map.addTilesetImage("Water_Tile", "Water_Tile"); - this.anims.create({ - key: "idleRight", - frames: this.anims.generateFrameNumbers("player", { start: 6, end: 11 }), - frameRate: 10, - repeat: -1, - }); + const floorLayer = map.createLayer("floor", [floor], 0, 0); + floorLayer.setScale(1, 1).setOrigin(0, 0) + .setCollisionByProperty({ collider: true }); - this.anims.create({ - key: "idleUp", - frames: this.anims.generateFrameNumbers("player", { start: 12, end: 17 }), - frameRate: 10, - repeat: -1, - }); + const Bridge_Stone_Horizontal = map.addTilesetImage("Bridge_Stone_Horizontal", "Bridge_Stone_Horizontal"); + const objectLayer = map.createLayer("object", [Bridge_Stone_Horizontal], 0, 0); + objectLayer.setScale(1, 1).setOrigin(0, 0) + .setCollisionByProperty({ collider: true }); + + this.player = this.physics.add + .sprite( + 256/2 - 50, + 256/2 - 35, + "player", + ) + .setScale(1); + + this.player.life = 100; - this.anims.create({ - key: "walkDown", - frames: this.anims.generateFrameNumbers("player", { start: 18, end: 23 }), - frameRate: 10, - repeat: -1, + this.player.setScale(1); // Scale the player sprite by 1.5 times + this.player.setBodySize(24, 28); + this.player.setOffset(10, 13); + this.cursors = this.input.keyboard.createCursorKeys(); + + this.player.play("idleDown"); + this.healthBar = this.createHealthBar(this.player.x, this.player.y, this.player); + + + + // Add leave button + const leaveButton = this.add.text( + this.cameras.main.width - 2, + this.cameras.main.height - 2, + 'Quit Match', + { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '#000', + padding: { x: 5, y: 2 } + } + ) + .setOrigin(1, 1) + .setScrollFactor(0) + .setInteractive() + .setDepth(1000); + + leaveButton.on('pointerdown', () => { + console.log('Returning to common map'); + + if (this.socket) { + this.socket.emit('playerLeftScene', { + playerId: this.socket.id, + from: 'BridgeScene', + to: 'CommonScene' + }); + } + + // Reset game scale before transitioning + this.game.scale.resize(800, 600); // Set to CommonScene dimensions + + this.scene.start('CommonScene', { + x: 620, + y: 360 }); + }); + + // Add waiting text instead of starting countdown immediately + this.countdownText = this.add.text( + this.cameras.main.width / 2, + this.cameras.main.height / 2, + 'In Queue...', + { + fontSize: '32px', + fill: '#fff', + stroke: '#000', + strokeThickness: 4 + } + ) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(1000); + + // Listen for player join/leave events + this.socket.on('playerJoined', () => this.checkPlayersAndStartCountdown()); + this.socket.on('playerLeft', () => this.checkPlayersAndStartCountdown()); + + // Create animations using AnimationManager + AnimationManager.createAnimations(this); - this.anims.create({ - key: "walkRight", - frames: this.anims.generateFrameNumbers("player", { start: 24, end: 29 }), - frameRate: 10, - repeat: -1, + // Setup socket events if socket exists + if (this.socket) { + // Remove any existing listeners first + this.socket.removeAllListeners('gameState'); + this.socket.removeAllListeners('playerDisconnected'); + + // Add new listeners + this.socket.on('gameState', (state) => { + if (state && state.players) { + Object.entries(state.players).forEach(([playerId, playerData]) => { + if (playerId !== this.socket.id && !this.otherPlayers[playerId]) { + // Add new player + this.addOtherPlayer(playerData); + } else if (this.otherPlayers[playerId]) { + // Update existing player + const otherPlayer = this.otherPlayers[playerId]; + otherPlayer.x = playerData.x; + otherPlayer.y = playerData.y; + if (playerData.animation) { + otherPlayer.play(playerData.animation, true); + } + } + }); + } }); - this.anims.create({ - key: "walkUp", - frames: this.anims.generateFrameNumbers("player", { start: 30, end: 35 }), - frameRate: 10, - repeat: -1, + this.socket.on('playerDisconnected', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } }); - this.anims.create({ - key: "attackDown", - frames: this.anims.generateFrameNumbers("player", { start: 36, end: 49 }), - frameRate: 10, - repeat: 1, + // Join the scene + this.socket.emit('joinScene', { + scene: 'BridgeScene', + x: this.game.config.width / 2, + y: this.game.config.height / 2 }); + } + + // Add portal collision handler + this.physics.add.overlap(this.player, this.portal, () => { + // Tell server we're leaving this scene + this.socket.emit('leaveBridgeScene'); + + // Start next scene + this.scene.start('CommonScene'); // or whatever scene you're transitioning to + }); + + // Initialize background scene + if (!this.scene.isActive('BackgroundScene')) { + this.scene.launch('BackgroundScene'); + } + + this.backgroundScene = this.scene.get('BackgroundScene'); + this.socket = this.backgroundScene.getSocket(); + + // Tell background scene we're here + this.backgroundScene.events.emit('changeScene', 'BridgeScene'); + + // Listen for player updates + this.backgroundScene.events.on('playerUpdated', ({ playerId, playerInfo, isNew, isMovement }) => { + if (playerId === this.socket.id) { + if (isNew) this.addPlayer(playerInfo); + } else { + if (isNew) { + this.addOtherPlayer(playerInfo); + } else if (isMovement) { + this.updateOtherPlayer(playerId, playerInfo); + } + } + }); + + // Listen for player removals + this.backgroundScene.events.on('playerRemoved', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } + }); + + this.otherPlayersGroup = this.add.group(); + + // Add these socket listeners + this.socket.on('currentPlayers', (players) => { + console.log('Current players in BridgeScene:', players); + + // Don't clear if we already have these players + const currentPlayerIds = new Set(Object.keys(players)); + const existingPlayerIds = new Set(Object.keys(this.playerWorldPosition)); + + // Only clear and recreate if the player sets are different + if (!this.areSetsEqual(currentPlayerIds, existingPlayerIds)) { + this.clearExistingPlayers(); + + Object.entries(players).forEach(([id, playerInfo]) => { + if (!this.existingPlayers.has(id)) { + console.log('Creating new player:', id); + if (id === this.socket.id) { + this.player = this.createPlayerWithListeners(id, playerInfo); + } else if (playerInfo.scene === 'BridgeScene') { + this.createPlayerWithListeners(id, playerInfo); + } + this.existingPlayers.add(id); + } + }); + + // After all players are created, check if we should start countdown + if (Object.keys(players).length >= 2) { + // Tell server we have enough players + this.socket.emit('bridgeReady', { + scene: 'BridgeScene', + players: Object.keys(players) + }); + } + } + }); + + this.socket.on('newPlayer', (playerInfo) => { + console.log('New player joined:', playerInfo); + if (playerInfo.playerId !== this.localPlayerId && playerInfo.scene === 'BridgeScene') { + this.createPlayerWithListeners(playerInfo.playerId, playerInfo); + } + }); + + this.socket.on('playerMovedInBridge', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + if (playerInfo.animation) { + otherPlayer.play(playerInfo.animation, true); + } + otherPlayer.setFlipX(playerInfo.flipX || false); + } + }); + + this.socket.on('playerDisconnected', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } + }); + + // Listen for server's countdown signal + this.socket.on('bridgeCountdown', () => { + if (!this.countdownStarted) { + this.startCountdown(); + } + }); + + // Store local player ID + this.localPlayerId = this.socket.id; + console.log('Local player ID:', this.localPlayerId); + + // Clean up old listeners before adding new ones + this.cleanupSocketListeners(); - this.anims.create({ - key: "attackRight", - frames: this.anims.generateFrameNumbers("player", { start: 42, end: 46 }), - frameRate: 10, - repeat: -1, + // Set up socket listeners + this.socket.on('currentPlayers', (players) => { + console.log('Current players received:', Object.keys(players)); + + // Clear existing players first + this.clearExistingPlayers(); + + Object.entries(players).forEach(([id, playerInfo]) => { + if (!this.initializedPlayers.has(id)) { + console.log('Initializing player:', id); + if (id === this.localPlayerId) { + console.log('Creating local player'); + this.player = this.createPlayerWithListeners(id, playerInfo); + } else if (playerInfo.scene === 'BridgeScene') { + console.log('Creating remote player'); + this.createPlayerWithListeners(id, playerInfo); + } + this.initializedPlayers.add(id); + } else { + console.log('Player already initialized:', id); + } }); + }); - this.anims.create({ - key: "attackUp", - frames: this.anims.generateFrameNumbers("player", { start: 48, end: 52 }), - frameRate: 10, - repeat: -1, + this.socket.on('newPlayer', (playerInfo) => { + console.log('New player joined:', playerInfo); + if (playerInfo.playerId !== this.localPlayerId && playerInfo.scene === 'BridgeScene') { + this.createPlayerWithListeners(playerInfo.playerId, playerInfo); + } + }); + + this.socket.on('playerMoved', (playerInfo) => { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + if (otherPlayer) { + // Clear any existing tweens to prevent position conflicts + this.tweens.killTweensOf(otherPlayer); + + // Update position immediately + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + + // Update animation only if it's different + if (playerInfo.animation && (!otherPlayer.anims.currentAnim || + otherPlayer.anims.currentAnim.key !== playerInfo.animation)) { + otherPlayer.play(playerInfo.animation, true); + } + + // Update flip state + otherPlayer.setFlipX(playerInfo.flipX); + + // Store latest info + otherPlayer.playerInfo = { + ...playerInfo, + lastUpdated: Date.now() + }; + } + }); + + // Only emit join if we haven't already + if (!this.hasJoinedScene) { + console.log('Emitting first-time join for player:', this.localPlayerId); + this.socket.emit('joinScene', { + scene: 'BridgeScene', + playerId: this.localPlayerId, + x: this.getSpawnPoint().x, + y: this.getSpawnPoint().y, + animation: 'idleDown', + flipX: false }); + this.hasJoinedScene = true; + } + + // Clean up old listeners before adding new ones + this.cleanupSocketListeners(); + + // Set up socket listeners + this.setupSocketListeners(); + + // Update server with player movement + this.time.addEvent({ + delay: 50, // Send updates every 50ms + callback: this.sendPlayerUpdate, + callbackScope: this, + loop: true + }); + + // Listen for player counts in this scene specifically + this.socket.on('playerCount', (data) => { + console.log('Player count update:', data); + if (data.scene === 'BridgeScene' && data.count >= 2 && !this.countdownStarted) { + this.startCountdown(); + } + }); - this.anims.create({ - key: "die", - frames: this.anims.generateFrameNumbers("player", { start: 54, end: 56 }), - frameRate: 10, - repeat: 0, + // Add spacebar input + this.spacebar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + + // Add attack animations + const attackAnims = [ + { key: 'attackDown', start: 36, end: 39 }, + { key: 'attackRight', start: 42, end: 45 }, + { key: 'attackUp', start: 48, end: 51 }, + { key: 'attackLeft', start: 42, end: 45 } + ]; + + attackAnims.forEach(anim => { + if (!this.anims.exists(anim.key)) { + this.anims.create({ + key: anim.key, + frames: this.anims.generateFrameNumbers('player', { + start: anim.start, + end: anim.end + }), + frameRate: 10, + repeat: 0, // Ensure animation only plays once + hideOnComplete: false // Don't hide sprite when animation completes + }); + } + }); + + // Create physics group for other players if it doesn't exist + this.otherPlayersGroup = this.physics.add.group({ + collideWorldBounds: true + }); + + // Add collision between players + this.physics.add.collider(this.player, this.otherPlayersGroup); + + // Add collision between other players + this.physics.add.collider(this.otherPlayersGroup, this.otherPlayersGroup); + + // Set world bounds + this.physics.world.setBounds(0, 0, this.game.config.width, this.game.config.height); + + // Set up scene-specific socket events + this.socket.on('currentBridgePlayers', (players) => { + console.log('Current bridge players:', players); + Object.keys(players).forEach((id) => { + if (id !== this.socket.id) { + this.addOtherPlayer(players[id]); + } }); - this.player.play("idleDown"); + }); + + this.socket.on('newBridgePlayer', (playerInfo) => { + console.log('New player joined bridge:', playerInfo); + if (playerInfo.playerId !== this.socket.id) { + this.addOtherPlayer(playerInfo); + } + }); + + // Emit that we've joined the bridge scene + this.socket.emit('joinScene', { + scene: 'BridgeScene', + x: this.player.x, + y: this.player.y + }); + } + + createHealthBar(x, y, player) { + const width = 40; + const height = 5; + + // White outline + const outline = this.add.rectangle(x, y - 40, width + 2, height + 2, 0xffffff); + + // Black background + const healthBarBackground = this.add.rectangle(x, y - 40, width, height, 0x000000); + + // Red health bar - set origin to left and start with full width + const healthBar = this.add.rectangle(x - width/2, y - 40, width, height, 0xff0000) + .setOrigin(0, 0.5); + + // Make sure initial width matches full health + healthBar.width = width; // Start with full width since health is 100 + + return { + outline: outline, + background: healthBarBackground, + bar: healthBar + }; + } - this.healthBar = this.createHealthBar(this.player.x, this.player.y, this.player); + update() { + const speed = 80; + const prevVelocity = this.player.body.velocity.clone(); + + // Only allow movement if countdown is finished + if (!this.canMove) { + if (this.player) { + this.player.body.setVelocity(0); + } + return; + } + + // Stop any previous movement from the last frame + this.player.body.setVelocity(0); + + // Horizontal movement + if (this.cursors.left.isDown) { + this.player.body.setVelocityX(-speed); + this.player.anims.play("walkRight", true); + this.player.flipX = true; + } else if (this.cursors.right.isDown) { + this.player.body.setVelocityX(speed); + this.player.anims.play("walkRight", true); + this.player.flipX = false; + } + + // Vertical movement + if (this.cursors.up.isDown) { + this.player.body.setVelocityY(-speed); + this.player.anims.play("walkUp", true); + } else if (this.cursors.down.isDown) { + this.player.body.setVelocityY(speed); + this.player.anims.play("walkDown", true); + } + + // Normalize and scale the velocity so that player can't move faster along a diagonal + this.player.body.velocity.normalize().scale(speed); + + // If no movement keys are pressed, stop the animation + if ( + this.cursors.left.isUp && + this.cursors.right.isUp && + this.cursors.up.isUp && + this.cursors.down.isUp + ) { + this.player.anims.stop(); + + // Set idle animation based on the last direction + if (prevVelocity.x < 0) { + this.player.anims.play("idleLeft", true); + this.player.flipX = true; + } else if (prevVelocity.x > 0) { + this.player.anims.play("idleRight", true); + this.player.flipX = false; + } else if (prevVelocity.y < 0) { + this.player.anims.play("idleUp", true); + } else if (prevVelocity.y > 0) { + this.player.anims.play("idleDown", true); + } } - createHealthBar(x, y, player) { + // Update health bar position and width + if (this.healthBar && this.player) { + const yOffset = -20; const width = 40; - const height = 5; - - // White outline - const outline = this.add.rectangle(x, y - 40, width + 2, height + 2, 0xffffff); - // Black background - const healthBarBackground = this.add.rectangle(x, y - 40, width, height, 0x000000); + this.healthBar.outline.x = this.player.x; + this.healthBar.outline.y = this.player.y + yOffset; + this.healthBar.background.x = this.player.x; + this.healthBar.background.y = this.player.y + yOffset; - // Red health bar - set origin to left - const healthBar = this.add.rectangle(x - width/2, y - 40, width, height, 0xff0000) - .setOrigin(0, 0.5); - - return { - outline: outline, - background: healthBarBackground, - bar: healthBar - }; + // Update red bar position and width based on current health + this.healthBar.bar.x = this.player.x - width/2; + this.healthBar.bar.y = this.player.y + yOffset; + this.healthBar.bar.width = (this.player.life / 100) * width; // Make sure this.player.life is set to 100 initially } - update() { - const speed = 80; - const prevVelocity = this.player.body.velocity.clone(); - let newX = this.player.x; - let newY = this.player.y; + this.otherPlayersGroup.getChildren().forEach((player) => { + if (player.playerInfo) { + player.setPosition(player.playerInfo.x, player.playerInfo.y); + player.play(player.playerInfo.animation, true); + player.flipX = player.playerInfo.flipX; + } + }); - // this.input.keyboard.addListener("keydown-F", (e) => { - // console.log(e) - // this.player.play("attackRight"); - // }) + // Update local player + if (this.player && this.canMove) { + // ... existing player movement code ... - // Stop any previous movement from the last frame - this.player.body.setVelocity(0); + // Only emit if this is the local player + if (this.socket.id === this.localPlayerId) { + this.sendPlayerUpdate(); + } + + // Handle attack + if (Phaser.Input.Keyboard.JustDown(this.spacebar) && !this.isAttacking) { + this.handleAttack(); // Use the consolidated attack handler + } + } - // Horizontal movement - if (this.cursors.left.isDown) { - newX -= speed * (1 / 60); - if (!this.level.isColliding(newX, newY)) { - this.player.body.setVelocityX(-speed); - this.player.anims.play("walkRight", true); // Assuming you have a 'walkLeft' animation - this.player.flipX = true; // Flip the sprite to face left + // Update other players + Object.entries(this.playerWorldPosition).forEach(([playerId, playerData]) => { + if (playerData.sprite && playerId !== this.localPlayerId) { + playerData.sprite.setPosition(playerData.x, playerData.y); + if (playerData.animation) { + playerData.sprite.play(playerData.animation, true); } - } else if (this.cursors.right.isDown) { - newX += speed * (1 / 60); - if (!this.level.isColliding(newX, newY)) { - this.player.body.setVelocityX(speed); - this.player.anims.play("walkRight", true); - this.player.flipX = false; // Ensure the sprite is facing right + playerData.sprite.setFlipX(playerData.flipX); + } + }); + + // Clean up any trailing sprites + Object.values(this.otherPlayers).forEach(otherPlayer => { + if (otherPlayer && otherPlayer.playerInfo) { + // If player hasn't been updated in a while, set to idle + const timeSinceUpdate = Date.now() - (otherPlayer.playerInfo.lastUpdated || 0); + if (timeSinceUpdate > 100) { // 100ms threshold + const currentAnim = otherPlayer.anims.currentAnim; + if (currentAnim && currentAnim.key.startsWith('walk')) { + const idleAnim = currentAnim.key.replace('walk', 'idle'); + otherPlayer.play(idleAnim, true); + } } } + }); + } + + handleSocketEvents() { + // ... keep existing socket events ... + + // Check if this handler exists and has flipX + this.socket.on('playerMoved', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + otherPlayer.anims.play(playerInfo.animation, true); + otherPlayer.flipX = playerInfo.flipX; // Make sure this is here + } + }); + } + + getSpawnPoint() { + // Define two specific spawn points + const spawnPoints = [ + { x: 50, y: 90 }, // First player spawn + { x: 200, y: 90 } // Second player spawn + ]; + + // Count existing players to determine spawn point + const playerCount = Object.keys(this.playerWorldPosition).length; + return spawnPoints[playerCount] || spawnPoints[0]; + } - // Vertical movement - if (this.cursors.up.isDown) { - newY -= speed * (1 / 60); - if (!this.level.isColliding(newX, newY)) { - this.player.body.setVelocityY(-speed); - this.player.anims.play("walkUp", true); + checkPlayersAndStartCountdown() { + // Remove local countdown check - wait for server signal instead + console.log('Waiting for server countdown signal...'); + } + + startCountdown() { + if (this.countdownStarted) return; // Prevent multiple countdowns + + console.log('Starting countdown'); + this.countdownStarted = true; + let count = 3; + + if (this.countdownText) { + this.countdownText.setText(count.toString()); + } + + const countdownInterval = setInterval(() => { + count--; + if (count > 0) { + this.countdownText?.setText(count.toString()); + } else { + this.countdownText?.setText('FIGHT!'); + this.canMove = true; + + // Remove countdown text after "FIGHT!" + setTimeout(() => { + this.countdownText?.destroy(); + this.countdownText = null; + }, 1000); + + clearInterval(countdownInterval); + } + }, 1000); + } + + handleAttack() { + // Find the closest player to attack + let closestPlayer = null; + let closestDistance = Infinity; + + Object.keys(this.otherPlayers).forEach((id) => { + const otherPlayer = this.otherPlayers[id]; + const distance = Phaser.Math.Distance.Between( + this.player.x, + this.player.y, + otherPlayer.x, + otherPlayer.y + ); + if (distance < closestDistance) { + closestDistance = distance; + closestPlayer = otherPlayer; + } + }); + + if (closestPlayer && closestDistance < 25) { + this.isAttacking = true; + const currentDirection = this.getPlayerDirection(); + const attackAnim = `attack${currentDirection}`; + + if (this.anims.exists(attackAnim)) { + // Play attack animation locally + this.player.play(attackAnim, true) + .once('animationcomplete', () => { + this.isAttacking = false; + const idleAnim = `idle${currentDirection}`; + if (this.anims.exists(idleAnim)) { + this.player.play(idleAnim, true); + } + }); + + // Emit attack event with target information + this.socket.emit('playerAttack', { + x: this.player.x, + y: this.player.y, + direction: currentDirection, + scene: 'BridgeScene', + targetId: closestPlayer.playerId + }); + + console.log('Attacking player:', closestPlayer.playerId, 'with animation:', attackAnim); + } + } + } + + setupSocketListeners() { + this.socket.on('currentPlayers', (players) => { + console.log('Current players in BridgeScene:', players); + + // Don't clear if we already have these players + const currentPlayerIds = new Set(Object.keys(players)); + const existingPlayerIds = new Set(Object.keys(this.playerWorldPosition)); + + // Only clear and recreate if the player sets are different + if (!this.areSetsEqual(currentPlayerIds, existingPlayerIds)) { + this.clearExistingPlayers(); + + Object.entries(players).forEach(([id, playerInfo]) => { + if (!this.existingPlayers.has(id)) { + console.log('Creating new player:', id); + if (id === this.socket.id) { + this.player = this.createPlayerWithListeners(id, playerInfo); + } else if (playerInfo.scene === 'BridgeScene') { + this.createPlayerWithListeners(id, playerInfo); + } + this.existingPlayers.add(id); + } + }); + + // After all players are created, check if we should start countdown + if (Object.keys(players).length >= 2) { + // Tell server we have enough players + this.socket.emit('bridgeReady', { + scene: 'BridgeScene', + players: Object.keys(players) + }); + } + } + }); + + // Listen for server's countdown signal + this.socket.on('bridgeCountdown', () => { + if (!this.countdownStarted) { + this.startCountdown(); + } + }); + + // Update other players more smoothly + this.socket.on('playerMovedInBridge', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + if (playerInfo.animation) { + otherPlayer.play(playerInfo.animation, true); } - } else if (this.cursors.down.isDown) { - newY += speed * (1 / 60); - console.log(this.level.isColliding(newX, newY)); - if (!this.level.isColliding(newX, newY)) { - console.log("not"); - this.player.body.setVelocityY(speed); - this.player.anims.play("walkDown", true); + otherPlayer.setFlipX(playerInfo.flipX || false); + } + }); + + this.socket.on('playerMoved', (playerInfo) => { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + if (otherPlayer) { + // Clear any existing tweens to prevent position conflicts + this.tweens.killTweensOf(otherPlayer); + + // Update position immediately + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + + // Update animation only if it's different + if (playerInfo.animation && (!otherPlayer.anims.currentAnim || + otherPlayer.anims.currentAnim.key !== playerInfo.animation)) { + otherPlayer.play(playerInfo.animation, true); } + + // Update flip state + otherPlayer.setFlipX(playerInfo.flipX); + + // Store latest info + otherPlayer.playerInfo = { + ...playerInfo, + lastUpdated: Date.now() + }; } + }); - // Normalize and scale the velocity so that player can't move faster along a diagonal - this.player.body.velocity.normalize().scale(speed); - - // If no movement keys are pressed, stop the animation - if ( - this.cursors.left.isUp && - this.cursors.right.isUp && - this.cursors.up.isUp && - this.cursors.down.isUp - ) { - this.player.anims.stop(); - - // Set idle animation based on the last direction - if (prevVelocity.x < 0) { - this.player.anims.play("idleRight", true); - this.player.flipX = true; - } else if (prevVelocity.x > 0) { - this.player.anims.play("idleRight", true); - this.player.flipX = false; - } else if (prevVelocity.y < 0) { - this.player.anims.play("idleUp", true); - } else if (prevVelocity.y > 0) { - this.player.anims.play("idleDown", true); + this.socket.on('playerAttacked', (attackInfo) => { + const attacker = attackInfo.playerId === this.socket.id ? + this.player : this.otherPlayers[attackInfo.playerId]; + + if (attacker && !attacker.isAttacking) { + const attackAnim = `attack${attackInfo.direction}`; + + if (this.anims.exists(attackAnim)) { + attacker.isAttacking = true; + + attacker.play(attackAnim, true) + .once('animationcomplete', () => { + attacker.isAttacking = false; + // Return to idle after attack + const idleAnim = `idle${attackInfo.direction}`; + if (this.anims.exists(idleAnim)) { + attacker.play(idleAnim, true); + } + }); } } + }); - // Update health bar position and width - if (this.healthBar) { - const yOffset = -20; - const width = 40; + this.socket.on('playerDamaged', (damageInfo) => { + console.log('Received damage info:', damageInfo); + const targetPlayer = damageInfo.playerId === this.socket.id ? + this.player : this.otherPlayers[damageInfo.playerId]; - this.healthBar.outline.x = this.player.x; - this.healthBar.outline.y = this.player.y + yOffset; - this.healthBar.background.x = this.player.x; - this.healthBar.background.y = this.player.y + yOffset; + if (targetPlayer) { + targetPlayer.life = damageInfo.newHealth; + if (targetPlayer.healthBar) { + targetPlayer.healthBar.update(); + } + } + }); + } + + shutdown() { + console.log('BridgeScene shutting down'); + this.cleanupSocketListeners(); + this.hasJoinedScene = false; + this.existingPlayers.clear(); + } + + createAnimations() { + const animations = [ + { key: 'idleDown', start: 0, end: 5 }, + { key: 'idleRight', start: 6, end: 11 }, + { key: 'idleUp', start: 12, end: 17 }, + { key: 'idleLeft', start: 18, end: 23 }, + { key: 'walkDown', start: 18, end: 23 }, + { key: 'walkRight', start: 24, end: 29 }, + { key: 'walkUp', start: 30, end: 35 }, + { key: 'attackDown', start: 36, end: 39 }, + { key: 'attackRight', start: 42, end: 45 }, + { key: 'attackUp', start: 48, end: 51 }, + { key: 'attackLeft', start: 52, end: 55 }, + { key: 'die', start: 54, end: 57 } + ]; + + animations.forEach(anim => { + if (!this.anims.exists(anim.key)) { + this.anims.create({ + key: anim.key, + frames: this.anims.generateFrameNumbers('player', { + start: anim.start, + end: anim.end + }), + frameRate: 10, + repeat: anim.key.startsWith('attack') || anim.key === 'die' ? 0 : -1 + }); + } + }); + } + + addOtherPlayer(playerInfo) { + if (!playerInfo) return null; + + try { + console.log('Adding other player:', playerInfo); + + // Destroy existing player if it exists + if (this.otherPlayers[playerInfo.playerId]) { + this.otherPlayers[playerInfo.playerId].destroy(); + } + + const otherPlayer = this.physics.add.sprite( + playerInfo.x || this.game.config.width / 2, + playerInfo.y || this.game.config.height / 2, + 'player' + ).setScale(1); + + // Set up physics body + otherPlayer.setBodySize(24, 28); + otherPlayer.setOffset(10, 13); + otherPlayer.setBounce(0.2); + otherPlayer.setCollideWorldBounds(true); + + // Enable physics but disable gravity + otherPlayer.body.setAllowGravity(false); + + // Set initial animation + if (this.anims.exists('idleDown')) { + otherPlayer.play('idleDown'); + } + + // Store player info + otherPlayer.playerInfo = playerInfo; + otherPlayer.playerId = playerInfo.playerId; + + // Add to tracking + this.otherPlayers[playerInfo.playerId] = otherPlayer; + + if (this.otherPlayersGroup) { + this.otherPlayersGroup.add(otherPlayer); + } + + console.log('Successfully added other player:', playerInfo.playerId); + return otherPlayer; + } catch (error) { + console.error('Error in addOtherPlayer:', error); + return null; + } + } + + removePlayer(playerId) { + this.otherPlayersGroup.getChildren().forEach((player) => { + if (player.playerId === playerId) { + player.destroy(); + } + }); + } + + createPlayerWithListeners(playerId, playerInfo) { + // Check if player already exists using the Set + if (this.existingPlayers.has(playerId)) { + console.log('Player already exists:', playerId); + return this.playerWorldPosition[playerId]?.sprite; + } + + console.log('Actually creating new player:', playerId); + const spawnPoint = this.getSpawnPoint(); + const playerSprite = this.physics.add.sprite( + spawnPoint.x, + spawnPoint.y, + 'player' + ).setScale(1); + + playerSprite.setBodySize(24, 28); + playerSprite.setOffset(10, 13); + playerSprite.setBounce(0.2); + playerSprite.setCollideWorldBounds(true); + playerSprite.body.setAllowGravity(false); + + playerSprite.play('idleDown'); + playerSprite.life = 100; // Make sure to set initial life + + this.playerWorldPosition[playerId] = { + x: spawnPoint.x, + y: spawnPoint.y, + sprite: playerSprite, + animation: 'idleDown', + flipX: false + }; + + // Only add to group if it exists + if (this.playersGroup) { + this.playersGroup.add(playerSprite); + } else { + console.warn('playersGroup not initialized'); + } + + this.existingPlayers.add(playerId); + + return playerSprite; + } + + handlePlayerCollision(player1, player2) { + // Handle player collision logic here + console.log('Players collided!'); + } + + sendPlayerUpdate() { + if (this.player && this.socket) { + const now = Date.now(); + const playerInfo = { + x: this.player.x, + y: this.player.y, + animation: this.player.anims.currentAnim?.key || 'idleDown', + flipX: this.player.flipX, + scene: 'BridgeScene' + }; + + // Only send update if enough time has passed and position/state has changed + if (now - this.lastMovementUpdate >= this.movementUpdateInterval && + this.hasPlayerStateChanged(playerInfo)) { - // Update red bar position and width - this.healthBar.bar.x = this.player.x - width/2; - this.healthBar.bar.y = this.player.y + yOffset; - this.healthBar.bar.width = (this.player.life / 100) * width; + this.socket.emit('playerMovement', playerInfo); + this.lastMovementUpdate = now; + this.lastPosition = { ...playerInfo }; + } + } + } + + updatePlayerInWorld(playerId, playerInfo) { + const playerData = this.playerWorldPosition[playerId]; + if (playerData?.sprite) { + // Update position + playerData.x = playerInfo.x; + playerData.y = playerInfo.y; + playerData.sprite.setPosition(playerInfo.x, playerInfo.y); + + // Update animation + if (playerInfo.animation && playerData.animation !== playerInfo.animation) { + playerData.animation = playerInfo.animation; + playerData.sprite.play(playerInfo.animation, true); } + + // Update flip + if (playerInfo.flipX !== undefined && playerData.flipX !== playerInfo.flipX) { + playerData.flipX = playerInfo.flipX; + playerData.sprite.setFlipX(playerInfo.flipX); + } + } + } + + clearExistingPlayers() { + console.log('Clearing existing players'); + Object.entries(this.playerWorldPosition).forEach(([playerId, playerData]) => { + if (playerData.sprite) { + playerData.sprite.destroy(); + } + }); + this.playerWorldPosition = {}; + if (this.playersGroup) { + this.playersGroup.clear(true, true); + } + this.existingPlayers.clear(); + } + + cleanupSocketListeners() { + if (this.socket) { + this.socket.removeAllListeners('currentPlayers'); + this.socket.removeAllListeners('newPlayer'); + this.socket.removeAllListeners('playerMoved'); + this.socket.removeAllListeners('playerCount'); + } + } + + // Helper method to compare Sets + areSetsEqual(set1, set2) { + if (set1.size !== set2.size) return false; + for (const item of set1) { + if (!set2.has(item)) return false; } + return true; + } + + getPlayerDirection() { + const currentAnim = this.player.anims.currentAnim; + if (!currentAnim) return 'Down'; + + // Check for attack animations first + if (currentAnim.key.includes('attack')) { + return currentAnim.key.replace('attack', ''); + } + + // Then check movement/idle animations + if (currentAnim.key.includes('Left')) return 'Left'; + if (currentAnim.key.includes('Right')) return 'Right'; + if (currentAnim.key.includes('Up')) return 'Up'; + return 'Down'; + } + + hasPlayerStateChanged(newState) { + const positionThreshold = 1; // Minimum movement to trigger update + return Math.abs(this.lastPosition.x - newState.x) > positionThreshold || + Math.abs(this.lastPosition.y - newState.y) > positionThreshold || + this.lastPosition.animation !== newState.animation || + this.lastPosition.flipX !== newState.flipX; + } } diff --git a/client/dungeonmap/src/CommonScene.js b/client/dungeonmap/src/CommonScene.js index e37db89..89cfa40 100644 --- a/client/dungeonmap/src/CommonScene.js +++ b/client/dungeonmap/src/CommonScene.js @@ -1,7 +1,8 @@ -import Phaser from 'phaser' +import { AnimationManager } from './managers/AnimationManager' +import { PlayerManager } from './managers/PlayerManager' +import { CombatManager} from './managers/CombatManager' import Level from './Level.js' -import { io } from 'socket.io-client' -const socketId = (import.meta.env.VITE_SOCKET_URL) +import BackgroundScene from './managers/backgroundscene' export default class CommonScene extends Phaser.Scene { constructor() { @@ -11,6 +12,16 @@ export default class CommonScene extends Phaser.Scene { this.level = new Level() this.spike = null this.lastEmitTime = 0 + this.socket = null + this.backgroundScene = null + this.chatBubbles = {} // Store chat bubbles for each player + } + + init(data) { + // Get socket from BackgroundScene + this.backgroundScene = this.scene.get('BackgroundScene'); + this.socket = this.backgroundScene.getSocket(); + console.log('Socket received from BackgroundScene:', this.socket.id); } preload() { @@ -41,6 +52,22 @@ export default class CommonScene extends Phaser.Scene { } create() { + this.backgroundScene = this.scene.get('BackgroundScene') + this.socket = this.backgroundScene.getSocket() + + if (!this.socket || !this.socket.connected) { + console.error('No socket connection available'); + return; + } + + console.log('Using socket from BackgroundScene:', this.socket.id); + + const spawnData = this.scene.settings.data; + if (spawnData && spawnData.x && spawnData.y) { + this.game.config.width = spawnData.x; + this.game.config.height = spawnData.y; + } + const map = this.make.tilemap({ key: 'common' }) const grass = map.addTilesetImage('Grass_Middle', 'Grass_Middle') const water = map.addTilesetImage('Water_Tile', 'Water_Tile') @@ -103,23 +130,65 @@ export default class CommonScene extends Phaser.Scene { .setOrigin(0, 0) .setCollisionByProperty({ collider: true }) - // const tileset2 = map.addTilesetImage("pillars", "pillars"); - // const layer3 = map.createLayer("pillars", tileset2, 0, 0); - // layer3.setScale(1, 1).setOrigin(0, 0); - // layer3.setCollisionByProperty({ collider: true }); + // Clear any existing labels first + this.children.list + .filter(child => child.type === 'Text') + .forEach(label => label.destroy()); + // Track positions where we've already placed labels + const labelPositions = new Set(); + // Function to check if position is too close to existing labels + const isTooClose = (x, y) => { + for (let pos of labelPositions) { + const [existingX, existingY] = pos.split(',').map(Number); + const distance = Math.sqrt(Math.pow(existingX - x, 2) + Math.pow(existingY - y, 2)); + if (distance < 50) { // Adjust this number to change how close labels can be + return true; + } + } + return false; + }; + + // Add permanent scene transition label for dungeon (just one) + const dungeonTiles = objectLayer.filterTiles(tile => tile.properties.dungeon); + if (dungeonTiles.length > 0) { + // Only use the first dungeon tile for the label + const tile = dungeonTiles[7]; + const posKey = `${tile.pixelX},${tile.pixelY}`; + this.add.text(tile.pixelX, tile.pixelY - 20, 'Battle (Dungeon)', { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '', + padding: { x: 5, y: 2 } + }).setOrigin(0.5); + labelPositions.add(posKey); + } + + // Add permanent scene transition label for bridge + const bridgeTiles = objectLayer.filterTiles(tile => tile.properties.bridge); + bridgeTiles.forEach(tile => { + const posKey = `${tile.pixelX},${tile.pixelY}`; + if (!isTooClose(tile.pixelX, tile.pixelY)) { + this.add.text(tile.pixelX + 16, tile.pixelY - 20, 'Battle (Bridge)', { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '', + padding: { x: 5, y: 2 } + }).setOrigin(0.5); + labelPositions.add(posKey); + } + }); - this.socket = io(socketId, { - withCredentials: false, - }) this.player = this.physics.add .sprite(this.game.config.width / 2, this.game.config.height / 2, 'player') .setScale(1) this.player.setCollideWorldBounds(true) this.player.life = 100 - this.player.attack = 10 + this.player.attack = 0 this.player.weapon = 'sword' this.player.setScale(1) // Scale the player sprite by 1.5 times this.player.setBodySize(24, 28) @@ -131,28 +200,30 @@ export default class CommonScene extends Phaser.Scene { frameRate: 10, repeat: -1, }) - let element = document.getElementById('input-box'); + let element = document.getElementById('input-box') const yesButton = document.getElementById('yes') const noButton = document.getElementById('no') this.physics.add.collider(this.player, layer1) this.physics.add.collider(this.player, objectLayer, (a, b) => { if (b?.properties?.dungeon) { element.style.display = 'block' - yesButton.addEventListener('click', ()=>{ - this.scene.start('DungeonScene') + yesButton.addEventListener('click', () => { + this.socket.emit('leaveCommonScene'); + this.scene.start('DungeonScene'); element.style.display = 'none' }) - noButton.addEventListener('click', ()=>{ + noButton.addEventListener('click', () => { element.style.display = 'none' }) } if (b?.properties?.bridge) { element.style.display = 'block' - yesButton.addEventListener('click', ()=>{ - this.scene.start('BridgeScene') + yesButton.addEventListener('click', () => { + this.socket.emit('leaveCommonScene'); + this.scene.start('BridgeScene'); element.style.display = 'none' }) - noButton.addEventListener('click', ()=>{ + noButton.addEventListener('click', () => { element.style.display = 'none' }) } @@ -161,7 +232,15 @@ export default class CommonScene extends Phaser.Scene { // this.cameras.main.setBounds(0, 0, +this.game.config.width, +this.game.config.height); this.cameras.main.startFollow(this.player, true) this.cameras.main.setFollowOffset(-50, -50) + + // Add other idle animations if missing + this.anims.create({ + key: 'idleLeft', + frames: this.anims.generateFrameNumbers('player', { start: 6, end: 11 }), + frameRate: 10, + repeat: -1, + }) this.anims.create({ key: 'idleRight', frames: this.anims.generateFrameNumbers('player', { start: 6, end: 11 }), @@ -197,33 +276,37 @@ export default class CommonScene extends Phaser.Scene { repeat: -1, }) + // Attack animations this.anims.create({ key: 'attackDown', - frames: this.anims.generateFrameNumbers('player', { start: 36, end: 39 }), + frames: this.anims.generateFrameNumbers('player', { start: 36, end: 41 }), frameRate: 10, - repeat: -1, - }) + repeat: 0 // Don't repeat attack animations + }); this.anims.create({ key: 'attackRight', - frames: this.anims.generateFrameNumbers('player', { start: 42, end: 46 }), + frames: this.anims.generateFrameNumbers('player', { start: 42, end: 47 }), frameRate: 10, - repeat: -1, - }) + repeat: 0 + }); this.anims.create({ key: 'attackUp', - frames: this.anims.generateFrameNumbers('player', { start: 48, end: 52 }), + frames: this.anims.generateFrameNumbers('player', { start: 48, end: 53 }), frameRate: 10, - repeat: -1, - }) + repeat: 0 + }); + // Attack Left uses the same frames as Attack Right but flipped this.anims.create({ - key: 'die', - frames: this.anims.generateFrameNumbers('player', { start: 54, end: 56 }), + key: 'attackLeft', + frames: this.anims.generateFrameNumbers('player', { start: 42, end: 47 }), frameRate: 10, - repeat: 0, - }) + repeat: 0 + }); + + AnimationManager.createAnimations(this) this.player.play('idleDown') this.handleSocketEvents() @@ -233,10 +316,163 @@ export default class CommonScene extends Phaser.Scene { ) // Add inventory key - this.inventoryKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.I); - + this.inventoryKey = this.input.keyboard.addKey( + Phaser.Input.Keyboard.KeyCodes.I + ) + // Create inventory (initially hidden) - this.createInventory(); + this.createInventory() + + // Add isAttacking flag + this.player.isAttacking = false + + // When colliding with portal + this.physics.add.overlap(this.player, this.portal, () => { + // Tell server we're leaving this scene + this.socket.emit('leaveCommonScene'); + + // Start BridgeScene + this.scene.start('BridgeScene'); + }); + + // Setup multiplayer events + this.socket.on('gameState', (state) => { + if (!state.players) return; + + Object.entries(state.players).forEach(([playerId, playerData]) => { + // Skip if it's not the current player and not in otherPlayers + if (playerId !== this.socket.id && !this.otherPlayers[playerId]) { + if (!this.reportedPlayers?.includes(playerId)) { + console.log('New player detected:', playerId); + this.reportedPlayers = [...(this.reportedPlayers || []), playerId]; + } + return; + } + + const player = playerId === this.socket.id ? this.player : this.otherPlayers[playerId]; + + if (!player || !player.anims) return; + + // Update position + player.x = Phaser.Math.Linear(player.x, playerData.x, 0.3); + player.y = Phaser.Math.Linear(player.y, playerData.y, 0.3); + + // Handle animations and direction + if (playerData.animation.includes('walk')) { + if (playerData.animation === 'walkLeft') { + player.play('walkLeft', true); + player.flipX = true; + player.lastDirection = 'Left'; + } else { + player.play(playerData.animation, true); + player.flipX = false; + player.lastDirection = playerData.animation.replace('walk', ''); + } + } else { + // Handle idle animations + if (player.lastDirection === 'Left') { + player.play('idleLeft', true); + player.flipX = true; + } else { + player.play(`idle${player.lastDirection || 'Down'}`, true); + player.flipX = false; + } + } + }); + }); + + // Join the scene + this.socket.emit('joinScene', { + scene: 'CommonScene', + x: this.game.config.width / 2, + y: this.game.config.height / 2 + }); + + // Create chat input + this.createChatInput(); + + // Listen for chat messages + this.socket.on('chatMessage', (data) => { + if (data.playerId !== this.socket.id) { // Don't add our own messages twice + addMessage(data.playerId, data.message); + } + }); + } + + update(time) { + console.log('Socket status:', this.socket?.connected); + console.log('Player exists:', !!this.player); + + if (!this.player || !this.socket) return; + + // Send input state to server + const input = { + left: this.cursors.left.isDown, + right: this.cursors.right.isDown, + up: this.cursors.up.isDown, + down: this.cursors.down.isDown, + attack: this.input.keyboard.addKey('SPACE').isDown // Add attack input + }; + + // Only send input updates when they change + if (JSON.stringify(input) !== JSON.stringify(this.lastInput)) { + this.socket.emit('playerInput', { + input: input + }); + this.lastInput = input; + } + + // Animation is now based on server-provided state + if (this.player.serverState) { + this.player.play(this.player.serverState.animation, true); + this.player.setFlipX(this.player.serverState.flipX); + } + } + + setupMultiplayerEvents() { + // Handle server-authoritative state updates + this.events.on('gameState', (state) => { + // Update all players based on server state + Object.keys(state.players).forEach((playerId) => { + const playerState = state.players[playerId]; + + if (playerId === this.socket.id) { + // Update local player + this.player.setPosition(playerState.x, playerState.y); + this.player.serverState = playerState; + } else { + // Update other players + if (!this.otherPlayers[playerId]) { + this.addOtherPlayer(playerState); + } else { + const otherPlayer = this.otherPlayers[playerId]; + otherPlayer.setPosition(playerState.x, playerState.y); + otherPlayer.play(playerState.animation, true); + otherPlayer.setFlipX(playerState.flipX); + } + } + }); + }); + + // Handle server-validated scene transitions + this.events.on('sceneTransition', (data) => { + if (data.playerId === this.socket.id) { + this.backgroundScene.events.emit('switchScene', data); + } + }); + + // Other event handlers remain similar but wait for server validation + } + + handleAttack() { + // Send attack intent to server instead of handling locally + this.socket.emit('attackIntent', { + scene: 'CommonScene', + position: { + x: this.player.x, + y: this.player.y + } + }); } handleSocketEvents() { @@ -246,6 +482,7 @@ export default class CommonScene extends Phaser.Scene { Object.keys(players).forEach((id) => { if (players[id].playerId === socket.id) { this.player.setPosition(players[id].x, players[id].y) + this.player.lastDirection = players[id].lastDirection } else { this.addOtherPlayer(players[id]) } @@ -258,13 +495,47 @@ export default class CommonScene extends Phaser.Scene { }) socket.on('playerMoved', (playerInfo) => { - if (this.otherPlayers[playerInfo.playerId]) { - var otherPlayer = this.otherPlayers[playerInfo.playerId]; - otherPlayer.setPosition(playerInfo.x, playerInfo.y); - otherPlayer.anims.play(playerInfo.animation, true); - otherPlayer.flipX = playerInfo.flipX; - } - }) + if (!playerInfo || !playerInfo.playerId) return; + + // Handle local player + if (playerInfo.playerId === this.socket.id) { + if (this.player && this.player.anims && !this.player.isAttacking) { + try { + this.player.anims.play(playerInfo.animation, true); + this.player.flipX = playerInfo.flipX; + this.player.lastDirection = playerInfo.lastDirection; + } catch (error) { + console.warn('Local player animation error:', error); + } + } + } + // Handle other players + else if (this.otherPlayers && this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + + // Update position + if (otherPlayer && typeof otherPlayer.setPosition === 'function') { + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + } + + // Update animation + if (otherPlayer && otherPlayer.anims && !otherPlayer.isAttacking) { + try { + if (playerInfo.animation && this.anims.exists(playerInfo.animation)) { + otherPlayer.anims.play(playerInfo.animation, true); + } + if (typeof playerInfo.flipX !== 'undefined') { + otherPlayer.flipX = playerInfo.flipX; + } + if (playerInfo.lastDirection) { + otherPlayer.lastDirection = playerInfo.lastDirection; + } + } catch (error) { + console.warn('Other player animation error:', error); + } + } + } + }); socket.on('playerAttacked', (data) => { if (this.otherPlayers[data.target]) { @@ -272,13 +543,26 @@ export default class CommonScene extends Phaser.Scene { console.log('Player attacked:', data) } }) + socket.on('playerAttackAnimation', (data) => { + console.log('Received attack animation:', data) + + if (data.attacker === this.socket.id) { + return + } + const otherPlayer = this.otherPlayers[data.attacker] + if (otherPlayer) { + PlayerManager.handleOtherPlayerAttack( + otherPlayer, + data.animation, + data.direction + ) + } + }) socket.on('playerDefeated', (playerId) => { - if (this.otherPlayers[playerId]) { - this.otherPlayers[playerId].destroy() - delete this.otherPlayers[playerId] + CombatManager.handlePlayerDeath(this, playerId) console.log('Player defeated:', playerId) - } + }) socket.on('playerDisconnected', (playerId) => { @@ -290,142 +574,160 @@ export default class CommonScene extends Phaser.Scene { } addOtherPlayer(playerInfo) { - const otherPlayer = this.physics.add.sprite( - playerInfo.x, - playerInfo.y, - 'player' - ) - otherPlayer.playerId = playerInfo.playerId - otherPlayer.life = playerInfo.life - otherPlayer.attack = playerInfo.attack - otherPlayer.weapon = playerInfo.weapon - this.otherPlayers[playerInfo.playerId] = otherPlayer - console.log('Added other player:', playerInfo) + try { + const otherPlayer = this.physics.add.sprite( + playerInfo.x || 400, + playerInfo.y || 300, + 'player' + ); + + otherPlayer.setScale(1); + otherPlayer.playerId = playerInfo.playerId; + otherPlayer.life = playerInfo.life || 100; + otherPlayer.attack = playerInfo.attack || 10; + otherPlayer.setCollideWorldBounds(true); + otherPlayer.setBodySize(24, 28); + otherPlayer.setOffset(10, 13); + + if (this.layer1) { + this.physics.add.collider(otherPlayer, this.layer1); + } + + this.otherPlayers[playerInfo.playerId] = otherPlayer; + otherPlayer.play('idleDown'); + + console.log('Added player:', playerInfo.playerId); + return otherPlayer; + + } catch (error) { + console.error('Error in addOtherPlayer:', error); + return null; + } } createInventory() { // Create inventory container - const padding = 10; - const cellSize = 40; - const rows = 4; - const cols = 6; - const width = (cellSize * cols) + (padding * 2); - const height = (cellSize * rows) + (padding * 2); - + const padding = 10 + const cellSize = 40 + const rows = 4 + const cols = 6 + const width = cellSize * cols + padding * 2 + const height = cellSize * rows + padding * 2 + // Position in center of screen - const x = this.cameras.main.centerX - width/2; - const y = this.cameras.main.centerY - height/2; - + const x = this.cameras.main.centerX - width / 2 + const y = this.cameras.main.centerY - height / 2 + // Create semi-transparent background - this.inventoryBg = this.add.rectangle(x, y, width, height, 0x000000) - .setOrigin(0, 0) - .setAlpha(0.7) - .setScrollFactor(0) - .setDepth(100); - + this.inventoryBg = this.add + .rectangle(x, y, width, height, 0x000000) + .setOrigin(0, 0) + .setAlpha(0.7) + .setScrollFactor(0) + .setDepth(1000); + // Create grid cells - this.inventorySlots = []; + this.inventorySlots = [] for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const slotX = x + padding + (col * cellSize); - const slotY = y + padding + (row * cellSize); - - // Create slot background - const slot = this.add.rectangle(slotX, slotY, cellSize - 2, cellSize - 2, 0x666666) - .setOrigin(0, 0) - .setAlpha(0.8) - .setScrollFactor(0) - .setDepth(101); - - this.inventorySlots.push(slot); - } + for (let col = 0; col < cols; col++) { + const slotX = x + padding + col * cellSize + const slotY = y + padding + row * cellSize + + // Create slot background + const slot = this.add + .rectangle(slotX, slotY, cellSize - 2, cellSize - 2, 0x666666) + .setOrigin(0, 0) + .setAlpha(0.8) + .setScrollFactor(0) + .setDepth(1001); + + this.inventorySlots.push(slot) + } } - + // Hide inventory initially - this.hideInventory(); + this.hideInventory() } hideInventory() { - this.inventoryBg.setVisible(false); - this.inventorySlots.forEach(slot => slot.setVisible(false)); + this.inventoryBg.setVisible(false) + this.inventorySlots.forEach((slot) => slot.setVisible(false)) } showInventory() { - this.inventoryBg.setVisible(true); - this.inventorySlots.forEach(slot => slot.setVisible(true)); + this.inventoryBg.setVisible(true) + this.inventorySlots.forEach((slot) => slot.setVisible(true)) } update(time, delta) { - const speed = 80 - const prevVelocity = this.player.body.velocity.clone() - let newX = this.player.x - let newY = this.player.y + if (!this.player) return; + + // Update depths based on Y position, but keep lower than UI elements + this.player.setDepth(this.player.y + 100); // Base player depth on Y position + + Object.values(this.otherPlayers).forEach(otherPlayer => { + otherPlayer.setDepth(otherPlayer.y + 100); // Same for other players + }); - // Stop any previous movement from the last frame - this.player.body.setVelocity(0) + const speed = 80; + let animation = 'idleDown'; // Change default idle animation - // Stop any previous movement - this.player.body.setVelocity(0) - - let animation = this.lastDirection ? 'idle' + this.lastDirection : 'idleDown' - - // Handle movement and set last direction - if (this.cursors.left.isDown) { - this.player.body.setVelocityX(-speed) - animation = 'walkRight' - this.lastDirection = 'Right' - this.player.flipX = true - } else if (this.cursors.right.isDown) { - this.player.body.setVelocityX(speed) - animation = 'walkRight' - this.lastDirection = 'Right' - this.player.flipX = false - } + this.player.body.setVelocity(0); - if (this.cursors.up.isDown) { - this.player.body.setVelocityY(-speed) - animation = 'walkUp' - this.lastDirection = 'Up' - } else if (this.cursors.down.isDown) { - this.player.body.setVelocityY(speed) - animation = 'walkDown' - this.lastDirection = 'Down' - } + if (!this.player.isAttacking) { + // Track last pressed direction + if (this.cursors.left.isDown) { + this.player.body.setVelocityX(-speed); + animation = 'walkLeft'; + this.player.lastDirection = 'Left'; + this.player.flipX = true; + } else if (this.cursors.right.isDown) { + this.player.body.setVelocityX(speed); + animation = 'walkRight'; + this.player.lastDirection = 'Right'; + this.player.flipX = false; + } - // Normalize and scale the velocity - this.player.body.velocity.normalize().scale(speed) + if (this.cursors.up.isDown) { + this.player.body.setVelocityY(-speed); + animation = 'walkUp'; + this.player.lastDirection = 'Up'; + } else if (this.cursors.down.isDown) { + this.player.body.setVelocityY(speed); + animation = 'walkDown'; + this.player.lastDirection = 'Down'; + } - // Handle idle animations based on last direction - if (!this.cursors.left.isDown && - !this.cursors.right.isDown && - !this.cursors.up.isDown && - !this.cursors.down.isDown) { - - if (this.lastDirection) { - animation = 'idle' + this.lastDirection + // If no movement keys are pressed, play the correct idle animation + if (this.player.body.velocity.x === 0 && this.player.body.velocity.y === 0) { + animation = `idle${this.player.lastDirection || 'Down'}`; } - } - // Play the animation - this.player.play(animation, true) + // Play the animation + if (this.anims.exists(animation)) { // Check if animation exists before playing + this.player.anims.play(animation, true); + } + } // Emit movement to server - if (time - this.lastEmitTime > 5) { - this.socket.emit('movePlayer', { + if (time - this.lastEmitTime > 16) { + this.socket.emit('playerInput', { x: this.player.x, y: this.player.y, animation: animation, flipX: this.player.flipX, - lastDirection: this.lastDirection - }) - this.lastEmitTime = time + lastDirection: this.lastDirection, + }); + this.lastEmitTime = time; } + + // Handle attack if (Phaser.Input.Keyboard.JustDown(this.attackKey)) { - this.handleAttack() + this.handleAttack(); } - // Add inventory toggle at the end of update + // Handle inventory toggle if (Phaser.Input.Keyboard.JustDown(this.inventoryKey)) { if (this.inventoryBg.visible) { this.hideInventory(); @@ -433,31 +735,341 @@ export default class CommonScene extends Phaser.Scene { this.showInventory(); } } + + // Update health bar + if (this.healthBar) { + const yOffset = -20; + const width = 40; + + this.healthBar.outline.x = this.player.x; + this.healthBar.outline.y = this.player.y + yOffset; + this.healthBar.background.x = this.player.x; + this.healthBar.background.y = this.player.y + yOffset; + + this.healthBar.bar.x = this.player.x - width/2; + this.healthBar.bar.y = this.player.y + yOffset; + this.healthBar.bar.width = (this.player.life / 100) * width; + } } handleAttack() { // Find the closest player to attack - let closestPlayer = null - let closestDistance = Infinity + let closestPlayer = null; + let closestDistance = Infinity; Object.keys(this.otherPlayers).forEach((id) => { - const otherPlayer = this.otherPlayers[id] - const distance = Phaser.Math.Distance.Between( - this.player.x, - this.player.y, - otherPlayer.x, - otherPlayer.y - ) - if (distance < closestDistance) { - closestDistance = distance - closestPlayer = otherPlayer + const otherPlayer = this.otherPlayers[id]; + const distance = Phaser.Math.Distance.Between( + this.player.x, + this.player.y, + otherPlayer.x, + otherPlayer.y + ); + if (distance < closestDistance) { + closestDistance = distance; + closestPlayer = otherPlayer; + } + }); + + if (closestPlayer && closestDistance < 25) { + // Get the current direction from the player's last movement + const direction = this.player.lastDirection || 'Down'; + + // Set the attack animation based on direction + const attackAnim = `attack${direction}`; + + // Pass both the animation and direction to handleAttack + PlayerManager.handleAttack(this, this.player, closestPlayer.playerId, attackAnim); + console.log('Attacking player:', closestPlayer.playerId, 'with animation:', attackAnim); + } + } + + renderGameState(state) { + Object.entries(state.players).forEach(([playerId, playerData]) => { + if (playerId === this.socket.id) { + this.renderPlayer(playerData); + } else { + this.renderOtherPlayer(playerId, playerData); } - }) + }); + } + + renderPlayer(playerData) { + if (!this.player) { + this.player = this.add.sprite(playerData.x, playerData.y, 'player'); + } + this.player.setPosition(playerData.x, playerData.y); + this.player.flipX = playerData.flipX; + if (playerData.animation) { + this.player.play(playerData.animation, true); + } + } - if (closestPlayer && closestDistance < 20) { - // Adjust attack range as needed - this.socket.emit('attackPlayer', closestPlayer.playerId) - console.log('Attacking player:', closestPlayer.playerId) + renderOtherPlayer(playerId, playerData) { + if (!this.otherPlayers[playerId]) { + this.otherPlayers[playerId] = this.add.sprite(playerData.x, playerData.y, 'player'); + } + const otherPlayer = this.otherPlayers[playerId]; + otherPlayer.setPosition(playerData.x, playerData.y); + otherPlayer.flipX = playerData.flipX; + if (playerData.animation) { + otherPlayer.play(playerData.animation, true); + } + } + + createChatInput() { + // Check if chat already exists, if so, return early + if (document.getElementById('game-chat-container')) { + return; + } + + // Add bad words filter + const badWords = [ + 'fuck', 'shit', 'ass', 'bitch', 'dick', 'pussy', 'cunt', + 'bastard', 'damn', 'piss', 'cock', 'slut', 'whore', + // Add more words as needed + ]; + + // Function to filter bad words + const filterMessage = (message) => { + let filteredMessage = message.toLowerCase(); + badWords.forEach(word => { + // Create a regular expression that matches the word with possible special characters + const regex = new RegExp(word.split('').join('[^a-zA-Z]*'), 'gi'); + // Replace bad word with asterisks + filteredMessage = filteredMessage.replace(regex, '*'.repeat(word.length)); + }); + return filteredMessage; + }; + + // Create chat container + const chatContainer = document.createElement('div'); + chatContainer.style.position = 'absolute'; + chatContainer.style.bottom = '65px'; + chatContainer.style.right = '20px'; + chatContainer.style.width = '260px'; + chatContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + chatContainer.style.borderRadius = '5px'; + chatContainer.style.padding = '5px'; + chatContainer.style.zIndex = '1000'; + chatContainer.style.minHeight = '30px'; // Added minimum height + + // Create minimize button + const minimizeButton = document.createElement('button'); + minimizeButton.textContent = '−'; + minimizeButton.style.position = 'absolute'; + minimizeButton.style.right = '5px'; + minimizeButton.style.top = '5px'; + minimizeButton.style.padding = '0px 6px'; + minimizeButton.style.backgroundColor = 'transparent'; + minimizeButton.style.border = '1px solid white'; + minimizeButton.style.color = 'white'; + minimizeButton.style.cursor = 'pointer'; + minimizeButton.style.fontSize = '20px'; // Increased font size + minimizeButton.style.lineHeight = '20px'; // Centered the symbol vertically + chatContainer.appendChild(minimizeButton); + + // Create messages container + const messagesContainer = document.createElement('div'); + messagesContainer.style.height = '200px'; + messagesContainer.style.overflowY = 'auto'; + messagesContainer.style.marginTop = '25px'; // Space for minimize button + messagesContainer.style.color = 'white'; + messagesContainer.style.fontSize = '14px'; + messagesContainer.style.wordBreak = 'break-word'; + chatContainer.appendChild(messagesContainer); + + // Create chat input element + const chatInput = document.createElement('input'); + chatInput.type = 'text'; + chatInput.placeholder = 'Type message...'; + chatInput.style.position = 'absolute'; + chatInput.style.bottom = '20px'; + chatInput.style.right = '80px'; + chatInput.style.width = '200px'; + chatInput.style.padding = '5px'; + chatInput.style.zIndex = '1000'; + chatInput.style.height = '25px'; + + // Create send button + const sendButton = document.createElement('button'); + sendButton.textContent = 'Send'; + sendButton.style.position = 'absolute'; + sendButton.style.bottom = '20px'; + sendButton.style.right = '20px'; + sendButton.style.padding = '5px 10px'; + sendButton.style.zIndex = '1000'; + sendButton.style.height = '25px'; + sendButton.style.verticalAlign = 'top'; + + // Update chat input styling + chatInput.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + chatInput.style.color = 'white'; + chatInput.style.border = '1px solid rgba(255, 255, 255, 0.3)'; + chatInput.style.borderRadius = '5px'; + chatInput.style.padding = '5px 10px'; + chatInput.style.outline = 'none'; + chatInput.style.width = '200px'; + + // Update send button styling to match + sendButton.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; + sendButton.style.color = 'white'; + sendButton.style.border = '1px solid rgba(255, 255, 255, 0.3)'; + sendButton.style.borderRadius = '5px'; + sendButton.style.padding = '5px 10px'; + sendButton.style.cursor = 'pointer'; + + // Add to document + document.body.appendChild(chatContainer); + document.body.appendChild(chatInput); + document.body.appendChild(sendButton); + + // Minimize functionality + let isMinimized = false; + minimizeButton.addEventListener('click', () => { + isMinimized = !isMinimized; + messagesContainer.style.display = isMinimized ? 'none' : 'block'; + minimizeButton.textContent = isMinimized ? '+' : '−'; + chatContainer.style.height = isMinimized ? 'auto' : 'auto'; + }); + + // Remove the duplicate sendMessage function and keep just one + const sendMessage = () => { + const message = chatInput.value.trim(); + if (message) { + const filteredMessage = filterMessage(message); + // Send message with player ID + this.socket.emit('chatMessage', { + playerId: this.socket.id, + message: filteredMessage + }); + chatInput.value = ''; + chatInput.blur(); + this.input.keyboard.enabled = true; + } + }; + + // Update message display function to use filter for incoming messages + const addMessage = (socketId, message) => { + const messageElement = document.createElement('div'); + messageElement.style.marginBottom = '5px'; + messageElement.style.padding = '3px'; + + // Highlight our own messages + if (socketId === this.socket.id) { + messageElement.innerHTML = `[${socketId}]: ${message}`; // Green color for own messages + } else { + messageElement.innerHTML = `[${socketId}]: ${message}`; // Grey for others + } + + messagesContainer.appendChild(messageElement); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + }; + + // Disable game input when chat is focused + chatInput.addEventListener('focus', (e) => { + e.stopPropagation(); + this.input.keyboard.enabled = false; + }); + + chatInput.addEventListener('blur', (e) => { + e.stopPropagation(); + this.input.keyboard.enabled = true; + }); + + // Event listeners + sendButton.addEventListener('click', sendMessage); + + chatInput.addEventListener('keydown', (e) => { + e.stopPropagation(); + if (e.key === 'Enter') { + sendMessage(); + } + }); + + // Store references for cleanup + this.chatInput = chatInput; + this.sendButton = sendButton; + this.chatContainer = chatContainer; + + // Add chat message handler if not already in handleSocketEvents + this.socket.on('chatMessage', (data) => { + console.log('Received chat message:', data); // Debug log + addMessage(data.playerId, data.message); + }); + } + + showChatBubble(playerId, message) { + // Remove existing bubble for this player if it exists + if (this.chatBubbles[playerId]) { + this.chatBubbles[playerId].destroy(); + } + + // Get player position + const player = playerId === this.socket.id ? this.player : this.otherPlayers[playerId]; + if (!player) return; + + // Create background rectangle + const padding = 0; + const bubbleWidth = 150; + const bubbleHeight = 40; + + const bubble = this.add.container(player.x, player.y - 60); + + // Add semi-transparent background + const background = this.add.rectangle( + 0, + 0, + bubbleWidth, + bubbleHeight, + 0x000000, + 0.5 // Alpha value for transparency + ); + + // Add text + const text = this.add.text( + 0, + 0, + message, + { + fontSize: '14px', + color: '#ffffff', + align: 'center', + wordWrap: { width: bubbleWidth - padding * 2 } + } + ); + + // Center text in bubble + text.setPosition( + -text.width / 2, + -text.height / 2 + ); + + // Add to container + bubble.add([background, text]); + + // Store bubble reference + this.chatBubbles[playerId] = bubble; + + // Destroy bubble after 5 seconds + this.time.delayedCall(5000, () => { + if (this.chatBubbles[playerId] === bubble) { + bubble.destroy(); + delete this.chatBubbles[playerId]; + } + }); + } + + // Add cleanup in scene shutdown + shutdown() { + if (this.chatInput) { + this.chatInput.remove(); + } + if (this.sendButton) { + this.sendButton.remove(); + } + if (this.chatContainer) { + this.chatContainer.remove(); } } } diff --git a/client/dungeonmap/src/DungeonScene.js b/client/dungeonmap/src/DungeonScene.js index 84eb2bb..bad63be 100644 --- a/client/dungeonmap/src/DungeonScene.js +++ b/client/dungeonmap/src/DungeonScene.js @@ -1,5 +1,12 @@ import Phaser from "phaser"; import Level from "./Level.js"; +import BackgroundScene from './managers/backgroundscene' +import { PlayerManager } from './managers/PlayerManager' +import { AnimationManager } from './managers/AnimationManager' +import { CombatManager } from './managers/CombatManager' +import { io } from 'socket.io-client' + + export default class DungeonScene extends Phaser.Scene { constructor() { @@ -8,6 +15,39 @@ export default class DungeonScene extends Phaser.Scene { this.level = new Level(); this.spike = null; this.healthBar = null; + this.otherPlayers = {}; + this.canMove = false; + this.countdownText = null; + this.countdownStarted = false; + this.socket = null; + this.backgroundScene = null; + this.animationsCreated = false; // Track if animations are created + this.otherPlayersGroup = null; + this.playerWorldPosition = {}; + this.playersGroup = null; + this.playerEventListeners = new Map(); + this.localPlayerId = null; // Track local player ID + this.hasJoinedScene = false; // Track if we've already joined + this.initializedPlayers = new Set(); // Track which players we've initialized + this.existingPlayers = new Set(); // Track existing player IDs + this.isAttacking = false; // Add attack state tracking + this.lastMovementUpdate = 0; + this.movementUpdateInterval = 50; // Update every 50ms + this.lastPosition = { x: 0, y: 0, animation: '', flipX: false }; + } + + init(data) { + console.log('DungeonScene init with data:', data); + // Only set socket once + if (!this.socket) { + if (data && data.socket) { + this.socket = data.socket; + } else { + this.backgroundScene = this.scene.get('BackgroundScene'); + this.socket = this.backgroundScene.getSocket(); + } + console.log('Socket initialized:', this.socket.id); + } } preload() { @@ -29,7 +69,13 @@ export default class DungeonScene extends Phaser.Scene { } create() { -this.game.scale.resize(256,256) + console.log('DungeonScene create starting'); + + // Initialize groups first, before any other creation logic + this.playersGroup = this.add.group(); + this.otherPlayersGroup = this.add.group(); + + this.game.scale.resize(256,256) const map = this.make.tilemap({ key: "dungeon" }); const floor = map.addTilesetImage("Dungeon_1", "Dungeon_1"); const floorLayer = map.createLayer("Tile Layer 1", [floor], 0, -100); @@ -79,80 +125,363 @@ this.game.scale.resize(256,256) this.player.setBodySize(24, 28); this.player.setOffset(10, 13); this.cursors = this.input.keyboard.createCursorKeys(); - this.anims.create({ - key: "idleDown", - frames: this.anims.generateFrameNumbers("player", { start: 0, end: 5 }), - frameRate: 10, - repeat: -1, + + this.player.play("idleDown"); + this.healthBar = this.createHealthBar(this.player.x, this.player.y, this.player); + + + + // Add leave button + const leaveButton = this.add.text( + this.cameras.main.width - 2, + this.cameras.main.height - 2, + 'Quit Match', + { + fontFamily: 'Verdana', + fontSize: '10px', + fill: '#fff', + backgroundColor: '#000', + padding: { x: 5, y: 2 } + } + ) + .setOrigin(1, 1) + .setScrollFactor(0) + .setInteractive() + .setDepth(1000); + + leaveButton.on('pointerdown', () => { + console.log('Returning to common map'); + + if (this.socket) { + this.socket.emit('playerLeftScene', { + playerId: this.socket.id, + from: 'DungeonScene', + to: 'CommonScene' + }); + } + + // Reset game scale before transitioning + this.game.scale.resize(800, 600); // Set to CommonScene dimensions + + this.scene.start('CommonScene', { + x: 620, + y: 360 + }); }); - this.physics.add.collider(this.player, pillarLayer); - this.physics.add.collider(this.player, objectLayer); + // Add waiting text instead of starting countdown immediately + this.countdownText = this.add.text( + this.cameras.main.width / 2, + this.cameras.main.height / 2, + 'In Queue...', + { + fontSize: '32px', + fill: '#fff', + stroke: '#000', + strokeThickness: 4 + } + ) + .setOrigin(0.5) + .setScrollFactor(0) + .setDepth(1000); + + // Listen for player join/leave events + this.socket.on('playerJoined', () => this.checkPlayersAndStartCountdown()); + this.socket.on('playerLeft', () => this.checkPlayersAndStartCountdown()); + + // Create animations using AnimationManager + AnimationManager.createAnimations(this); - this.anims.create({ - key: "idleRight", - frames: this.anims.generateFrameNumbers("player", { start: 6, end: 11 }), - frameRate: 10, - repeat: -1, + // Setup socket events if socket exists + if (this.socket) { + // Remove any existing listeners first + this.socket.removeAllListeners('gameState'); + this.socket.removeAllListeners('playerDisconnected'); + + // Add new listeners + this.socket.on('gameState', (state) => { + if (state && state.players) { + Object.entries(state.players).forEach(([playerId, playerData]) => { + if (playerId !== this.socket.id && !this.otherPlayers[playerId]) { + // Add new player + this.addOtherPlayer(playerData); + } else if (this.otherPlayers[playerId]) { + // Update existing player + const otherPlayer = this.otherPlayers[playerId]; + otherPlayer.x = playerData.x; + otherPlayer.y = playerData.y; + if (playerData.animation) { + otherPlayer.play(playerData.animation, true); + } + } + }); + } + }); + + this.socket.on('playerDisconnected', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } + }); + + // Join the scene + this.socket.emit('joinScene', { + scene: 'DungeonScene', + x: this.game.config.width / 2, + y: this.game.config.height / 2 + }); + } + + // Add portal collision handler + this.physics.add.overlap(this.player, this.portal, () => { + // Tell server we're leaving this scene + this.socket.emit('leaveDungeonScene'); + + // Start next scene + this.scene.start('CommonScene'); // or whatever scene you're transitioning to }); - this.anims.create({ - key: "idleUp", - frames: this.anims.generateFrameNumbers("player", { start: 12, end: 17 }), - frameRate: 10, - repeat: -1, + // Initialize background scene + if (!this.scene.isActive('BackgroundScene')) { + this.scene.launch('BackgroundScene'); + } + + this.backgroundScene = this.scene.get('BackgroundScene'); + this.socket = this.backgroundScene.getSocket(); + + // Tell background scene we're here + this.backgroundScene.events.emit('changeScene', 'DungeonScene'); + + // Listen for player updates + this.backgroundScene.events.on('playerUpdated', ({ playerId, playerInfo, isNew, isMovement }) => { + if (playerId === this.socket.id) { + if (isNew) this.addPlayer(playerInfo); + } else { + if (isNew) { + this.addOtherPlayer(playerInfo); + } else if (isMovement) { + this.updateOtherPlayer(playerId, playerInfo); + } + } + }); + + // Listen for player removals + this.backgroundScene.events.on('playerRemoved', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } }); - this.anims.create({ - key: "walkDown", - frames: this.anims.generateFrameNumbers("player", { start: 18, end: 23 }), - frameRate: 10, - repeat: -1, + this.otherPlayersGroup = this.add.group(); + + // Add these socket listeners + this.socket.on('currentPlayers', (players) => { + console.log('Current players in DungeonScene:', players); + + // Don't clear if we already have these players + const currentPlayerIds = new Set(Object.keys(players)); + const existingPlayerIds = new Set(Object.keys(this.playerWorldPosition)); + + // Only clear and recreate if the player sets are different + if (!this.areSetsEqual(currentPlayerIds, existingPlayerIds)) { + this.clearExistingPlayers(); + + Object.entries(players).forEach(([id, playerInfo]) => { + if (!this.existingPlayers.has(id)) { + console.log('Creating new player:', id); + if (id === this.socket.id) { + this.player = this.createPlayerWithListeners(id, playerInfo); + } else if (playerInfo.scene === 'DungeonScene') { + this.createPlayerWithListeners(id, playerInfo); + } + this.existingPlayers.add(id); + } + }); + + // After all players are created, check if we should start countdown + if (Object.keys(players).length >= 2) { + // Tell server we have enough players + this.socket.emit('dungeonReady', { + scene: 'DungeonScene', + players: Object.keys(players) + }); + } + } }); - this.anims.create({ - key: "walkRight", - frames: this.anims.generateFrameNumbers("player", { start: 24, end: 29 }), - frameRate: 10, - repeat: -1, + this.socket.on('newPlayer', (playerInfo) => { + console.log('New player joined:', playerInfo); + if (playerInfo.playerId !== this.localPlayerId && playerInfo.scene === 'DungeonScene') { + this.createPlayerWithListeners(playerInfo.playerId, playerInfo); + } }); - this.anims.create({ - key: "walkUp", - frames: this.anims.generateFrameNumbers("player", { start: 30, end: 35 }), - frameRate: 10, - repeat: -1, + this.socket.on('playerMovedInDungeon', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + if (playerInfo.animation) { + otherPlayer.play(playerInfo.animation, true); + } + otherPlayer.setFlipX(playerInfo.flipX || false); + } }); - this.anims.create({ - key: "attackDown", - frames: this.anims.generateFrameNumbers("player", { start: 36, end: 39 }), - frameRate: 10, - repeat: 1, + this.socket.on('playerDisconnected', (playerId) => { + if (this.otherPlayers[playerId]) { + this.otherPlayers[playerId].destroy(); + delete this.otherPlayers[playerId]; + } }); - this.anims.create({ - key: "attackRight", - frames: this.anims.generateFrameNumbers("player", { start: 42, end: 46 }), - frameRate: 10, - repeat: -1, + // Listen for server's countdown signal + this.socket.on('dungeonCountdown', () => { + if (!this.countdownStarted) { + this.startCountdown(); + } }); - this.anims.create({ - key: "attackUp", - frames: this.anims.generateFrameNumbers("player", { start: 48, end: 52 }), - frameRate: 10, - repeat: -1, + // Store local player ID + this.localPlayerId = this.socket.id; + console.log('Local player ID:', this.localPlayerId); + + // Clean up old listeners before adding new ones + this.cleanupSocketListeners(); + + // Set up socket listeners + this.socket.on('currentPlayers', (players) => { + console.log('Current players received:', Object.keys(players)); + + // Clear existing players first + this.clearExistingPlayers(); + + Object.entries(players).forEach(([id, playerInfo]) => { + if (!this.initializedPlayers.has(id)) { + console.log('Initializing player:', id); + if (id === this.localPlayerId) { + console.log('Creating local player'); + this.player = this.createPlayerWithListeners(id, playerInfo); + } else if (playerInfo.scene === 'DungeonScene') { + console.log('Creating remote player'); + this.createPlayerWithListeners(id, playerInfo); + } + this.initializedPlayers.add(id); + } else { + console.log('Player already initialized:', id); + } + }); }); - this.anims.create({ - key: "die", - frames: this.anims.generateFrameNumbers("player", { start: 54, end: 56 }), - frameRate: 10, - repeat: 0, + this.socket.on('newPlayer', (playerInfo) => { + console.log('New player joined:', playerInfo); + if (playerInfo.playerId !== this.localPlayerId && playerInfo.scene === 'DungeonScene') { + this.createPlayerWithListeners(playerInfo.playerId, playerInfo); + } }); - this.player.play("idleDown"); - this.healthBar = this.createHealthBar(this.player.x, this.player.y, this.player); + + this.socket.on('playerMoved', (playerInfo) => { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + if (otherPlayer) { + // Clear any existing tweens to prevent position conflicts + this.tweens.killTweensOf(otherPlayer); + + // Update position immediately + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + + // Update animation only if it's different + if (playerInfo.animation && (!otherPlayer.anims.currentAnim || + otherPlayer.anims.currentAnim.key !== playerInfo.animation)) { + otherPlayer.play(playerInfo.animation, true); + } + + // Update flip state + otherPlayer.setFlipX(playerInfo.flipX); + + // Store latest info + otherPlayer.playerInfo = { + ...playerInfo, + lastUpdated: Date.now() + }; + } + }); + + // Only emit join if we haven't already + if (!this.hasJoinedScene) { + console.log('Emitting first-time join for player:', this.localPlayerId); + this.socket.emit('joinScene', { + scene: 'DungeonScene', + playerId: this.localPlayerId, + x: this.getSpawnPoint().x, + y: this.getSpawnPoint().y, + animation: 'idleDown', + flipX: false + }); + this.hasJoinedScene = true; + } + + // Clean up old listeners before adding new ones + this.cleanupSocketListeners(); + + // Set up socket listeners + this.setupSocketListeners(); + + // Update server with player movement + this.time.addEvent({ + delay: 50, // Send updates every 50ms + callback: this.sendPlayerUpdate, + callbackScope: this, + loop: true + }); + + // Listen for player counts in this scene specifically + this.socket.on('playerCount', (data) => { + console.log('Player count update:', data); + if (data.scene === 'DungeonScene' && data.count >= 2 && !this.countdownStarted) { + this.startCountdown(); + } + }); + + // Add spacebar input + this.spacebar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); + + // Add attack animations + const attackAnims = [ + { key: 'attackDown', start: 36, end: 39 }, + { key: 'attackRight', start: 42, end: 45 }, + { key: 'attackUp', start: 48, end: 51 }, + { key: 'attackLeft', start: 42, end: 45 } + ]; + + attackAnims.forEach(anim => { + if (!this.anims.exists(anim.key)) { + this.anims.create({ + key: anim.key, + frames: this.anims.generateFrameNumbers('player', { + start: anim.start, + end: anim.end + }), + frameRate: 10, + repeat: 0, // Ensure animation only plays once + hideOnComplete: false // Don't hide sprite when animation completes + }); + } + }); + + // Create physics group for other players if it doesn't exist + this.otherPlayersGroup = this.physics.add.group({ + collideWorldBounds: true + }); + + // Add collision between players + this.physics.add.collider(this.player, this.otherPlayersGroup); + + // Add collision between other players + this.physics.add.collider(this.otherPlayersGroup, this.otherPlayersGroup); + + // Set world bounds + this.physics.world.setBounds(0, 0, this.game.config.width, this.game.config.height); } createHealthBar(x, y, player) { @@ -165,10 +494,13 @@ this.game.scale.resize(256,256) // Black background const healthBarBackground = this.add.rectangle(x, y - 40, width, height, 0x000000); - // Red health bar - set origin to left + // Red health bar - set origin to left and start with full width const healthBar = this.add.rectangle(x - width/2, y - 40, width, height, 0xff0000) .setOrigin(0, 0.5); + // Make sure initial width matches full health + healthBar.width = width; // Start with full width since health is 100 + return { outline: outline, background: healthBarBackground, @@ -179,49 +511,36 @@ this.game.scale.resize(256,256) update() { const speed = 80; const prevVelocity = this.player.body.velocity.clone(); - let newX = this.player.x; - let newY = this.player.y; - // this.input.keyboard.addListener("keydown-F", (e) => { - // console.log(e) - // this.player.play("attackRight"); - // }) + // Only allow movement if countdown is finished + if (!this.canMove) { + if (this.player) { + this.player.body.setVelocity(0); + } + return; + } // Stop any previous movement from the last frame this.player.body.setVelocity(0); // Horizontal movement if (this.cursors.left.isDown) { - newX -= speed * (1 / 60); - if (!this.level.isColliding(newX, newY)) { this.player.body.setVelocityX(-speed); - this.player.anims.play("walkRight", true); // Assuming you have a 'walkLeft' animation - this.player.flipX = true; // Flip the sprite to face left - } + this.player.anims.play("walkRight", true); + this.player.flipX = true; } else if (this.cursors.right.isDown) { - newX += speed * (1 / 60); - if (!this.level.isColliding(newX, newY)) { this.player.body.setVelocityX(speed); this.player.anims.play("walkRight", true); - this.player.flipX = false; // Ensure the sprite is facing right - } + this.player.flipX = false; } // Vertical movement if (this.cursors.up.isDown) { - newY -= speed * (1 / 60); - if (!this.level.isColliding(newX, newY)) { this.player.body.setVelocityY(-speed); this.player.anims.play("walkUp", true); - } } else if (this.cursors.down.isDown) { - newY += speed * (1 / 60); - console.log(this.level.isColliding(newX, newY)); - if (!this.level.isColliding(newX, newY)) { - console.log("not"); this.player.body.setVelocityY(speed); this.player.anims.play("walkDown", true); - } } // Normalize and scale the velocity so that player can't move faster along a diagonal @@ -238,7 +557,7 @@ this.game.scale.resize(256,256) // Set idle animation based on the last direction if (prevVelocity.x < 0) { - this.player.anims.play("idleRight", true); + this.player.anims.play("idleLeft", true); this.player.flipX = true; } else if (prevVelocity.x > 0) { this.player.anims.play("idleRight", true); @@ -251,7 +570,7 @@ this.game.scale.resize(256,256) } // Update health bar position and width - if (this.healthBar) { + if (this.healthBar && this.player) { const yOffset = -20; const width = 40; @@ -260,11 +579,60 @@ this.game.scale.resize(256,256) this.healthBar.background.x = this.player.x; this.healthBar.background.y = this.player.y + yOffset; - // Update red bar position and width + // Update red bar position and width based on current health this.healthBar.bar.x = this.player.x - width/2; this.healthBar.bar.y = this.player.y + yOffset; - this.healthBar.bar.width = (this.player.life / 100) * width; + this.healthBar.bar.width = (this.player.life / 100) * width; // Make sure this.player.life is set to 100 initially + } + + this.otherPlayersGroup.getChildren().forEach((player) => { + if (player.playerInfo) { + player.setPosition(player.playerInfo.x, player.playerInfo.y); + player.play(player.playerInfo.animation, true); + player.flipX = player.playerInfo.flipX; + } + }); + + // Update local player + if (this.player && this.canMove) { + // ... existing player movement code ... + + // Only emit if this is the local player + if (this.socket.id === this.localPlayerId) { + this.sendPlayerUpdate(); + } + + // Handle attack + if (Phaser.Input.Keyboard.JustDown(this.spacebar) && !this.isAttacking) { + this.handleAttack(); // Use the consolidated attack handler + } } + + // Update other players + Object.entries(this.playerWorldPosition).forEach(([playerId, playerData]) => { + if (playerData.sprite && playerId !== this.localPlayerId) { + playerData.sprite.setPosition(playerData.x, playerData.y); + if (playerData.animation) { + playerData.sprite.play(playerData.animation, true); + } + playerData.sprite.setFlipX(playerData.flipX); + } + }); + + // Clean up any trailing sprites + Object.values(this.otherPlayers).forEach(otherPlayer => { + if (otherPlayer && otherPlayer.playerInfo) { + // If player hasn't been updated in a while, set to idle + const timeSinceUpdate = Date.now() - (otherPlayer.playerInfo.lastUpdated || 0); + if (timeSinceUpdate > 100) { // 100ms threshold + const currentAnim = otherPlayer.anims.currentAnim; + if (currentAnim && currentAnim.key.startsWith('walk')) { + const idleAnim = currentAnim.key.replace('walk', 'idle'); + otherPlayer.play(idleAnim, true); + } + } + } + }); } handleSocketEvents() { @@ -280,4 +648,462 @@ this.game.scale.resize(256,256) } }); } + + getSpawnPoint() { + // Define two specific spawn points + const spawnPoints = [ + { x: 50, y: 90 }, // First player spawn + { x: 200, y: 90 } // Second player spawn + ]; + + // Count existing players to determine spawn point + const playerCount = Object.keys(this.playerWorldPosition).length; + return spawnPoints[playerCount] || spawnPoints[0]; + } + + checkPlayersAndStartCountdown() { + // Remove local countdown check - wait for server signal instead + console.log('Waiting for server countdown signal...'); + } + + startCountdown() { + if (this.countdownStarted) return; // Prevent multiple countdowns + + console.log('Starting countdown'); + this.countdownStarted = true; + let count = 3; + + if (this.countdownText) { + this.countdownText.setText(count.toString()); + } + + const countdownInterval = setInterval(() => { + count--; + if (count > 0) { + this.countdownText?.setText(count.toString()); + } else { + this.countdownText?.setText('FIGHT!'); + this.canMove = true; + + // Remove countdown text after "FIGHT!" + setTimeout(() => { + this.countdownText?.destroy(); + this.countdownText = null; + }, 1000); + + clearInterval(countdownInterval); + } + }, 1000); + } + + handleAttack() { + // Find the closest player to attack + let closestPlayer = null; + let closestDistance = Infinity; + + Object.keys(this.otherPlayers).forEach((id) => { + const otherPlayer = this.otherPlayers[id]; + const distance = Phaser.Math.Distance.Between( + this.player.x, + this.player.y, + otherPlayer.x, + otherPlayer.y + ); + if (distance < closestDistance) { + closestDistance = distance; + closestPlayer = otherPlayer; + } + }); + + if (closestPlayer && closestDistance < 25) { + this.isAttacking = true; + const currentDirection = this.getPlayerDirection(); + const attackAnim = `attack${currentDirection}`; + + if (this.anims.exists(attackAnim)) { + // Play attack animation locally + this.player.play(attackAnim, true) + .once('animationcomplete', () => { + this.isAttacking = false; + const idleAnim = `idle${currentDirection}`; + if (this.anims.exists(idleAnim)) { + this.player.play(idleAnim, true); + } + }); + + // Emit attack event with target information + this.socket.emit('playerAttack', { + x: this.player.x, + y: this.player.y, + direction: currentDirection, + scene: 'DungeonScene', + targetId: closestPlayer.playerId + }); + + console.log('Attacking player:', closestPlayer.playerId, 'with animation:', attackAnim); + } + } + } + + setupSocketListeners() { + this.socket.on('currentPlayers', (players) => { + console.log('Current players in DungeonScene:', players); + + // Don't clear if we already have these players + const currentPlayerIds = new Set(Object.keys(players)); + const existingPlayerIds = new Set(Object.keys(this.playerWorldPosition)); + + // Only clear and recreate if the player sets are different + if (!this.areSetsEqual(currentPlayerIds, existingPlayerIds)) { + this.clearExistingPlayers(); + + Object.entries(players).forEach(([id, playerInfo]) => { + if (!this.existingPlayers.has(id)) { + console.log('Creating new player:', id); + if (id === this.socket.id) { + this.player = this.createPlayerWithListeners(id, playerInfo); + } else if (playerInfo.scene === 'DungeonScene') { + this.createPlayerWithListeners(id, playerInfo); + } + this.existingPlayers.add(id); + } + }); + + // After all players are created, check if we should start countdown + if (Object.keys(players).length >= 2) { + // Tell server we have enough players + this.socket.emit('dungeonReady', { + scene: 'DungeonScene', + players: Object.keys(players) + }); + } + } + }); + + // Listen for server's countdown signal + this.socket.on('dungeonCountdown', () => { + if (!this.countdownStarted) { + this.startCountdown(); + } + }); + + // Update other players more smoothly + this.socket.on('playerMovedInDungeon', (playerInfo) => { + if (this.otherPlayers[playerInfo.playerId]) { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + if (playerInfo.animation) { + otherPlayer.play(playerInfo.animation, true); + } + otherPlayer.setFlipX(playerInfo.flipX || false); + } + }); + + this.socket.on('playerMoved', (playerInfo) => { + const otherPlayer = this.otherPlayers[playerInfo.playerId]; + if (otherPlayer) { + // Clear any existing tweens to prevent position conflicts + this.tweens.killTweensOf(otherPlayer); + + // Update position immediately + otherPlayer.setPosition(playerInfo.x, playerInfo.y); + + // Update animation only if it's different + if (playerInfo.animation && (!otherPlayer.anims.currentAnim || + otherPlayer.anims.currentAnim.key !== playerInfo.animation)) { + otherPlayer.play(playerInfo.animation, true); + } + + // Update flip state + otherPlayer.setFlipX(playerInfo.flipX); + + // Store latest info + otherPlayer.playerInfo = { + ...playerInfo, + lastUpdated: Date.now() + }; + } + }); + + this.socket.on('playerAttacked', (attackInfo) => { + const attacker = attackInfo.playerId === this.socket.id ? + this.player : this.otherPlayers[attackInfo.playerId]; + + if (attacker && !attacker.isAttacking) { + const attackAnim = `attack${attackInfo.direction}`; + + if (this.anims.exists(attackAnim)) { + attacker.isAttacking = true; + + attacker.play(attackAnim, true) + .once('animationcomplete', () => { + attacker.isAttacking = false; + // Return to idle after attack + const idleAnim = `idle${attackInfo.direction}`; + if (this.anims.exists(idleAnim)) { + attacker.play(idleAnim, true); + } + }); + } + } + }); + + this.socket.on('playerDamaged', (damageInfo) => { + console.log('Received damage info:', damageInfo); + const targetPlayer = damageInfo.playerId === this.socket.id ? + this.player : this.otherPlayers[damageInfo.playerId]; + + if (targetPlayer) { + targetPlayer.life = damageInfo.newHealth; + if (targetPlayer.healthBar) { + targetPlayer.healthBar.update(); + } + } + }); + } + + shutdown() { + console.log('DungeonScene shutting down'); + this.cleanupSocketListeners(); + this.hasJoinedScene = false; + this.existingPlayers.clear(); + } + + createAnimations() { + const animations = [ + { key: 'idleDown', start: 0, end: 5 }, + { key: 'idleRight', start: 6, end: 11 }, + { key: 'idleUp', start: 12, end: 17 }, + { key: 'idleLeft', start: 18, end: 23 }, + { key: 'walkDown', start: 18, end: 23 }, + { key: 'walkRight', start: 24, end: 29 }, + { key: 'walkUp', start: 30, end: 35 }, + { key: 'attackDown', start: 36, end: 39 }, + { key: 'attackRight', start: 42, end: 45 }, + { key: 'attackUp', start: 48, end: 51 }, + { key: 'attackLeft', start: 52, end: 55 }, + { key: 'die', start: 54, end: 57 } + ]; + + animations.forEach(anim => { + if (!this.anims.exists(anim.key)) { + this.anims.create({ + key: anim.key, + frames: this.anims.generateFrameNumbers('player', { + start: anim.start, + end: anim.end + }), + frameRate: 10, + repeat: anim.key.startsWith('attack') || anim.key === 'die' ? 0 : -1 + }); + } + }); + } + + addOtherPlayer(playerInfo) { + if (!playerInfo) return null; + + try { + console.log('Adding other player:', playerInfo); + + // Destroy existing player if it exists + if (this.otherPlayers[playerInfo.playerId]) { + this.otherPlayers[playerInfo.playerId].destroy(); + } + + const otherPlayer = this.physics.add.sprite( + playerInfo.x || this.game.config.width / 2, + playerInfo.y || this.game.config.height / 2, + 'player' + ).setScale(1); + + // Set up physics body + otherPlayer.setBodySize(24, 28); + otherPlayer.setOffset(10, 13); + otherPlayer.setBounce(0.2); + otherPlayer.setCollideWorldBounds(true); + + // Enable physics but disable gravity + otherPlayer.body.setAllowGravity(false); + + // Set initial animation + if (this.anims.exists('idleDown')) { + otherPlayer.play('idleDown'); + } + + // Store player info + otherPlayer.playerInfo = playerInfo; + otherPlayer.playerId = playerInfo.playerId; + + // Add to tracking + this.otherPlayers[playerInfo.playerId] = otherPlayer; + + if (this.otherPlayersGroup) { + this.otherPlayersGroup.add(otherPlayer); + } + + console.log('Successfully added other player:', playerInfo.playerId); + return otherPlayer; + } catch (error) { + console.error('Error in addOtherPlayer:', error); + return null; + } + } + + removePlayer(playerId) { + this.otherPlayersGroup.getChildren().forEach((player) => { + if (player.playerId === playerId) { + player.destroy(); + } + }); + } + + createPlayerWithListeners(playerId, playerInfo) { + // Check if player already exists using the Set + if (this.existingPlayers.has(playerId)) { + console.log('Player already exists:', playerId); + return this.playerWorldPosition[playerId]?.sprite; + } + + console.log('Actually creating new player:', playerId); + const spawnPoint = this.getSpawnPoint(); + const playerSprite = this.physics.add.sprite( + spawnPoint.x, + spawnPoint.y, + 'player' + ).setScale(1); + + playerSprite.setBodySize(24, 28); + playerSprite.setOffset(10, 13); + playerSprite.setBounce(0.2); + playerSprite.setCollideWorldBounds(true); + playerSprite.body.setAllowGravity(false); + + playerSprite.play('idleDown'); + playerSprite.life = 100; // Make sure to set initial life + + this.playerWorldPosition[playerId] = { + x: spawnPoint.x, + y: spawnPoint.y, + sprite: playerSprite, + animation: 'idleDown', + flipX: false + }; + + // Only add to group if it exists + if (this.playersGroup) { + this.playersGroup.add(playerSprite); + } else { + console.warn('playersGroup not initialized'); + } + + this.existingPlayers.add(playerId); + + return playerSprite; + } + + handlePlayerCollision(player1, player2) { + // Handle player collision logic here + console.log('Players collided!'); + } + + sendPlayerUpdate() { + if (this.player && this.socket) { + const now = Date.now(); + const playerInfo = { + x: this.player.x, + y: this.player.y, + animation: this.player.anims.currentAnim?.key || 'idleDown', + flipX: this.player.flipX, + scene: 'DungeonScene' + }; + + // Only send update if enough time has passed and position/state has changed + if (now - this.lastMovementUpdate >= this.movementUpdateInterval && + this.hasPlayerStateChanged(playerInfo)) { + + this.socket.emit('playerMovement', playerInfo); + this.lastMovementUpdate = now; + this.lastPosition = { ...playerInfo }; + } + } + } + + updatePlayerInWorld(playerId, playerInfo) { + const playerData = this.playerWorldPosition[playerId]; + if (playerData?.sprite) { + // Update position + playerData.x = playerInfo.x; + playerData.y = playerInfo.y; + playerData.sprite.setPosition(playerInfo.x, playerInfo.y); + + // Update animation + if (playerInfo.animation && playerData.animation !== playerInfo.animation) { + playerData.animation = playerInfo.animation; + playerData.sprite.play(playerInfo.animation, true); + } + + // Update flip + if (playerInfo.flipX !== undefined && playerData.flipX !== playerInfo.flipX) { + playerData.flipX = playerInfo.flipX; + playerData.sprite.setFlipX(playerInfo.flipX); + } + } + } + + clearExistingPlayers() { + console.log('Clearing existing players'); + Object.entries(this.playerWorldPosition).forEach(([playerId, playerData]) => { + if (playerData.sprite) { + playerData.sprite.destroy(); + } + }); + this.playerWorldPosition = {}; + if (this.playersGroup) { + this.playersGroup.clear(true, true); + } + this.existingPlayers.clear(); + } + + cleanupSocketListeners() { + if (this.socket) { + this.socket.removeAllListeners('currentPlayers'); + this.socket.removeAllListeners('newPlayer'); + this.socket.removeAllListeners('playerMoved'); + this.socket.removeAllListeners('playerCount'); + } + } + + // Helper method to compare Sets + areSetsEqual(set1, set2) { + if (set1.size !== set2.size) return false; + for (const item of set1) { + if (!set2.has(item)) return false; + } + return true; + } + + getPlayerDirection() { + const currentAnim = this.player.anims.currentAnim; + if (!currentAnim) return 'Down'; + + // Check for attack animations first + if (currentAnim.key.includes('attack')) { + return currentAnim.key.replace('attack', ''); + } + + // Then check movement/idle animations + if (currentAnim.key.includes('Left')) return 'Left'; + if (currentAnim.key.includes('Right')) return 'Right'; + if (currentAnim.key.includes('Up')) return 'Up'; + return 'Down'; + } + + hasPlayerStateChanged(newState) { + const positionThreshold = 1; // Minimum movement to trigger update + return Math.abs(this.lastPosition.x - newState.x) > positionThreshold || + Math.abs(this.lastPosition.y - newState.y) > positionThreshold || + this.lastPosition.animation !== newState.animation || + this.lastPosition.flipX !== newState.flipX; + } } diff --git a/client/dungeonmap/src/main.js b/client/dungeonmap/src/main.js index c963c42..7c230cd 100644 --- a/client/dungeonmap/src/main.js +++ b/client/dungeonmap/src/main.js @@ -1,12 +1,12 @@ import Phaser from "phaser"; -import DungeonScene from "./DungeonScene.js"; -import CommonScene from "./CommonScene.js"; -import BridgeScene from "./BridgeScene.js"; +import DungeonScene from "./DungeonScene"; +import CommonScene from "./CommonScene"; +import BridgeScene from "./BridgeScene"; +import BackgroundScene from "./managers/backgroundscene"; import io from "socket.io-client"; - const socket = io(); -export default new Phaser.Game({ +const config = { type: Phaser.AUTO, width: 800, height: 600, @@ -17,7 +17,7 @@ export default new Phaser.Game({ debug: false, }, }, - scene: [CommonScene, DungeonScene, BridgeScene], + scene: [BackgroundScene, CommonScene, DungeonScene, BridgeScene], scale: { zoom: 3, }, @@ -26,4 +26,7 @@ export default new Phaser.Game({ window.socket = socket; } } -}); +}; + +// Create game instance with config +new Phaser.Game(config); \ No newline at end of file diff --git a/client/dungeonmap/src/managers/AnimationManager.js b/client/dungeonmap/src/managers/AnimationManager.js new file mode 100644 index 0000000..b25e687 --- /dev/null +++ b/client/dungeonmap/src/managers/AnimationManager.js @@ -0,0 +1,70 @@ +export class AnimationManager { + static createAnimations(scene) { + const animations = [ + { + key: 'idleDown', + frames: { start: 0, end: 5 } + }, + { + key: 'idleRight', + frames: { start: 6, end: 11 } + }, + { + key: 'idleLeft', + frames: { start: 6, end: 11 } + }, + { + key: 'idleUp', + frames: { start: 12, end: 17 } + }, + { + key: 'walkDown', + frames: { start: 18, end: 23 } + }, + { + key: 'walkRight', + frames: { start: 24, end: 29 } + }, + { + key: 'walkLeft', + frames: { start: 24, end: 29 } + }, + { + key: 'walkUp', + frames: { start: 30, end: 35 } + }, + { + key: 'attackDown', + frames: { start: 36, end: 49 } + }, + { + key: 'attackRight', + frames: { start: 42, end: 46 } + }, + { + key: 'attackLeft', + frames: { start: 42, end: 46 } + }, + { + key: 'attackUp', + frames: { start: 48, end: 52 } + }, + { + key: 'die', + frames: { start: 54, end: 56 } + } + ]; + + animations.forEach(animation => { + // Check if animation already exists + if (!scene.anims.exists(animation.key)) { + scene.anims.create({ + key: animation.key, + frames: scene.anims.generateFrameNumbers('player', animation.frames), + frameRate: 10, + repeat: animation.key.includes('idle') ? -1 : 0 + }); + } + }); + } +} diff --git a/client/dungeonmap/src/managers/CombatManager.js b/client/dungeonmap/src/managers/CombatManager.js new file mode 100644 index 0000000..28d03dc --- /dev/null +++ b/client/dungeonmap/src/managers/CombatManager.js @@ -0,0 +1,32 @@ +// src/managers/CombatManager.js +export class CombatManager { + static handlePlayerDeath(scene, playerId) { + console.log('Local player died') + if (playerId === scene.socket.id) { + // Local player death + scene.player.isAttacking = false + scene.player.isDead = true + scene.player.anims.play('die', true).once('animationcomplete', () => { + // Handle respawn or game over logic here + console.log('Local player died') + }) + } else if (scene.otherPlayers[playerId]) { + // Other player death + const deadPlayer = scene.otherPlayers[playerId] + deadPlayer.isAttacking = false + deadPlayer.isDead = true + deadPlayer.anims.play('die', true).once('animationcomplete', () => { + deadPlayer.destroy() + delete scene.otherPlayers[playerId] + console.log('Other player died:', playerId) + }) + } + + // Emit kill for XP + if (killerId) { + scene.socket.emit('playerKilled', { + killerId: killerId + }); + } + } +} diff --git a/client/dungeonmap/src/managers/PlayerManager.js b/client/dungeonmap/src/managers/PlayerManager.js new file mode 100644 index 0000000..b3b2ee6 --- /dev/null +++ b/client/dungeonmap/src/managers/PlayerManager.js @@ -0,0 +1,115 @@ +export class PlayerManager { + static handleAttack(scene, player, targetId) { + if (player.isAttacking) return; + + player.isAttacking = true; + + // Get the current direction from the player + const direction = player.lastDirection || 'Down'; + const animation = `attack${direction}`; + + // Play attack animation + player.play(animation, true); + + // Emit attack animation to other players + scene.socket.emit('playerAttackAnimation', { + animation: animation, + direction: direction, + attacker: scene.socket.id + }); + + scene.time.delayedCall(500, () => { + player.isAttacking = false; + if (targetId) { + scene.socket.emit('attackPlayer', targetId); + } + }); + } + + static handleOtherPlayerAttack(otherPlayer, animation) { + if (otherPlayer.isAttacking) return; + + otherPlayer.isAttacking = true; + otherPlayer.play(animation, true); + + otherPlayer.scene.time.delayedCall(500, () => { + otherPlayer.isAttacking = false; + }); + } + + static handleMovement(scene, player) { + const speed = 80; + let animation = 'idleDown'; + let velocityChanged = false; + + if (!player.isAttacking) { + // Store previous position + const prevX = player.x; + const prevY = player.y; + + // Reset velocity + player.body.setVelocity(0); + + // Handle movement with collision checks + if (scene.cursors.left.isDown) { + player.body.setVelocityX(-speed); + animation = 'walkLeft'; + player.lastDirection = 'Left'; + player.flipX = true; + velocityChanged = true; + if (player.x === prevX) console.log('Collision detected: Left'); + } else if (scene.cursors.right.isDown) { + player.body.setVelocityX(speed); + animation = 'walkRight'; + player.lastDirection = 'Right'; + player.flipX = false; + velocityChanged = true; + if (player.x === prevX) console.log('Collision detected: Right'); + } + + if (scene.cursors.up.isDown) { + player.body.setVelocityY(-speed); + animation = 'walkUp'; + player.lastDirection = 'Up'; + velocityChanged = true; + if (player.y === prevY) console.log('Collision detected: Up'); + } else if (scene.cursors.down.isDown) { + player.body.setVelocityY(speed); + animation = 'walkDown'; + player.lastDirection = 'Down'; + velocityChanged = true; + if (player.y === prevY) console.log('Collision detected: Down'); + } + + // Normalize diagonal movement + if (player.body.velocity.x !== 0 && player.body.velocity.y !== 0) { + const normalize = Math.sqrt(2); + player.body.velocity.x /= normalize; + player.body.velocity.y /= normalize; + } + + // If no movement or collision occurred, play idle animation + if (!velocityChanged || (prevX === player.x && prevY === player.y)) { + animation = `idle${player.lastDirection || 'Down'}`; + player.body.setVelocity(0); + } + + // Play the animation + player.play(animation, true); + + // Only emit if position actually changed + if (player.x !== prevX || player.y !== prevY) { + scene.socket.emit('playerState', { + x: player.x, + y: player.y, + animation: animation, + flipX: player.flipX, + lastDirection: player.lastDirection, + isAttacking: player.isAttacking + }); + } + } + + return animation; + } +} diff --git a/client/dungeonmap/src/managers/XPBar.js b/client/dungeonmap/src/managers/XPBar.js new file mode 100644 index 0000000..cf2ba96 --- /dev/null +++ b/client/dungeonmap/src/managers/XPBar.js @@ -0,0 +1,78 @@ +export class XPBar { + constructor(scene) { + this.scene = scene; + + // Get existing XP from scene data or use defaults + this.currentXP = scene.registry.get('currentXP') || 0; + this.currentLevel = scene.registry.get('currentLevel') || 1; + this.maxXP = 100; + + // Fixed positions for top left corner + const x = 20; + const y = 20; + + // Create black background with more opacity + this.background = scene.add.rectangle(x, y, 200, 25, 0x000000, 1); + this.background.setOrigin(0, 0); + this.background.setScrollFactor(0); + this.background.setDepth(999999); + this.background.setAlpha(0.7); + + // Create green XP fill bar with brighter color + this.fillBar = scene.add.rectangle(x + 2, y + 2, 196, 21, 0x00FF00, 1); + this.fillBar.setOrigin(0, 0); + this.fillBar.setScrollFactor(0); + this.fillBar.setDepth(999999); + + // Add level text with larger font + this.levelText = scene.add.text(x, y - 25, `Level ${this.currentLevel}`, { + font: '20px Arial', + fill: '#FFFFFF', + stroke: '#000000', + strokeThickness: 5 + }); + this.levelText.setOrigin(0, 0); + this.levelText.setScrollFactor(0); + this.levelText.setDepth(999999); + + // XP text with larger font + this.xpText = scene.add.text(x + 210, y + 3, `${this.currentXP}/${this.maxXP}`, { + font: '18px Arial', + fill: '#FFFFFF', + stroke: '#000000', + strokeThickness: 5 + }); + this.xpText.setScrollFactor(0); + this.xpText.setDepth(999999); + + // Make all elements fixed to camera + const elements = [this.background, this.fillBar, this.levelText, this.xpText]; + elements.forEach(element => { + element.setScrollFactor(0); + element.setVisible(true); + }); + + // Initial update + this.update(this.currentXP, this.currentLevel); + } + + update(xp, level) { + this.currentXP = xp; + this.currentLevel = level; + + // Store values in scene registry + this.scene.registry.set('currentXP', this.currentXP); + this.scene.registry.set('currentLevel', this.currentLevel); + + const percentage = (this.currentXP / this.maxXP); + this.fillBar.width = 196 * percentage; + + this.levelText.setText(`Level ${this.currentLevel}`); + this.xpText.setText(`${this.currentXP}/${this.maxXP}`); + + // Ensure visibility after update + [this.background, this.fillBar, this.levelText, this.xpText].forEach(element => { + element.setVisible(true); + }); + } +} \ No newline at end of file diff --git a/client/dungeonmap/src/managers/backgroundscene.js b/client/dungeonmap/src/managers/backgroundscene.js new file mode 100644 index 0000000..0bc197e --- /dev/null +++ b/client/dungeonmap/src/managers/backgroundscene.js @@ -0,0 +1,121 @@ +import { io } from 'socket.io-client'; +import CommonScene from '../CommonScene'; +import DungeonScene from '../DungeonScene'; +import BridgeScene from '../BridgeScene'; + +export default class BackgroundScene extends Phaser.Scene { + constructor() { + super({ key: 'BackgroundScene', active: true }); + this.socket = null; + + } + + create() { + if (!this.socket) { + const serverUrl = 'http://localhost:3000'; // Make sure this matches your server + console.log('Connecting to server at:', serverUrl); + + this.socket = io(serverUrl, { + transports: ['websocket'], + upgrade: false, + reconnection: true, + reconnectionAttempts: 5 + }); + + this.socket.on('connect', () => { + console.log('Connected to server with ID:', this.socket.id); + this.hasJoinedScene = false; + }); + + this.socket.on('connect_error', (error) => { + console.error('Socket connection error:', error); + }); + } + + // Listen for scene changes + this.events.on('changeScene', (sceneName) => { + if (!this.hasJoinedScene) { + console.log('Joining scene:', sceneName); + this.socket.emit('joinScene', { + scene: sceneName, + x: this.game.config.width / 2, + y: this.game.config.height / 2 + }); + this.hasJoinedScene = true; + } + }); + + // Start with CommonScene + if (!this.currentScene) { + this.startScene('CommonScene'); + } + } + + setupSocketListeners() { + this.socket.on('connect', () => { + console.log('Connected to server with ID:', this.socket.id); + this.hasJoinedScene = false; + }); + + this.socket.on('currentPlayers', (players) => { + console.log('Received current players:', players); + if (this.currentScene) { + this.currentScene.events.emit('currentPlayers', players); + } + }); + + this.socket.on('newPlayer', (playerInfo) => { + console.log('New player joined:', playerInfo); + if (this.currentScene) { + this.currentScene.events.emit('newPlayer', playerInfo); + } + }); + + this.socket.on('playerMoved', (playerInfo) => { + if (this.currentScene) { + this.currentScene.events.emit('playerMoved', playerInfo); + } + }); + + this.socket.on('playerDisconnected', (playerId) => { + console.log('Player disconnected:', playerId); + if (this.currentScene) { + this.currentScene.events.emit('playerDisconnected', playerId); + } + }); + + this.socket.on('gameState', (state) => { + if (this.currentScene) { + this.currentScene.events.emit('gameState', state); + } + }); + } + + startScene(sceneName, data = {}) { + console.log(`Starting scene: ${sceneName} with socket ID:`, this.socket.id); + + // Stop current scene if it exists + if (this.currentScene) { + console.log(`Leaving scene: ${this.currentScene.scene.key}`); + this.socket.emit('leaveScene', { + scene: this.currentScene.scene.key, + playerId: this.socket.id + }); + this.scene.stop(this.currentScene.scene.key); + } + + // Start new scene with socket reference + this.scene.start(sceneName, { + socket: this.socket, + backgroundScene: this, + ...data + }); + + // Update current scene reference + this.currentScene = this.scene.get(sceneName); + } + + getSocket() { + return this.socket; + } +} diff --git a/client/server.js b/client/server.js new file mode 100644 index 0000000..e975f01 --- /dev/null +++ b/client/server.js @@ -0,0 +1,140 @@ +const express = require('express') +const http = require('http') +const { Server } = require('socket.io') +const app = express() +const httpServer = http.createServer(app) +const io = new Server(httpServer, { + cors: { + origin: ['http://localhost:8080', 'http://127.0.0.1:8080'], + credentials: false, + }, +}) + +const players = {} + +io.on('connection', (socket) => { + console.log('A user connected:', socket.id) + + players[socket.id] = { + playerId: socket.id, + x: 400, + y: 300, + life: 100, + attack: 10, + weapon: 'sword', + animation: 'idleDown', + lastDirection: 'Down', + flipX: false, + isAttacking: false, + currentScene: 'CommonScene', + xp: 0, + level: 1 + } + + socket.emit('currentPlayers', players) + socket.broadcast.emit('newPlayer', players[socket.id]) + + socket.on('playerInput', (data) => { + if (players[socket.id]) { + players[socket.id].x = data.x + players[socket.id].y = data.y + players[socket.id].animation = data.animation + players[socket.id].flipX = data.flipX + players[socket.id].lastDirection = data.lastDirection + + io.emit('playerMoved', { + ...players[socket.id], + playerId: socket.id, + }) + } + }) + + socket.on('leaveScene', (data) => { + console.log('Player leaving scene:', socket.id); // Debug log + + if (players[socket.id]) { + // Tell other players in the current scene that this player left + socket.to(data.from).emit('playerDisconnected', socket.id); + + // Update player's scene + players[socket.id].currentScene = data.to; + + // Leave the old scene room and join the new one + socket.leave(data.from); + socket.join(data.to); + } + }) + + socket.on('attackPlayer', (data) => { + const targetId = data.targetId + if (players[socket.id] && players[targetId]) { + players[targetId].life -= players[socket.id].attack + + io.emit('playerAttackAnimation', { + attacker: socket.id, + target: targetId, + animation: data.animation, + direction: data.direction, + }) + + if (players[targetId].life <= 0) { + io.emit('playerDefeated', targetId) + setTimeout(() => { + delete players[targetId] + }, 1000) + } else { + io.emit('playerAttacked', { + attacker: socket.id, + target: targetId, + life: players[targetId].life, + }) + } + } + }) + + socket.on('playerKilled', (killerInfo) => { + const killer = players[killerInfo.killerId]; + if (killer) { + killer.xp += 5; // 5 XP per kill + + // Calculate new level (every 100 XP = 1 level) + const newLevel = Math.floor(killer.xp / 100) + 1; + + // Check if player leveled up + if (newLevel > killer.level) { + killer.level = newLevel; + // Emit level up event + io.to(killerInfo.killerId).emit('levelUp', { + level: killer.level, + xp: killer.xp + }); + } + + // Update XP for all clients + io.emit('xpUpdate', { + playerId: killerInfo.killerId, + xp: killer.xp, + level: killer.level + }); + } + }); + + // Send initial XP data when player joins + socket.on('requestXPData', () => { + socket.emit('xpUpdate', { + playerId: socket.id, + xp: players[socket.id].xp, + level: players[socket.id].level + }); + }); + + socket.on('disconnect', () => { + console.log('A user disconnected:', socket.id) + delete players[socket.id] + io.emit('playerDisconnected', socket.id) + }) +}) + +httpServer.listen(3000, () => { + console.log('Listening on port 3000') +}) diff --git a/server/package-lock.json b/server/package-lock.json index 0dbebb6..15cead6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,17 @@ { "name": "server", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", "dependencies": { - "express": "^4.21.2", - "socket.io": "^4.8.1" + "cors": "^2.8.5", + "express": "^4.17.1", + "socket.io": "^4.5.1" } }, "node_modules/@socket.io/component-emitter": { @@ -946,6 +951,7 @@ "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", diff --git a/server/package.json b/server/package.json index f9ca066..d405844 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,18 @@ { "dependencies": { + "cors": "^2.8.5", "express": "^4.21.2", "socket.io": "^4.8.1" - } + }, + "name": "server", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" } diff --git a/server/server.js b/server/server.js index b9465cd..85e6f11 100644 --- a/server/server.js +++ b/server/server.js @@ -1,77 +1,358 @@ const express = require('express') -const http = require('http') -const { Server } = require('socket.io') const app = express() -const httpServer = http.createServer(app) -const io = new Server(httpServer, { - cors: { - origin: 'https://latch-v1.vercel.app/', - methods: ["GET", "POST"], - credentials: false, - }, +const server = require('http').Server(app) +const io = require('socket.io')(server, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } }) -const players = {} +// Enable CORS for all routes +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + next(); +}) -io.on('connection', (socket) => { - console.log('A user connected:', socket.id) - - // Add new player to the players object - players[socket.id] = { - playerId: socket.id, - x: 400, - y: 300, - life: 100, - attack: 10, - weapon: 'sword', - animation: 'idleDown' - } - - // Send the current players to the new player - socket.emit('currentPlayers', players) - - // Notify existing players of the new player - socket.broadcast.emit('newPlayer', players[socket.id]) - - // Handle player movement - socket.on('movePlayer', (movementData) => { - if (players[socket.id]) { - players[socket.id].x = movementData.x - players[socket.id].y = movementData.y - players[socket.id].animation = movementData.animation - players[socket.id].flipX = movementData.flipX - io.emit('playerMoved', players[socket.id]) +// Scene configurations +const sceneConfig = { + CommonScene: { + spawnPoints: [ + { x: 400, y: 300 } // Center spawn point for CommonScene + ] + }, + BridgeScene: { + spawnPoints: [ + { x: 100, y: 100 }, + { x: 700, y: 100 } + ] + }, + DungeonScene: { + spawnPoints: [ + { x: 100, y: 100 }, + { x: 700, y: 100 } + ] } - }) - - // Handle player attack - socket.on('attackPlayer', (targetId) => { - if (players[socket.id] && players[targetId]) { - players[targetId].life -= players[socket.id].attack - // log the both players life - console.log('Attacker:', players[socket.id].life) - console.log('Target:', players[targetId].life) - if (players[targetId].life <= 0) { - io.emit('playerDefeated', targetId) - delete players[targetId] - } else { - io.emit('playerAttacked', { - attacker: socket.id, - target: targetId, - life: players[targetId].life, - }) - } +}; + +// Game state to track players and scenes +const gameState = { + CommonScene: { + players: {} + }, + BridgeScene: { + players: {} + }, + DungeonScene: { + players: {} } - }) - - // Handle player disconnection - socket.on('disconnect', () => { - console.log('A user disconnected:', socket.id) - delete players[socket.id] - io.emit('playerDisconnected', socket.id) - }) -}) +}; -httpServer.listen(3000, () => { - console.log('Listening on port 3000') -}) +io.on('connection', (socket) => { + console.log('Player connected:', socket.id); + + socket.on('joinScene', (data) => { + console.log(`Player ${socket.id} joining ${data.scene}`); + const scene = data.scene; + + // Leave previous scene if any + if (socket.scene) { + socket.leave(socket.scene); + if (gameState[socket.scene]?.players[socket.id]) { + delete gameState[socket.scene].players[socket.id]; + } + } + + // Join new scene + socket.scene = scene; + socket.join(scene); + + // Initialize scene state if needed + if (!gameState[scene]) { + gameState[scene] = { players: {} }; + } + + // Add player to scene + gameState[scene].players[socket.id] = { + playerId: socket.id, + x: Math.random() * 800, + y: Math.random() * 600, + animation: 'idleDown', + flipX: false, + lastDirection: 'Down', + isAttacking: false + }; + + // Send current scene state to new player + socket.emit('currentPlayers', gameState[scene].players); + + // Notify others in scene + socket.to(scene).emit('newPlayer', { + playerId: socket.id, + ...gameState[scene].players[socket.id] + }); + }); + + socket.on('playerMovement', (movementData) => { + if (socket.scene && gameState[socket.scene].players[socket.id]) { + // Update player position + gameState[socket.scene].players[socket.id] = { + ...gameState[socket.scene].players[socket.id], + ...movementData + }; + + // Send player movement to others in same scene + socket.to(socket.scene).emit('playerMoved', { + playerId: socket.id, + ...movementData + }); + } + }); + + socket.on('disconnect', () => { + console.log('Player disconnected:', socket.id); + if (socket.scene && gameState[socket.scene]?.players[socket.id]) { + delete gameState[socket.scene].players[socket.id]; + io.to(socket.scene).emit('playerDisconnected', socket.id); + } + }); + + socket.on('keyState', (keys) => { + if (!socket.scene || !gameState[socket.scene].players[socket.id]) return; + + const player = gameState[socket.scene].players[socket.id]; + const speed = 80; + let animation = 'idleDown'; + let moved = false; + + // Process movement based on either WASD or arrow keys + const left = keys.arrows.left || keys.wasd.left; + const right = keys.arrows.right || keys.wasd.right; + const up = keys.arrows.up || keys.wasd.up; + const down = keys.arrows.down || keys.wasd.down; + + // Calculate movement + if (left) { + player.x -= speed * (16/1000); + animation = 'walkRight'; + player.flipX = true; + player.lastDirection = 'Right'; + moved = true; + } else if (right) { + player.x += speed * (16/1000); + animation = 'walkRight'; + player.flipX = false; + player.lastDirection = 'Right'; + moved = true; + } + + if (up) { + player.y -= speed * (16/1000); + animation = 'walkUp'; + player.lastDirection = 'Up'; + moved = true; + } else if (down) { + player.y += speed * (16/1000); + animation = 'walkDown'; + player.lastDirection = 'Down'; + moved = true; + } + + // Set idle animation if not moving + if (!moved) { + animation = `idle${player.lastDirection || 'Down'}`; + } + + // Update player state + player.animation = animation; + + // Broadcast updated game state + io.to(socket.scene).emit('gameState', { + players: gameState[socket.scene].players + }); + }); + + socket.on('chatMessage', (data) => { + // Broadcast the message to all clients in the same scene + io.to(socket.scene).emit('chatMessage', { + playerId: socket.id, + message: data.message + }); + }); + + socket.on('playerInput', (data) => { + // If it's a state update (has x, y, animation) + if (data.x !== undefined && data.y !== undefined && data.animation) { + const player = gameState[socket.scene]?.players[socket.id]; + if (player) { + player.x = data.x; + player.y = data.y; + player.animation = data.animation; + player.flipX = data.flipX; + + // Broadcast update without logging + io.to(socket.scene).emit('gameState', { + players: gameState[socket.scene].players + }); + } + return; + } + + // If it's an input update (has input controls) + if (data.input) { + const player = gameState[socket.scene]?.players[socket.id]; + if (!player) return; + + const speed = 80; + let animation = 'idleDown'; + + // Don't process movement if player is attacking + if (player.isAttacking) { + animation = `attack${player.lastDirection || 'Down'}`; + io.emit('gameState', { players: gameState[socket.scene].players }); + return; + } + + // Reset velocity + let velocityX = 0; + let velocityY = 0; + + // Handle input with safe checks + const input = data.input; + if (input.left === true) { + velocityX = -speed; + animation = 'walkLeft'; + player.lastDirection = 'Left'; + player.flipX = true; + } else if (input.right === true) { + velocityX = speed; + animation = 'walkRight'; + player.lastDirection = 'Right'; + player.flipX = false; + } + + if (input.up === true) { + velocityY = -speed; + animation = 'walkUp'; + player.lastDirection = 'Up'; + } else if (input.down === true) { + velocityY = speed; + animation = 'walkDown'; + player.lastDirection = 'Down'; + } + + // Handle attack input + if (input.attack === true) { + player.isAttacking = true; + animation = `attack${player.lastDirection || 'Down'}`; + + // Reset attack state after animation + setTimeout(() => { + player.isAttacking = false; + }, 500); + } + + // Normalize diagonal movement + if (velocityX !== 0 && velocityY !== 0) { + const normalize = Math.sqrt(2); + velocityX /= normalize; + velocityY /= normalize; + } + + // Update position if not attacking + if (!player.isAttacking) { + player.x += velocityX * (1/60); + player.y += velocityY * (1/60); + } + + // If no movement and not attacking, use idle animation + if (velocityX === 0 && velocityY === 0 && !player.isAttacking) { + animation = `idle${player.lastDirection || 'Down'}`; + } + + // Update player state + player.animation = animation; + + // Broadcast new state to all players + io.to(socket.scene).emit('gameState', { + players: gameState[socket.scene].players + }); + } + }); + + socket.on('dungeonReady', (data) => { + // When a client reports enough players, broadcast countdown to all + io.in('DungeonScene').emit('dungeonCountdown'); + }); + + socket.on('bridgeReady', (data) => { + // When a client reports enough players, broadcast countdown to all + io.in('BridgeScene').emit('bridgeCountdown'); + }); + + socket.on('playerAttack', (attackInfo) => { + const attacker = gameState[attackInfo.scene]?.players[socket.id]; + if (!attacker) return; + + // Emit attack animation to all players including attacker + io.in(attackInfo.scene).emit('playerAttacked', { + playerId: socket.id, + direction: attackInfo.direction + }); + + // Check for players in attack range + const attackRange = 50; // pixels + const attackDamage = 10; // 10 damage per hit + + Object.entries(gameState[attackInfo.scene].players).forEach(([targetId, targetPlayer]) => { + if (targetId !== socket.id && targetPlayer.scene === attackInfo.scene) { + // Check if target is in range + const dx = targetPlayer.x - attackInfo.x; + const dy = targetPlayer.y - attackInfo.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance <= attackRange) { + // Initialize health if not set + if (typeof targetPlayer.health === 'undefined') { + targetPlayer.health = 100; + } + + // Apply damage + targetPlayer.health = Math.max(0, targetPlayer.health - attackDamage); + + // Emit damage to all players + io.in(attackInfo.scene).emit('playerDamaged', { + playerId: targetId, + newHealth: targetPlayer.health, + attackerId: socket.id + }); + + // Check for player death + if (targetPlayer.health <= 0) { + io.in(attackInfo.scene).emit('playerDied', { + playerId: targetId + }); + } + } + } + }); + }); +}); + +// Game loop for continuous state updates +setInterval(() => { + Object.keys(gameState).forEach(scene => { + if (Object.keys(gameState[scene].players).length > 0) { + io.to(scene).emit('gameState', { + players: gameState[scene].players + }); + } + }); +}, 1000 / 60); // 60 times per second + +const PORT = 3000; +server.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +});