Back to Browser Side Quest
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.

src/client/map.ts
// 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));
  }
}