SOURCE / PINNED RELEASE
Made of little things.
Browser Side Quest
- Release
- b9d42f5768e4…
- Author-recorded commit
- c267690145fa…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/browser-side-quest
Archive hash verified: 6d4b90b8a5f328f7…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Port of BrowserQuest client/js/map.js (MPL-2.0). The map is imported at build
// time, so the worker/Ajax loading paths of the original collapse to one sync init.
import { Orientations, type Orientation } from '../shared/gametypes.js';
import { gridPositionToTileIndex, tileIndexToGridPosition } from '../shared/utils.js';
import { Area } from './timing.js';
import type { ClientMapData } from './assets.js';
export interface DoorDestination {
x: number;
y: number;
orientation: Orientation;
cameraX: number;
cameraY: number;
portal: boolean;
}
export class GameMap {
readonly width: number;
readonly height: number;
readonly tilesize: number;
readonly data: (number | number[])[];
readonly musicAreas: NonNullable<ClientMapData['musicAreas']>;
readonly grid: number[][];
readonly plateauGrid: number[][];
readonly doors = new Map<number, DoorDestination>();
readonly checkpoints: Area[] = [];
private readonly high: Set<number>;
private readonly animated: ClientMapData['animated'];
constructor(map: ClientMapData) {
this.width = map.width;
this.height = map.height;
this.tilesize = map.tilesize;
this.data = map.data;
this.musicAreas = map.musicAreas ?? [];
this.high = new Set(map.high);
this.animated = map.animated;
this.grid = [];
for (let i = 0; i < this.height; i++) this.grid[i] = new Array<number>(this.width).fill(0);
for (const tileIndex of map.collisions) {
const pos = this.tileIndexToGridPosition(tileIndex + 1);
this.grid[pos.y][pos.x] = 1;
}
for (const tileIndex of map.blocking ?? []) {
const pos = this.tileIndexToGridPosition(tileIndex + 1);
if (this.grid[pos.y] !== undefined) this.grid[pos.y][pos.x] = 1;
}
const plateau = new Set(map.plateau ?? []);
this.plateauGrid = [];
let tileIndex = 0;
for (let i = 0; i < this.height; i++) {
this.plateauGrid[i] = [];
for (let j = 0; j < this.width; j++) {
this.plateauGrid[i][j] = plateau.has(tileIndex) ? 1 : 0;
tileIndex += 1;
}
}
for (const door of map.doors) {
const orientation =
door.to === 'u'
? Orientations.UP
: door.to === 'l'
? Orientations.LEFT
: door.to === 'r'
? Orientations.RIGHT
: Orientations.DOWN;
this.doors.set(this.gridPositionToTileIndex(door.x, door.y), {
x: door.tx,
y: door.ty,
orientation,
cameraX: door.tcx,
cameraY: door.tcy,
portal: door.p === 1,
});
}
for (const cp of map.checkpoints) {
const area = new Area(cp.x, cp.y, cp.w, cp.h);
area.id = cp.id;
this.checkpoints.push(area);
}
}
tileIndexToGridPosition(tileNum: number): { x: number; y: number } {
return tileIndexToGridPosition(tileNum, this.width);
}
gridPositionToTileIndex(x: number, y: number): number {
return gridPositionToTileIndex(x, y, this.width);
}
isColliding(x: number, y: number): boolean {
if (this.isOutOfBounds(x, y)) return false;
return this.grid[y][x] === 1;
}
isPlateau(x: number, y: number): boolean {
if (this.isOutOfBounds(x, y)) return false;
return this.plateauGrid[y][x] === 1;
}
isOutOfBounds(x: number, y: number): boolean {
return Number.isInteger(x) && Number.isInteger(y) && (x < 0 || x >= this.width || y < 0 || y >= this.height);
}
/** Returns true if the given tile id is "high", i.e. above all entities. */
isHighTile(id: number): boolean {
return this.high.has(id + 1);
}
isAnimatedTile(id: number): boolean {
return id + 1 in this.animated;
}
getTileAnimationLength(id: number): number {
return this.animated[id + 1].l;
}
getTileAnimationDelay(id: number): number {
return this.animated[id + 1].d || 100;
}
isDoor(x: number, y: number): boolean {
return this.doors.has(this.gridPositionToTileIndex(x, y));
}
getDoorDestination(x: number, y: number): DoorDestination | undefined {
return this.doors.get(this.gridPositionToTileIndex(x, y));
}
getCurrentCheckpoint(entity: { gridX: number; gridY: number }): Area | undefined {
return this.checkpoints.find((checkpoint) => checkpoint.contains(entity));
}
}
