UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

196 lines (195 loc) 5.99 kB
import * as Color from "./color.js"; ; ; ; ; /** * Lighting computation, based on a traditional FOV for multiple light sources and multiple passes. */ export default class Lighting { constructor(reflectivityCallback, options = {}) { this._reflectivityCallback = reflectivityCallback; this._options = {}; options = Object.assign({ passes: 1, emissionThreshold: 100, range: 10 }, options); this._lights = {}; this._reflectivityCache = {}; this._fovCache = {}; this.setOptions(options); } /** * Adjust options at runtime */ setOptions(options) { Object.assign(this._options, options); if (options && options.range) { this.reset(); } return this; } /** * Set the used Field-Of-View algo */ setFOV(fov) { this._fov = fov; this._fovCache = {}; return this; } /** * Set (or remove) a light source */ setLight(x, y, color) { let key = x + "," + y; if (color) { this._lights[key] = (typeof (color) == "string" ? Color.fromString(color) : color); } else { delete this._lights[key]; } return this; } /** * Remove all light sources */ clearLights() { this._lights = {}; } /** * Reset the pre-computed topology values. Call whenever the underlying map changes its light-passability. */ reset() { this._reflectivityCache = {}; this._fovCache = {}; return this; } /** * Compute the lighting */ compute(lightingCallback) { let doneCells = {}; let emittingCells = {}; let litCells = {}; for (let key in this._lights) { /* prepare emitters for first pass */ let light = this._lights[key]; emittingCells[key] = [0, 0, 0]; Color.add_(emittingCells[key], light); } for (let i = 0; i < this._options.passes; i++) { /* main loop */ this._emitLight(emittingCells, litCells, doneCells); if (i + 1 == this._options.passes) { continue; } /* not for the last pass */ emittingCells = this._computeEmitters(litCells, doneCells); } for (let litKey in litCells) { /* let the user know what and how is lit */ let parts = litKey.split(","); let x = parseInt(parts[0]); let y = parseInt(parts[1]); lightingCallback(x, y, litCells[litKey]); } return this; } /** * Compute one iteration from all emitting cells * @param emittingCells These emit light * @param litCells Add projected light to these * @param doneCells These already emitted, forbid them from further calculations */ _emitLight(emittingCells, litCells, doneCells) { for (let key in emittingCells) { let parts = key.split(","); let x = parseInt(parts[0]); let y = parseInt(parts[1]); this._emitLightFromCell(x, y, emittingCells[key], litCells); doneCells[key] = 1; } return this; } /** * Prepare a list of emitters for next pass */ _computeEmitters(litCells, doneCells) { let result = {}; for (let key in litCells) { if (key in doneCells) { continue; } /* already emitted */ let color = litCells[key]; let reflectivity; if (key in this._reflectivityCache) { reflectivity = this._reflectivityCache[key]; } else { let parts = key.split(","); let x = parseInt(parts[0]); let y = parseInt(parts[1]); reflectivity = this._reflectivityCallback(x, y); this._reflectivityCache[key] = reflectivity; } if (reflectivity == 0) { continue; } /* will not reflect at all */ /* compute emission color */ let emission = [0, 0, 0]; let intensity = 0; for (let i = 0; i < 3; i++) { let part = Math.round(color[i] * reflectivity); emission[i] = part; intensity += part; } if (intensity > this._options.emissionThreshold) { result[key] = emission; } } return result; } /** * Compute one iteration from one cell */ _emitLightFromCell(x, y, color, litCells) { let key = x + "," + y; let fov; if (key in this._fovCache) { fov = this._fovCache[key]; } else { fov = this._updateFOV(x, y); } for (let fovKey in fov) { let formFactor = fov[fovKey]; let result; if (fovKey in litCells) { /* already lit */ result = litCells[fovKey]; } else { /* newly lit */ result = [0, 0, 0]; litCells[fovKey] = result; } for (let i = 0; i < 3; i++) { result[i] += Math.round(color[i] * formFactor); } /* add light color */ } return this; } /** * Compute FOV ("form factor") for a potential light source at [x,y] */ _updateFOV(x, y) { let key1 = x + "," + y; let cache = {}; this._fovCache[key1] = cache; let range = this._options.range; function cb(x, y, r, vis) { let key2 = x + "," + y; let formFactor = vis * (1 - r / range); if (formFactor == 0) { return; } cache[key2] = formFactor; } ; this._fov.compute(x, y, range, cb.bind(this)); return cache; } }