import RenderedEffectSource from "./rendered-effect-source.mjs"; import PointEffectSourceMixin from "./point-effect-source.mjs"; import {LIGHTING_LEVELS} from "../../../common/constants.mjs"; /** * @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData * @typedef {import("./rendered-effect-source-source.mjs").RenderedEffectSourceData} RenderedEffectSourceData */ /** * @typedef {Object} VisionSourceData * @property {number} contrast The amount of contrast * @property {number} attenuation Strength of the attenuation between bright, dim, and dark * @property {number} saturation The amount of color saturation * @property {number} brightness The vision brightness. * @property {string} visionMode The vision mode. * @property {number} lightRadius The range of light perception. * @property {boolean} blinded Is this vision source blinded? */ /** * A specialized subclass of RenderedEffectSource which represents a source of point-based vision. * @extends {RenderedEffectSource} */ export default class PointVisionSource extends PointEffectSourceMixin(RenderedEffectSource) { /** @inheritdoc */ static sourceType = "sight"; /** @override */ static _initializeShaderKeys = ["visionMode", "blinded"]; /** @override */ static _refreshUniformsKeys = ["radius", "color", "attenuation", "brightness", "contrast", "saturation", "visionMode"]; /** * The corresponding lighting levels for dim light. * @type {number} * @protected */ static _dimLightingLevel = LIGHTING_LEVELS.DIM; /** * The corresponding lighting levels for bright light. * @type {string} * @protected */ static _brightLightingLevel = LIGHTING_LEVELS.BRIGHT; /** @inheritdoc */ static EDGE_OFFSET = -2; /** @override */ static effectsCollection = "visionSources"; /** @inheritDoc */ static defaultData = { ...super.defaultData, contrast: 0, attenuation: 0.5, saturation: 0, brightness: 0, visionMode: "basic", lightRadius: null } /** @override */ static get _layers() { return foundry.utils.mergeObject(super._layers, { background: { defaultShader: BackgroundVisionShader }, coloration: { defaultShader: ColorationVisionShader }, illumination: { defaultShader: IlluminationVisionShader } }); } /* -------------------------------------------- */ /* Vision Source Attributes */ /* -------------------------------------------- */ /** * The vision mode linked to this VisionSource * @type {VisionMode|null} */ visionMode = null; /** * The vision mode activation flag for handlers * @type {boolean} * @internal */ _visionModeActivated = false; /** * The unconstrained LOS polygon. * @type {PointSourcePolygon} */ los; /** * The polygon of light perception. * @type {PointSourcePolygon} */ light; /* -------------------------------------------- */ /** * An alias for the shape of the vision source. * @type {PointSourcePolygon|PIXI.Polygon} */ get fov() { return this.shape; } /* -------------------------------------------- */ /** * If this vision source background is rendered into the lighting container. * @type {boolean} */ get preferred() { return this.visionMode?.vision.preferred; } /* -------------------------------------------- */ /** * Is the rendered source animated? * @type {boolean} */ get isAnimated() { return this.active && this.data.animation && this.visionMode?.animated; } /* -------------------------------------------- */ /** * Light perception radius of this vision source, taking into account if the source is blinded. * @type {number} */ get lightRadius() { return this.#hasBlindedVisionMode ? 0 : (this.data.lightRadius ?? 0); } /* -------------------------------------------- */ /** @override */ get radius() { return (this.#hasBlindedVisionMode ? this.data.externalRadius : this.data.radius) ?? 0; } /* -------------------------------------------- */ /* Point Vision Source Blinded Management */ /* -------------------------------------------- */ /** * Is this source temporarily blinded? * @type {boolean} */ get isBlinded() { return (this.data.radius === 0) && ((this.data.lightRadius === 0) || !this.visionMode?.perceivesLight) || Object.values(this.blinded).includes(true); }; /** * Records of blinding strings with a boolean value. * By default, if any of this record is true, the source is blinded. * @type {Record} */ blinded = {}; /** * Data overrides that could happen with blindness vision mode. * @type {object} */ visionModeOverrides = {}; /* -------------------------------------------- */ /** * Update darkness blinding according to darkness sources collection. */ #updateBlindedState() { this.blinded.darkness = canvas.effects.testInsideDarkness({x: this.x, y: this.y}, this.elevation); } /* -------------------------------------------- */ /** * To know if blindness vision mode is configured for this source. * Note: Convenient method used to avoid calling this.blinded which is costly. * @returns {boolean} */ get #hasBlindedVisionMode() { return this.visionMode === CONFIG.Canvas.visionModes.blindness; } /* -------------------------------------------- */ /* Vision Source Initialization */ /* -------------------------------------------- */ /** @inheritDoc */ _initialize(data) { super._initialize(data); this.data.lightRadius ??= canvas.dimensions.maxR; if ( this.data.lightRadius > 0 ) this.data.lightRadius = Math.max(this.data.lightRadius, this.data.externalRadius); if ( this.data.radius > 0 ) this.data.radius = Math.max(this.data.radius, this.data.externalRadius); if ( !(this.data.visionMode in CONFIG.Canvas.visionModes) ) this.data.visionMode = "basic"; } /* -------------------------------------------- */ /** @inheritDoc */ _createShapes() { this._updateVisionMode(); super._createShapes(); this.los = this.shape; this.light = this._createLightPolygon(); this.shape = this._createRestrictedPolygon(); } /* -------------------------------------------- */ /** * Responsible for assigning the Vision Mode and calling the activation and deactivation handlers. * @protected */ _updateVisionMode() { const previousVM = this.visionMode; this.visionMode = CONFIG.Canvas.visionModes[this.data.visionMode]; // Check blinding conditions this.#updateBlindedState(); // Apply vision mode according to conditions if ( this.isBlinded ) this.visionMode = CONFIG.Canvas.visionModes.blindness; // Process vision mode overrides for blindness const defaults = this.visionMode.vision.defaults; const data = this.data; const applyOverride = prop => this.#hasBlindedVisionMode && (defaults[prop] !== undefined) ? defaults[prop] : data[prop]; const blindedColor = applyOverride("color"); this.visionModeOverrides.colorRGB = blindedColor !== null ? Color.from(blindedColor).rgb : null; this.visionModeOverrides.brightness = applyOverride("brightness"); this.visionModeOverrides.contrast = applyOverride("contrast"); this.visionModeOverrides.saturation = applyOverride("saturation"); this.visionModeOverrides.attenuation = applyOverride("attenuation"); // Process deactivation and activation handlers if ( this.visionMode !== previousVM ) previousVM?.deactivate(this); } /* -------------------------------------------- */ /** @inheritDoc */ _configure(changes) { this.visionMode.activate(this); super._configure(changes); this.animation.animation = this.visionMode.animate; } /* -------------------------------------------- */ /** @override */ _configureLayer(layer, layerId) { const vmUniforms = this.visionMode.vision[layerId].uniforms; layer.vmUniforms = Object.entries(vmUniforms); } /* -------------------------------------------- */ /** @inheritDoc */ _getPolygonConfiguration() { return Object.assign(super._getPolygonConfiguration(), { radius: this.data.disabled || this.suppressed ? 0 : (this.blinded.darkness ? this.data.externalRadius : canvas.dimensions.maxR), useThreshold: true, includeDarkness: true }); } /* -------------------------------------------- */ /** * Creates the polygon that represents light perception. * If the light perception radius is unconstrained, no new polygon instance is created; * instead the LOS polygon of this vision source is returned. * @returns {PointSourcePolygon} The new polygon or `this.los`. * @protected */ _createLightPolygon() { return this.#createConstrainedPolygon(this.lightRadius); } /* -------------------------------------------- */ /** * Create a restricted FOV polygon by limiting the radius of the unrestricted LOS polygon. * If the vision radius is unconstrained, no new polygon instance is created; * instead the LOS polygon of this vision source is returned. * @returns {PointSourcePolygon} The new polygon or `this.los`. * @protected */ _createRestrictedPolygon() { return this.#createConstrainedPolygon(this.radius || this.data.externalRadius); } /* -------------------------------------------- */ /** * Create a constrained polygon by limiting the radius of the unrestricted LOS polygon. * If the radius is unconstrained, no new polygon instance is created; * instead the LOS polygon of this vision source is returned. * @param {number} radius The radius to constraint to. * @returns {PointSourcePolygon} The new polygon or `this.los`. */ #createConstrainedPolygon(radius) { if ( radius >= this.los.config.radius ) return this.los; const {x, y} = this.data; const circle = new PIXI.Circle(x, y, radius); const density = PIXI.Circle.approximateVertexDensity(radius); return this.los.applyConstraint(circle, {density, scalingFactor: 100}); } /* -------------------------------------------- */ /* Shader Management */ /* -------------------------------------------- */ /** @override */ _configureShaders() { const vm = this.visionMode.vision; const shaders = {}; for ( const layer in this.layers ) { shaders[layer] = vm[`${layer.toLowerCase()}`]?.shader || this.layers[layer].defaultShader; } return shaders; } /* -------------------------------------------- */ /** @inheritDoc */ _updateColorationUniforms() { super._updateColorationUniforms(); const shader = this.layers.coloration.shader; if ( !shader ) return; const u = shader?.uniforms; const d = shader.constructor.defaultUniforms; u.colorEffect = this.visionModeOverrides.colorRGB ?? d.colorEffect; u.useSampler = true; const vmUniforms = this.layers.coloration.vmUniforms; if ( vmUniforms.length ) this._updateVisionModeUniforms(shader, vmUniforms); } /* -------------------------------------------- */ /** @inheritDoc */ _updateIlluminationUniforms() { super._updateIlluminationUniforms(); const shader = this.layers.illumination.shader; if ( !shader ) return; shader.uniforms.useSampler = false; // We don't need to use the background sampler into vision illumination const vmUniforms = this.layers.illumination.vmUniforms; if ( vmUniforms.length ) this._updateVisionModeUniforms(shader, vmUniforms); } /* -------------------------------------------- */ /** @inheritDoc */ _updateBackgroundUniforms() { super._updateBackgroundUniforms(); const shader = this.layers.background.shader; if ( !shader ) return; const u = shader.uniforms; u.technique = 0; u.contrast = this.visionModeOverrides.contrast; u.useSampler = true; const vmUniforms = this.layers.background.vmUniforms; if ( vmUniforms.length ) this._updateVisionModeUniforms(shader, vmUniforms); } /* -------------------------------------------- */ /** @inheritDoc */ _updateCommonUniforms(shader) { const u = shader.uniforms; const d = shader.constructor.defaultUniforms; const c = canvas.colors; // Passing common environment values u.computeIllumination = true; u.darknessLevel = canvas.environment.darknessLevel; c.ambientBrightest.applyRGB(u.ambientBrightest); c.ambientDarkness.applyRGB(u.ambientDarkness); c.ambientDaylight.applyRGB(u.ambientDaylight); u.weights[0] = canvas.environment.weights.dark; u.weights[1] = canvas.environment.weights.halfdark; u.weights[2] = canvas.environment.weights.dim; u.weights[3] = canvas.environment.weights.bright; u.dimLevelCorrection = this.constructor._dimLightingLevel; u.brightLevelCorrection = this.constructor._brightLightingLevel; // Vision values const attenuation = this.visionModeOverrides.attenuation; u.attenuation = Math.max(attenuation, 0.0125); const brightness = this.visionModeOverrides.brightness; u.brightness = (brightness + 1) / 2; u.saturation = this.visionModeOverrides.saturation; u.linkedToDarknessLevel = this.visionMode.vision.darkness.adaptive; // Other values u.elevation = this.data.elevation; u.screenDimensions = canvas.screenDimensions; u.colorTint = this.visionModeOverrides.colorRGB ?? d.colorTint; // Textures if ( !u.depthTexture ) u.depthTexture = canvas.masks.depth.renderTexture; if ( !u.primaryTexture ) u.primaryTexture = canvas.primary.renderTexture; if ( !u.darknessLevelTexture ) u.darknessLevelTexture = canvas.effects.illumination.renderTexture; } /* -------------------------------------------- */ /** * Update layer uniforms according to vision mode uniforms, if any. * @param {AdaptiveVisionShader} shader The shader being updated. * @param {Array} vmUniforms The targeted layer. * @protected */ _updateVisionModeUniforms(shader, vmUniforms) { const shaderUniforms = shader.uniforms; for ( const [uniform, value] of vmUniforms ) { if ( Array.isArray(value) ) { const u = (shaderUniforms[uniform] ??= []); for ( const i in value ) u[i] = value[i]; } else shaderUniforms[uniform] = value; } } }