Files
2025-01-04 00:34:03 +01:00

179 lines
6.3 KiB
JavaScript

/**
* 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);
}
}