Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions code/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
140 changes: 96 additions & 44 deletions code/src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}



Expand Down
2 changes: 1 addition & 1 deletion code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import './style.css';
import GameWindow from './game';

new GameWindow();
new GameWindow(true);
61 changes: 59 additions & 2 deletions code/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, BoxCollider> = new Map();
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -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');
Expand All @@ -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)
}
}

Expand Down