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/updater.ts
// Port of BrowserQuest client/js/updater.js (MPL-2.0).
import { Orientations } from '../shared/gametypes.js';
import { Character, type Entity } from './entity.js';
import type { Game } from './game.js';
import { Timer } from './timing.js';

export class Updater {
  private readonly playerAggroTimer = new Timer(1000);

  constructor(private readonly game: Game) {}

  update(): void {
    this.updateZoning();
    this.updateCharacters();
    this.updatePlayerAggro();
    this.updateTransitions();
    this.updateAnimations();
    this.updateAnimatedTiles();
    this.updateChatBubbles();
    this.updateInfos();
  }

  private updateCharacters(): void {
    this.game.forEachEntity((entity) => {
      if (entity.isLoaded) {
        if (entity instanceof Character) {
          this.updateCharacter(entity);
          this.game.onCharacterUpdate(entity);
        }
        this.updateEntityFading(entity);
      }
    });
  }

  private updatePlayerAggro(): void {
    const t = this.game.currentTime;
    const player = this.game.player;
    // Check player aggro every 1s when not moving nor attacking
    if (player && !player.isMoving() && !player.isAttacking() && this.playerAggroTimer.isOver(t)) {
      player.checkAggro();
    }
  }

  private updateEntityFading(entity: Entity): void {
    if (entity.isFading) {
      const duration = 1000;
      const dt = this.game.currentTime - entity.startFadingTime;
      if (dt > duration) {
        entity.isFading = false;
        entity.fadingAlpha = 1;
      } else {
        entity.fadingAlpha = dt / duration;
      }
    }
  }

  private updateTransitions(): void {
    this.game.forEachEntity((entity) => {
      if (entity instanceof Character && entity.movement.inProgress) {
        entity.movement.step(this.game.currentTime);
      }
    });
    const z = this.game.currentZoning;
    if (z && z.inProgress) z.step(this.game.currentTime);
  }

  private updateZoning(): void {
    const g = this.game;
    const c = g.camera;
    const z = g.currentZoning;
    const ts = 16;
    const speed = 500;

    if (z && z.inProgress === false) {
      const orientation = g.zoningOrientation;
      let startValue = 0;
      let endValue = 0;
      let updateFunc: ((v: number) => void) | null = null;
      let endFunc: (() => void) | null = null;

      if (orientation === Orientations.LEFT || orientation === Orientations.RIGHT) {
        const offset = (c.gridW - 2) * ts;
        startValue = orientation === Orientations.LEFT ? c.x - ts : c.x + ts;
        endValue = orientation === Orientations.LEFT ? c.x - offset : c.x + offset;
        updateFunc = (x) => {
          c.setPosition(x, c.y);
          g.initAnimatedTiles();
          g.renderer.renderStaticCanvases();
        };
        endFunc = () => {
          c.setPosition(z.endValue, c.y);
          g.endZoning();
        };
      } else if (orientation === Orientations.UP || orientation === Orientations.DOWN) {
        const offset = (c.gridH - 2) * ts;
        startValue = orientation === Orientations.UP ? c.y - ts : c.y + ts;
        endValue = orientation === Orientations.UP ? c.y - offset : c.y + offset;
        updateFunc = (y) => {
          c.setPosition(c.x, y);
          g.initAnimatedTiles();
          g.renderer.renderStaticCanvases();
        };
        endFunc = () => {
          c.setPosition(c.x, z.endValue);
          g.endZoning();
        };
      }

      z.start(g.currentTime, updateFunc, endFunc, startValue, endValue, speed);
    }
  }

  private updateCharacter(c: Character): void {
    // Estimate of the movement distance for one update
    const tick = Math.round(16 / Math.round(c.moveSpeed / (1000 / this.game.renderer.FPS)));

    if (c.isMoving() && c.movement.inProgress === false) {
      const t = this.game.currentTime;
      switch (c.orientation) {
        case Orientations.LEFT:
          c.movement.start(t, (x) => { c.x = x; c.hasMoved(); },
            () => { c.x = c.movement.endValue; c.hasMoved(); c.nextStep(); },
            c.x - tick, c.x - 16, c.moveSpeed);
          break;
        case Orientations.RIGHT:
          c.movement.start(t, (x) => { c.x = x; c.hasMoved(); },
            () => { c.x = c.movement.endValue; c.hasMoved(); c.nextStep(); },
            c.x + tick, c.x + 16, c.moveSpeed);
          break;
        case Orientations.UP:
          c.movement.start(t, (y) => { c.y = y; c.hasMoved(); },
            () => { c.y = c.movement.endValue; c.hasMoved(); c.nextStep(); },
            c.y - tick, c.y - 16, c.moveSpeed);
          break;
        case Orientations.DOWN:
          c.movement.start(t, (y) => { c.y = y; c.hasMoved(); },
            () => { c.y = c.movement.endValue; c.hasMoved(); c.nextStep(); },
            c.y + tick, c.y + 16, c.moveSpeed);
          break;
      }
    }
  }

  private updateAnimations(): void {
    const t = this.game.currentTime;
    this.game.forEachEntity((entity) => {
      if (entity.currentAnimation?.update(t)) entity.setDirty();
    });
    this.game.sparksAnimation?.update(t);
    this.game.targetAnimation?.update(t);
  }

  updateAnimatedTiles(): void {
    const t = this.game.currentTime;
    this.game.forEachAnimatedTile((tile) => {
      if (tile.animate(t)) tile.isDirty = true;
    });
  }

  private updateChatBubbles(): void {
    this.game.bubbleManager.update(this.game.currentTime);
  }

  private updateInfos(): void {
    this.game.infoManager.update(this.game.currentTime);
  }
}