/** * A special subclass of DataField used to reference an AbstractBaseShader definition. */ class ShaderField extends foundry.data.fields.DataField { /** @inheritdoc */ static get _defaults() { const defaults = super._defaults; defaults.nullable = true; defaults.initial = undefined; return defaults; } /** @override */ _cast(value) { if ( !foundry.utils.isSubclass(value, AbstractBaseShader) ) { throw new Error("The value provided to a ShaderField must be an AbstractBaseShader subclass."); } return value; } } /** * A Vision Mode which can be selected for use by a Token. * The selected Vision Mode alters the appearance of various aspects of the canvas while that Token is the POV. */ class VisionMode extends foundry.abstract.DataModel { /** * Construct a Vision Mode using provided configuration parameters and callback functions. * @param {object} data Data which fulfills the model defined by the VisionMode schema. * @param {object} [options] Additional options passed to the DataModel constructor. */ constructor(data={}, options={}) { super(data, options); this.animated = options.animated ?? false; } /** @inheritDoc */ static defineSchema() { const fields = foundry.data.fields; const shaderSchema = () => new fields.SchemaField({ shader: new ShaderField(), uniforms: new fields.ObjectField() }); const lightingSchema = () => new fields.SchemaField({ visibility: new fields.NumberField({ initial: this.LIGHTING_VISIBILITY.ENABLED, choices: Object.values(this.LIGHTING_VISIBILITY) }), postProcessingModes: new fields.ArrayField(new fields.StringField()), uniforms: new fields.ObjectField() }); // Return model schema return { id: new fields.StringField({blank: false}), label: new fields.StringField({blank: false}), tokenConfig: new fields.BooleanField({initial: true}), canvas: new fields.SchemaField({ shader: new ShaderField(), uniforms: new fields.ObjectField() }), lighting: new fields.SchemaField({ background: lightingSchema(), coloration: lightingSchema(), illumination: lightingSchema(), darkness: lightingSchema(), levels: new fields.ObjectField({ validate: o => { const values = Object.values(CONST.LIGHTING_LEVELS); return Object.entries(o).every(([k, v]) => values.includes(Number(k)) && values.includes(v)); }, validationError: "may only contain a mapping of keys from VisionMode.LIGHTING_LEVELS" }), multipliers: new fields.ObjectField({ validate: o => { const values = Object.values(CONST.LIGHTING_LEVELS); return Object.entries(o).every(([k, v]) => values.includes(Number(k)) && Number.isFinite(v)); }, validationError: "must provide a mapping of keys from VisionMode.LIGHTING_LEVELS to numeric multiplier values" }) }), vision: new fields.SchemaField({ background: shaderSchema(), coloration: shaderSchema(), illumination: shaderSchema(), darkness: new fields.SchemaField({ adaptive: new fields.BooleanField({initial: true}) }), defaults: new fields.SchemaField({ color: new fields.ColorField({required: false, initial: undefined}), attenuation: new fields.AlphaField({required: false, initial: undefined}), brightness: new fields.NumberField({required: false, initial: undefined, nullable: false, min: -1, max: 1}), saturation: new fields.NumberField({required: false, initial: undefined, nullable: false, min: -1, max: 1}), contrast: new fields.NumberField({required: false, initial: undefined, nullable: false, min: -1, max: 1}) }), preferred: new fields.BooleanField({initial: false}) }) }; } /** * The lighting illumination levels which are supported. * @enum {number} */ static LIGHTING_LEVELS = CONST.LIGHTING_LEVELS; /** * Flags for how each lighting channel should be rendered for the currently active vision modes: * - Disabled: this lighting layer is not rendered, the shaders does not decide. * - Enabled: this lighting layer is rendered normally, and the shaders can choose if they should be rendered or not. * - Required: the lighting layer is rendered, the shaders does not decide. * @enum {number} */ static LIGHTING_VISIBILITY = { DISABLED: 0, ENABLED: 1, REQUIRED: 2 }; /** * A flag for whether this vision source is animated * @type {boolean} */ animated = false; /** * Does this vision mode enable light sources? * True unless it disables lighting entirely. * @type {boolean} */ get perceivesLight() { const {background, illumination, coloration} = this.lighting; return !!(background.visibility || illumination.visibility || coloration.visibility); } /** * Special activation handling that could be implemented by VisionMode subclasses * @param {VisionSource} source Activate this VisionMode for a specific source * @abstract */ _activate(source) {} /** * Special deactivation handling that could be implemented by VisionMode subclasses * @param {VisionSource} source Deactivate this VisionMode for a specific source * @abstract */ _deactivate(source) {} /** * Special handling which is needed when this Vision Mode is activated for a VisionSource. * @param {VisionSource} source Activate this VisionMode for a specific source */ activate(source) { if ( source._visionModeActivated ) return; source._visionModeActivated = true; this._activate(source); } /** * Special handling which is needed when this Vision Mode is deactivated for a VisionSource. * @param {VisionSource} source Deactivate this VisionMode for a specific source */ deactivate(source) { if ( !source._visionModeActivated ) return; source._visionModeActivated = false; this._deactivate(source); } /** * An animation function which runs every frame while this Vision Mode is active. * @param {number} dt The deltaTime passed by the PIXI Ticker */ animate(dt) { return foundry.canvas.sources.PointVisionSource.prototype.animateTime.call(this, dt); } }