diff --git a/package-lock.json b/package-lock.json index c99caa7..26d0f19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1184,6 +1184,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", @@ -1226,6 +1240,14 @@ "node": ">= 0.8.0" } }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1288,6 +1310,14 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1722,6 +1752,23 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2270,6 +2317,28 @@ "jws": "^4.0.0" } }, + "node_modules/grant/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/grant/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/grant/node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -2308,6 +2377,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2330,6 +2411,19 @@ "he": "bin/he" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2603,8 +2697,6 @@ "safe-buffer": "^5.0.1" } }, -<<<<<<< HEAD -======= "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2643,7 +2735,6 @@ "safe-buffer": "^5.0.1" } }, ->>>>>>> main "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2827,6 +2918,22 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", diff --git a/src/rooms/GameRoom.ts b/src/rooms/GameRoom.ts index d615a2c..cff9c6e 100644 --- a/src/rooms/GameRoom.ts +++ b/src/rooms/GameRoom.ts @@ -52,6 +52,16 @@ export class GameRoom extends Room { console.log(this.state.getMapInfo()); client.send("mapInfo", this.state.getMapInfo()); }); + + this.onMessage("fireLaser", (client, message) => { + const result = this.state.fireLaser(message.laserId); + this.broadcast("laserFired", { + laserId: message.laserId, + active: result.active, + path: result.path, + cratesDestroyed: result.cratesDestroyed, + }); + }); } onJoin(client: Client, options: any) { diff --git a/src/rooms/json/examples/room1.json b/src/rooms/json/examples/room1.json index 82d7f8c..8064926 100644 --- a/src/rooms/json/examples/room1.json +++ b/src/rooms/json/examples/room1.json @@ -97,7 +97,7 @@ "type": "button", "id": "button-red-1", "x": 1, - "y": 5, + "y": 6, "color": "#FF3B30", "doorId": "door-red-1" }, @@ -107,18 +107,35 @@ "y": 3, "damage": 1, "cooldownMs": 600 + }, + { + "type": "laser", + "id": "laser-1", + "x": 1, + "y": 5, + "direction": "right", + "range": 8 + }, + { + "type": "laser", + "id": "laser-2", + "x": 4, + "y": 1, + "direction": "down", + "range": 6 } ], "entities": { "players": [ { "x": 2, "y": 1 }, - { "x": 5, "y": 5 } + { "x": 7, "y": 5 } ], "enemies": [], "capybara": { "x": 0, "y": 0 }, "crates": [ { "x": 4, "y": 5 }, - { "x": 3, "y": 3 } + { "x": 5, "y": 5 }, + { "x": 4, "y": 3 } ] } } diff --git a/src/rooms/schema/LaserState.ts b/src/rooms/schema/LaserState.ts new file mode 100644 index 0000000..e29b952 --- /dev/null +++ b/src/rooms/schema/LaserState.ts @@ -0,0 +1,56 @@ +import { type, Schema, MapSchema } from "@colyseus/schema"; +import { Position } from "./Position"; + +export class Laser extends Schema { + @type("string") id: string; + @type(Position) position: Position = new Position(); + @type("string") direction: string; // "up", "down", "left", "right" + @type("number") range: number = 10; + @type("boolean") active: boolean = false; +} + +export class LaserState extends Schema { + @type({ map: Laser }) lasers = new MapSchema(); + @type({ map: "string" }) positionMap = new MapSchema(); // key: "x_y", value: laserId + + createLaser( + id: string, + x: number, + y: number, + direction: string, + range: number = 10 + ): Laser { + const laser = new Laser(); + laser.id = id; + laser.position.x = x; + laser.position.y = y; + laser.direction = direction; + laser.range = range; + laser.active = false; + this.lasers.set(id, laser); + this.positionMap.set(`${x}_${y}`, id); + return laser; + } + + getLaserAt(x: number, y: number): Laser | undefined { + const laserId = this.positionMap.get(`${x}_${y}`); + return laserId ? this.lasers.get(laserId) : undefined; + } + + getLaser(id: string): Laser | undefined { + return this.lasers.get(id); + } + + removeLaser(id: string) { + const laser = this.lasers.get(id); + if (laser) { + this.positionMap.delete(`${laser.position.x}_${laser.position.y}`); + this.lasers.delete(id); + } + } + + onRoomDispose() { + this.lasers.clear(); + this.positionMap.clear(); + } +} diff --git a/src/rooms/schema/RoomState.ts b/src/rooms/schema/RoomState.ts index de35152..95e8894 100644 --- a/src/rooms/schema/RoomState.ts +++ b/src/rooms/schema/RoomState.ts @@ -4,6 +4,7 @@ import { PlayerState } from "./PlayerState.js"; import { CrateState } from "./CrateState.js"; import { ButtonState } from "./ButtonState.js"; import { DoorState } from "./DoorState.js"; +import { LaserState } from "./LaserState.js"; export class RoomState extends Schema { @type(["string"]) grid = new ArraySchema(); @@ -16,6 +17,7 @@ export class RoomState extends Schema { @type(CrateState) crateState: CrateState = new CrateState(); @type(DoorState) doorState: DoorState = new DoorState(); @type(ButtonState) buttonState: ButtonState = new ButtonState(); + @type(LaserState) laserState: LaserState = new LaserState(); loadRoomFromJson(jsonData: any) { try { @@ -59,6 +61,14 @@ export class RoomState extends Schema { mechanicData.y, mechanicData.doorId ); + } else if (mechanicType === "laser") { + this.laserState.createLaser( + mechanicData.id, + mechanicData.x, + mechanicData.y, + mechanicData.direction, + mechanicData.range ?? 10 + ); } } } @@ -126,6 +136,7 @@ export class RoomState extends Schema { this.crateState.onRoomDispose(); this.doorState.onRoomDispose(); this.buttonState.onRoomDispose(); + this.laserState.onRoomDispose(); } movePlayer(sessionId: string, deltaX: number, deltaY: number): boolean { @@ -185,6 +196,13 @@ export class RoomState extends Schema { y: button.position.y, pressed: button.pressed, })), + lasers: Array.from(this.laserState.lasers.values()).map((laser) => ({ + laserId: laser.id, + x: laser.position.x, + y: laser.position.y, + direction: laser.direction, + range: laser.range, + })), }; } @@ -266,4 +284,83 @@ export class RoomState extends Schema { } return doorsAndButtonsToUpdate; } + + fireLaser(laserId: string): { + active: boolean; + path: { x: number; y: number }[]; + cratesDestroyed: { crateId: string; x: number; y: number }[]; + } { + const laser = this.laserState.getLaser(laserId); + if (!laser) { + return { active: false, path: [], cratesDestroyed: [] }; + } + + // Toggle the laser active state + laser.active = !laser.active; + + // If turning off, return empty path + if (!laser.active) { + return { active: false, path: [], cratesDestroyed: [] }; + } + + const path: { x: number; y: number }[] = []; + const cratesDestroyed: { crateId: string; x: number; y: number }[] = []; + + let dx = 0; + let dy = 0; + + switch (laser.direction) { + case "up": + dy = -1; + break; + case "down": + dy = 1; + break; + case "left": + dx = -1; + break; + case "right": + dx = 1; + break; + } + + let currentX = laser.position.x + dx; + let currentY = laser.position.y + dy; + + for (let i = 0; i < laser.range; i++) { + // Stop if out of bounds + if ( + currentX < 0 || + currentX >= this.width || + currentY < 0 || + currentY >= this.height + ) { + break; + } + + // Stop if hit a wall + const cell = this.getCellValue(currentX, currentY); + if (cell.startsWith("w")) { + break; + } + + path.push({ x: currentX, y: currentY }); + + // Check for crate at this position and destroy it + const crate = this.crateState.getCrateAt(currentX, currentY); + if (crate) { + cratesDestroyed.push({ + crateId: crate.id, + x: currentX, + y: currentY, + }); + this.crateState.removeCrate(crate.id); + } + + currentX += dx; + currentY += dy; + } + + return { active: true, path, cratesDestroyed }; + } }