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/overlays.ts
// Port of BrowserQuest client/js/{bubble,infomanager}.js (MPL-2.0).
// Chat text is set with textContent: player chat is never parsed as HTML.
import { imageUrl } from './assets.js';
import { Timer } from './timing.js';

class Bubble {
  readonly timer: Timer;

  constructor(
    readonly id: number,
    readonly element: HTMLDivElement,
    time: number,
  ) {
    this.timer = new Timer(5000, time);
  }

  isOver(time: number): boolean {
    return this.timer.isOver(time);
  }

  destroy(): void {
    this.element.remove();
  }

  reset(time: number): void {
    this.timer.lastTime = time;
  }
}

export class BubbleManager {
  private bubbles = new Map<number, Bubble>();

  constructor(private readonly container: HTMLElement) {}

  getBubbleById(id: number): Bubble | null {
    return this.bubbles.get(id) ?? null;
  }

  create(id: number, message: string, time: number): void {
    const existing = this.bubbles.get(id);
    if (existing) {
      existing.reset(time);
      existing.element.querySelector('p')!.textContent = message;
      return;
    }
    const el = document.createElement('div');
    el.className = 'bubble';
    const p = document.createElement('p');
    p.textContent = message;
    const thingy = document.createElement('div');
    thingy.className = 'thingy';
    thingy.style.backgroundImage = `url("${imageUrl('thingy')}")`;
    el.append(p, thingy);
    this.container.append(el);
    this.bubbles.set(id, new Bubble(id, el, time));
  }

  update(time: number): void {
    for (const [id, bubble] of this.bubbles) {
      if (bubble.isOver(time)) {
        bubble.destroy();
        this.bubbles.delete(id);
      }
    }
  }

  clean(): void {
    for (const bubble of this.bubbles.values()) bubble.destroy();
    this.bubbles.clear();
  }

  destroyBubble(id: number): void {
    const bubble = this.bubbles.get(id);
    if (bubble) {
      bubble.destroy();
      this.bubbles.delete(id);
    }
  }
}

const damageInfoColors = {
  received: { fill: 'rgb(255, 50, 50)', stroke: 'rgb(255, 180, 180)' },
  inflicted: { fill: 'white', stroke: '#373737' },
  healed: { fill: 'rgb(80, 255, 80)', stroke: 'rgb(50, 120, 50)' },
} as const;

export type DamageType = keyof typeof damageInfoColors;

export class DamageInfo {
  opacity = 1.0;
  lastTime = 0;
  readonly speed = 100;
  readonly fillColor: string;
  readonly strokeColor: string;

  constructor(
    readonly id: string,
    readonly value: string | number,
    readonly x: number,
    public y: number,
    type: DamageType,
    private readonly onDestroy: (id: string) => void,
  ) {
    this.fillColor = damageInfoColors[type].fill;
    this.strokeColor = damageInfoColors[type].stroke;
  }

  update(time: number): void {
    if (time - this.lastTime > this.speed) {
      this.lastTime = time;
      this.y -= 1;
      this.opacity -= 0.07;
      if (this.opacity < 0) this.onDestroy(this.id);
    }
  }
}

export class InfoManager {
  private infos = new Map<string, DamageInfo>();
  private destroyQueue: string[] = [];

  constructor(private readonly now: () => number) {}

  addDamageInfo(value: string | number, x: number, y: number, type: DamageType): void {
    const time = this.now();
    const id = `${time}${Math.abs(Number(value) || 0)}${x}${y}${Math.random()}`;
    this.infos.set(id, new DamageInfo(id, value, x, y, type, (i) => this.destroyQueue.push(i)));
  }

  forEachInfo(callback: (info: DamageInfo) => void): void {
    for (const info of this.infos.values()) callback(info);
  }

  update(time: number): void {
    this.forEachInfo((info) => info.update(time));
    for (const id of this.destroyQueue) this.infos.delete(id);
    this.destroyQueue = [];
  }
}