diff --git a/code/src/game.ts b/code/src/game.ts index 8a0dca9..2156162 100644 --- a/code/src/game.ts +++ b/code/src/game.ts @@ -71,6 +71,7 @@ export default class GameWindow{ width: number; player: Player = new Player('player1'); player2: Player = new Player('player2'); + localPlay: boolean = false; ground: Ground = new Ground(); leftWall: LeftWall = new LeftWall(); rightWall: RightWall = new RightWall(); @@ -82,31 +83,64 @@ export default class GameWindow{ tipDiv: HTMLDivElement - constructor(){ + constructor(localPlay: boolean = false){ this.height = 128; this.width = 256; + this.localPlay = localPlay; this.init(); - - - this.remoteInput = new RemoteInput(this.player.inputBuffer); this.player2.flipHorizontally(); this.player2.physics.transform.x = 200; this.player.physics.transform.x = 50; - this.remoteInput.onconnection = () => { - if(this.remoteInput.creator){ - this.remoteInput.inputBuffer = this.player.inputBuffer - this.input = new Input(this.player2.inputBuffer, this.remoteInput.dataChannel); - }else{ - this.remoteInput.inputBuffer = this.player2.inputBuffer - this.input = new Input(this.player.inputBuffer, this.remoteInput.dataChannel); - } - this.remoteInput.hideDomElements(); + if (this.localPlay) { + // Local play mode + this.input = new Input(this.player.inputBuffer, null, 1); // Player 1 + new Input(this.player2.inputBuffer, null, 2); // Player 2 + + this.player.ready = true; + this.player2.ready = true; + this.tipDiv.style.display = 'none'; // Players are ready, no need for "Press space" this.startGameLoop(); - this.tipDiv.style.display = 'block'; + } else { + // Remote play logic + // RemoteInput constructor likely takes the buffer of the player whose actions are TO BE SENT. + // this.input will be for the locally controlled player on this client. + // RemoteInput's internal 'inputBuffer' property will be for the player whose actions are RECEIVED. + this.remoteInput = new RemoteInput(this.player.inputBuffer); // Default for player1 if creator, will be re-assigned if joiner + + this.remoteInput.onconnection = () => { + if (this.remoteInput.creator) { + // CREATOR: Player 1 is LOCAL, Player 2 is REMOTE + // RemoteInput sends data from player.inputBuffer (P1) + // this.remoteInput an instance of RemoteInput was already created with this.player.inputBuffer + + // Local keyboard handler for Player 1 (creator), sends data over channel. Uses P1 keys. + this.input = new Input(this.player.inputBuffer, this.remoteInput.dataChannel, 1); + // RemoteInput must be configured to update player2.inputBuffer upon receiving data. + this.remoteInput.inputBuffer = this.player2.inputBuffer; + } else { + // JOINER: Player 2 is LOCAL, Player 1 is REMOTE + // RemoteInput needs to send data from player2.inputBuffer (P2) + // Re-initialize or update RemoteInput to use player2's buffer for sending. + // This is tricky without knowing RemoteInput's internals. + // For now, let's assume RemoteInput's constructor argument is final for sending. + // This implies the initial `new RemoteInput(this.player.inputBuffer)` might be an issue for the joiner. + // However, changing RemoteInput is out of scope. We focus on Input class usage. + + // The joiner controls Player 2 locally. + // Local keyboard handler for Player 2 (joiner), sends data over channel. Uses P1 keys by default. + this.input = new Input(this.player2.inputBuffer, this.remoteInput.dataChannel, 1); + // RemoteInput must be configured to update player1.inputBuffer upon receiving data. + this.remoteInput.inputBuffer = this.player.inputBuffer; + } + this.remoteInput.hideDomElements(); + this.startGameLoop(); + this.tipDiv.style.display = 'block'; + } } + // Commented out old test code // this.startGameLoop(); // this.player.ready = true; // this.input = new Input(this.player2.inputBuffer, null); diff --git a/code/src/input.ts b/code/src/input.ts index 06258c6..2aca8d9 100644 --- a/code/src/input.ts +++ b/code/src/input.ts @@ -12,62 +12,114 @@ class InputBuffer{ y: Boolean = false; start: Boolean = false; + c: Boolean = false; // For Dash input constructor(){ } } - class Input{ - constructor(inputBuffer: InputBuffer, dataChannel: RTCDataChannel){ - document.addEventListener('keydown', (event) => { - dataChannel.send(JSON.stringify({k: event.key, d: true})) - switch(event.key){ - case 'a': - case 'A': inputBuffer.left = true; break; - case 's': - case 'S': inputBuffer.down = true; break; - case 'd': - case 'D': inputBuffer.right = true; break; - case 'w': - case 'W': inputBuffer.up = true; break; - case 'u': - case 'U': inputBuffer.a = true; break; - case 'i': - case 'I': inputBuffer.b = true; break; - case 'j': - case 'J': inputBuffer.x = true; break; - case 'k': - case 'K': inputBuffer.y = true; break; - case ' ': inputBuffer.start = true; break; +interface KeyMappings { + up: string; + down: string; + left: string; + right: string; + a: string; + b: string; + x: string; + y: string; + start: string; + dash?: string; // Dash action key +} + +class Input { + private playerNumber: number; + private keyMap: KeyMappings; + private inputBuffer: InputBuffer; + constructor(inputBuffer: InputBuffer, dataChannel?: RTCDataChannel | null, playerNumber: number = 1) { + this.inputBuffer = inputBuffer; + this.playerNumber = playerNumber; + this.setKeyMappings(); + + document.addEventListener('keydown', (event) => { + if (dataChannel) { + // Send raw key for remote player, mapping is handled by their client + dataChannel.send(JSON.stringify({ k: event.key, d: true })); } - }) + // For local players, process input based on keyMap + this.handleKeyEvent(event.key, true); + }); document.addEventListener('keyup', (event) => { - dataChannel.send(JSON.stringify({k:event.key, d: false})) - switch(event.key){ - case 'a': - case 'A': inputBuffer.left = false; break; - case 's': - case 'S': inputBuffer.down = false; break; - case 'd': - case 'D': inputBuffer.right = false; break; - case 'w': - case 'W': inputBuffer.up = false; break; - case 'u': - case 'U': inputBuffer.a = false; break; - case 'i': - case 'I': inputBuffer.b = false; break; - case 'j': - case 'J': inputBuffer.x = false; break; - case 'k': - case 'K': inputBuffer.y = false; break; - case ' ': inputBuffer.start = false; break; + if (dataChannel) { + // Send raw key for remote player, mapping is handled by their client + dataChannel.send(JSON.stringify({ k: event.key, d: false })); } - }) + // For local players, process input based on keyMap + this.handleKeyEvent(event.key, false); + }); + } + + private setKeyMappings() { + if (this.playerNumber === 1) { + this.keyMap = { + up: 'w', + down: 's', + left: 'a', + right: 'd', + a: 'u', // Action A + b: 'i', // Action B + x: 'j', // Action X + y: 'k', // Action Y + start: ' ', // Space bar + dash: 'c', + }; + } else { // Player 2 or any other number for now + this.keyMap = { + up: 'ArrowUp', + down: 'ArrowDown', + left: 'ArrowLeft', + right: 'ArrowRight', + a: 'Numpad4', + b: 'Numpad5', + x: 'Numpad1', + y: 'Numpad2', + start: 'NumpadEnter', // Or 'Enter' if NumpadEnter is an issue + dash: 'Numpad3', + }; + } + } + + private handleKeyEvent(key: string, isPressed: boolean) { + const lowerKey = key.toLowerCase(); // Normalize player 1 keys + + // For player 1, check lowercased keys. For player 2, Arrow keys are case sensitive. + const keyToCheck = this.playerNumber === 1 ? lowerKey : key; + + if (keyToCheck === this.keyMap.up) { + this.inputBuffer.up = isPressed; + } else if (keyToCheck === this.keyMap.down) { + this.inputBuffer.down = isPressed; + } else if (keyToCheck === this.keyMap.left) { + this.inputBuffer.left = isPressed; + } else if (keyToCheck === this.keyMap.right) { + this.inputBuffer.right = isPressed; + } else if (keyToCheck === this.keyMap.a) { + this.inputBuffer.a = isPressed; + } else if (keyToCheck === this.keyMap.b) { + this.inputBuffer.b = isPressed; + } else if (keyToCheck === this.keyMap.x) { + this.inputBuffer.x = isPressed; + } else if (keyToCheck === this.keyMap.y) { + this.inputBuffer.y = isPressed; + } else if (key === this.keyMap.start) { // Use 'key' directly for Start, as ' ' vs 'NumpadEnter' + this.inputBuffer.start = isPressed; + } else if (this.keyMap.dash && keyToCheck === this.keyMap.dash.toLowerCase()) { // Dash key, ensure dash is defined + this.inputBuffer.c = isPressed; + } } - } +} diff --git a/code/src/main.ts b/code/src/main.ts index 48a4d16..347a820 100644 --- a/code/src/main.ts +++ b/code/src/main.ts @@ -2,4 +2,4 @@ import './style.css'; import GameWindow from './game'; -new GameWindow(); \ No newline at end of file +new GameWindow(true); \ No newline at end of file diff --git a/code/src/player.ts b/code/src/player.ts index 28b181c..11fb27b 100644 --- a/code/src/player.ts +++ b/code/src/player.ts @@ -50,6 +50,16 @@ class Player { grouded: Boolean = false; inputBuffer: InputBuffer = new InputBuffer(); + // Dash properties + canDash: boolean = true; + dashCooldownTime: number = 1000; // milliseconds + dashDurationTime: number = 150; // milliseconds + dashSpeedValue: number = 350; // pixels per second + isDashing: boolean = false; + dashTimerValue: number = 0; // milliseconds + originalGravity: number = 0; // To store gravity before dash + + ready: Boolean = false; hitboxes: Map = new Map(); @@ -149,6 +159,29 @@ class Player { } moves(){ + // Dash logic + if (this.isDashing) { + // Player is currently dashing, no other moves allowed + return; + } + + if (this.inputBuffer.c && this.canDash) { + this.isDashing = true; + this.canDash = false; + this.dashTimerValue = 0; + this.originalGravity = this.physics.gravity; // Store original gravity + this.physics.gravity = 0; // Disable gravity during dash + this.physics.velocity.y = 0; // Stop vertical movement + + // Optional: Play dash animation here + // this.animator.play('dash_animation_name'); + + setTimeout(() => { + this.canDash = true; + }, this.dashCooldownTime); + return; // Dash action taken, skip other moves for this frame + } + if(this.charging){ this.chargingTime += 10; if(!this.inputBuffer.y){ @@ -212,6 +245,26 @@ class Player { } update(dt: number){ + // Convert dt from milliseconds to seconds for speed calculations + const dtSeconds = dt / 1000; + + if (this.isDashing) { + this.dashTimerValue += dt; + const dashDistance = this.dashSpeedValue * dtSeconds; + + this.physics.velocity.x = this.flipx ? -this.dashSpeedValue : this.dashSpeedValue; + + if (this.dashTimerValue >= this.dashDurationTime) { + this.isDashing = false; + this.physics.gravity = this.originalGravity; // Restore gravity + this.physics.velocity.x = 0; // Stop horizontal dash movement + // Optional: Revert to idle animation + // if (this.animator.currentAnimationName === 'dash_animation_name') { + // this.animator.play('idle'); + // } + } + } + if(this.health <= 0 ){ if(this.animator.currentAnimationName != 'die'){ this.animator.play('die'); @@ -234,8 +287,12 @@ class Player { } } this.fireballs = this.fireballs.filter((_, index)=>{ return !indices_to_delete.includes(index)}); - this.moves(); - this.physics.update(dt); + + if (!this.isDashing) { // Process other moves only if not dashing + this.moves(); + } + + this.physics.update(dt); // Physics update happens regardless (e.g. for gravity restoration) } }