Skip to content
Merged
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
22 changes: 22 additions & 0 deletions src/phaser/entities/crate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { SpriteAnimator } from "../lib/sprite-animator";
import { Entity } from "./entity";

export class Crate extends Entity {
public readonly crateId: number;

constructor(
scene: Phaser.Scene,
x: number,
y: number,
crateId: number,
textureKey = "crate",
animator: SpriteAnimator | null = null,
) {
super(scene, x, y, textureKey, animator);
this.crateId = crateId;
}

public get id(): number {
return this.crateId;
}
}
43 changes: 43 additions & 0 deletions src/phaser/mechanics/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Mechanic } from "./mechanic";

export class Button extends Mechanic {
public readonly buttonId: string;
public readonly color: string;
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The color field in Button is stored but never used. The code always loads hardcoded green/red button textures regardless of the color value passed from the backend. Either implement color-based texture loading (e.g., 'button-{color}.png') or remove the unused color field from the interface and constructor.

Copilot uses AI. Check for mistakes.
private pressed: boolean;
private pressedTextureKey: string;
private releasedTextureKey: string;

constructor(
scene: Phaser.Scene,
x: number,
y: number,
buttonId: string,
color: string,
pressed = false,
pressedTextureKey = "button-pressed",
releasedTextureKey = "button-released",
) {
super(scene, x, y, pressed ? pressedTextureKey : releasedTextureKey);
this.buttonId = buttonId;
this.color = color;
this.pressed = pressed;
this.pressedTextureKey = pressedTextureKey;
this.releasedTextureKey = releasedTextureKey;
}

public get id(): string {
return this.buttonId;
}

Comment on lines +28 to +31
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The id getter is defined but never used in the codebase. The Button instances are already accessed via their buttonId property. Consider removing it or documenting why it's needed for future use.

Suggested change
public get id(): string {
return this.buttonId;
}

Copilot uses AI. Check for mistakes.
public get isPressed(): boolean {
return this.pressed;
}

public set isPressed(value: boolean) {
this.pressed = value;
const textureKey = this.pressed
? this.pressedTextureKey
: this.releasedTextureKey;
this.changeTexture(textureKey);
}
}
41 changes: 41 additions & 0 deletions src/phaser/mechanics/door.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Mechanic } from "./mechanic";

export class Door extends Mechanic {
public readonly doorId: string;
public readonly color: string;
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The color field in Door and Button types is stored but never used. The code always loads hardcoded green textures ('door-green-open.png', 'button-green.png') regardless of the color value passed from the backend. Either implement color-based texture loading (e.g., 'door-{color}-open.png') or remove the unused color field from the interfaces and constructors.

Copilot uses AI. Check for mistakes.
private openTextureKey: string;
private closedTextureKey: string;
private open: boolean;

constructor(
scene: Phaser.Scene,
x: number,
y: number,
doorId: string,
color: string,
open = false,
openTextureKey = "door-open",
closedTextureKey = "door-closed",
) {
super(scene, x, y, open ? openTextureKey : closedTextureKey);
this.doorId = doorId;
this.color = color;
this.open = open;
this.openTextureKey = openTextureKey;
this.closedTextureKey = closedTextureKey;
}

public get id(): string {
return this.doorId;
}
Comment on lines +28 to +30
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The id getter is defined but never used in the codebase. The Door instances are already accessed via their doorId property. Consider removing it or documenting why it's needed for future use.

Copilot uses AI. Check for mistakes.

public get isOpen(): boolean {
return this.open;
}

public set isOpen(value: boolean) {
this.open = value;
const textureKey = this.open ? this.openTextureKey : this.closedTextureKey;
this.changeTexture(textureKey);
}
}
31 changes: 31 additions & 0 deletions src/phaser/mechanics/mechanic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { TILE_SIZE } from "../lib/const";

export class Mechanic extends Phaser.GameObjects.Container {
protected sprite: Phaser.GameObjects.Sprite;
protected gridX: number;
protected gridY: number;

constructor(
scene: Phaser.Scene,
gridX: number,
gridY: number,
textureKey = "door",
) {
super(scene);

this.gridX = gridX;
this.gridY = gridY;

this.sprite = this.scene.add.sprite(0, 0, textureKey);
this.add(this.sprite);

this.setPosition(
this.gridX * TILE_SIZE + TILE_SIZE / 2,
this.gridY * TILE_SIZE + TILE_SIZE / 2,
);
}

public changeTexture(textureKey: string) {
this.sprite.setTexture(textureKey);
}
}
86 changes: 86 additions & 0 deletions src/phaser/scenes/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { Room } from "colyseus.js";
import * as Phaser from "phaser";

import type { Button as ButtonType } from "../../types/button";
import type { Crate as CrateType } from "../../types/crate";
import type { Door as DoorType } from "../../types/door";
import type {
MessageCratesUpdate,
MessageDoorsAndButtonsUpdate,
MessageMapInfo,
MessageOnAddPlayer,
MessageOnRemovePlayer,
MessagePositionUpdate,
} from "../../types/messages";
import type { Player as PlayerType } from "../../types/player";
import { Crate } from "../entities/crate";
import { Player } from "../entities/player";
import { TILE_SIZE } from "../lib/const";
import {
Expand All @@ -18,10 +24,15 @@ import {
} from "../lib/player-animators";
import type { SpriteAnimator } from "../lib/sprite-animator";
import { getTileName } from "../lib/utils";
import { Button } from "../mechanics/button";
import { Door } from "../mechanics/door";

export class Main extends Phaser.Scene {
private room!: Room;
private players = new Map<string, Player>();
private crates = new Map<number, Crate>();
private buttons = new Map<string, Button>();
private doors = new Map<string, Door>();
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
private wasd!: {
W: Phaser.Input.Keyboard.Key;
Expand Down Expand Up @@ -49,6 +60,10 @@ export class Main extends Phaser.Scene {
this.load.image("wall", "images/wall.png");
this.load.image("crate", "images/crate.png");
this.load.image("ground", "images/ground.png");
this.load.image("button-released", "images/buttons/button-green.png");
this.load.image("button-pressed", "images/buttons/button-red.png");
this.load.image("door-open", "images/doors/door-green-open.png");
this.load.image("door-closed", "images/doors/door-green-closed.png");

for (const [index, textureKey] of PLAYER_TEXTURE_KEYS.entries()) {
this.load.spritesheet(
Expand Down Expand Up @@ -89,6 +104,18 @@ export class Main extends Phaser.Scene {
for (const player of message.players) {
this.addPlayer(player);
}

for (const crate of message.crates) {
this.addCrate(crate);
}

for (const button of message.buttons) {
this.addButton(button);
}

for (const door of message.doors) {
this.addDoor(door);
}
});

room.onMessage("onAddPlayer", (message: MessageOnAddPlayer) => {
Expand Down Expand Up @@ -117,6 +144,33 @@ export class Main extends Phaser.Scene {
}
});

room.onMessage("cratesUpdate", (message: MessageCratesUpdate) => {
for (const crateUpdate of message.crates) {
const crate = this.crates.get(crateUpdate.crateId);
if (crate !== undefined) {
crate.move(crateUpdate.direction);
}
}
});

room.onMessage(
"doorsAndButtonsUpdate",
(message: MessageDoorsAndButtonsUpdate) => {
for (const element of message.doorsAndButtons) {
const door = this.doors.get(element.doorId);
const button = this.buttons.get(element.buttonId);

if (door !== undefined) {
door.isOpen = element.open;
}

if (button !== undefined) {
button.isPressed = element.open;
Comment on lines +164 to +168
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message handler processes door and button updates together, but there's no validation that ensures both the doorId and buttonId actually exist before attempting to update them. If the backend sends an update with only a doorId or only a buttonId that exists, the undefined checks will silently skip the missing entity without logging. Consider adding a warning log when an expected door or button is not found to aid debugging.

Suggested change
door.isOpen = element.open;
}
if (button !== undefined) {
button.isPressed = element.open;
door.isOpen = element.open;
} else {
console.warn(
`doorsAndButtonsUpdate: door with id "${element.doorId}" not found when setting open=${element.open}`,
);
}
if (button !== undefined) {
button.isPressed = element.open;
} else {
console.warn(
`doorsAndButtonsUpdate: button with id "${element.buttonId}" not found when setting pressed=${element.open}`,
);

Copilot uses AI. Check for mistakes.
}
}
},
);

this.room.send("getMapInfo");
} catch (error) {
console.error("Error setting up Colyseus message handlers:", error);
Expand Down Expand Up @@ -152,6 +206,38 @@ export class Main extends Phaser.Scene {
this.add.existing(player);
}

private addCrate(crateInfo: CrateType) {
const crate = new Crate(this, crateInfo.x, crateInfo.y, crateInfo.crateId);
this.add.existing(crate);
this.crates.set(crateInfo.crateId, crate);
}

private addButton(buttonInfo: ButtonType) {
const button = new Button(
this,
buttonInfo.x,
buttonInfo.y,
buttonInfo.buttonId,
buttonInfo.color,
buttonInfo.pressed,
);
this.add.existing(button);
this.buttons.set(buttonInfo.buttonId, button);
}

private addDoor(doorInfo: DoorType) {
const door = new Door(
this,
doorInfo.x,
doorInfo.y,
doorInfo.doorId,
doorInfo.color,
doorInfo.open,
);
this.add.existing(door);
this.doors.set(doorInfo.doorId, door);
}

createMap(grid: number[][], width: number, height: number) {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
Expand Down
7 changes: 7 additions & 0 deletions src/types/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Button {
x: number;
y: number;
buttonId: string;
color: string;
pressed: boolean;
}
5 changes: 5 additions & 0 deletions src/types/crate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Crate {
x: number;
y: number;
crateId: number;
}
7 changes: 7 additions & 0 deletions src/types/door.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Door {
doorId: string;
color: string;
x: number;
y: number;
open: boolean;
}
14 changes: 14 additions & 0 deletions src/types/messages.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import type { Button } from "../types/button";
import type { Crate } from "../types/crate";
import type { Door } from "../types/door";
import type { Player } from "../types/player";

export interface MessageMapInfo {
grid: number[][];
width: number;
height: number;
players: Player[];
crates: Crate[];
doors: Door[];
buttons: Button[];
}

export interface MessageCratesUpdate {
crates: { crateId: number; direction: "left" | "right" | "up" | "down" }[];
}

export interface MessageDoorsAndButtonsUpdate {
doorsAndButtons: { doorId: string; buttonId: string; open: boolean }[];
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message interface names an array field doorsAndButtons, but each element contains individual doorId and buttonId properties, suggesting a one-to-one pairing. This naming could be clearer. Consider renaming to doorButtonPairs or connections to better convey that each entry represents a door-button relationship, not separate collections.

Suggested change
doorsAndButtons: { doorId: string; buttonId: string; open: boolean }[];
doorButtonPairs: { doorId: string; buttonId: string; open: boolean }[];

Copilot uses AI. Check for mistakes.
}

export interface MessagePositionUpdate {
Expand Down