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

525 lines
20 KiB
JavaScript

/**
* The collection of data schema and document definitions for primary documents which are shared between the both the
* client and the server.
* @namespace data
*/
import {DataModel} from "../abstract/module.mjs";
import * as fields from "./fields.mjs";
import * as documents from "../documents/_module.mjs";
import {logCompatibilityWarning} from "../utils/logging.mjs";
/**
* @typedef {import("./fields.mjs").DataFieldOptions} DataFieldOptions
* @typedef {import("./fields.mjs").FilePathFieldOptions} FilePathFieldOptions
*/
/**
* @typedef {Object} LightAnimationData
* @property {string} type The animation type which is applied
* @property {number} speed The speed of the animation, a number between 0 and 10
* @property {number} intensity The intensity of the animation, a number between 1 and 10
* @property {boolean} reverse Reverse the direction of animation.
*/
/**
* A reusable document structure for the internal data used to render the appearance of a light source.
* This is re-used by both the AmbientLightData and TokenData classes.
* @extends DataModel
* @memberof data
*
* @property {boolean} negative Is this light source a negative source? (i.e. darkness source)
* @property {number} alpha An opacity for the emitted light, if any
* @property {number} angle The angle of emission for this point source
* @property {number} bright The allowed radius of bright vision or illumination
* @property {number} color A tint color for the emitted light, if any
* @property {number} coloration The coloration technique applied in the shader
* @property {number} contrast The amount of contrast this light applies to the background texture
* @property {number} dim The allowed radius of dim vision or illumination
* @property {number} attenuation Fade the difference between bright, dim, and dark gradually?
* @property {number} luminosity The luminosity applied in the shader
* @property {number} saturation The amount of color saturation this light applies to the background texture
* @property {number} shadows The depth of shadows this light applies to the background texture
* @property {LightAnimationData} animation An animation configuration for the source
* @property {{min: number, max: number}} darkness A darkness range (min and max) for which the source should be active
*/
class LightData extends DataModel {
static defineSchema() {
return {
negative: new fields.BooleanField(),
priority: new fields.NumberField({required: true, nullable: false, integer: true, initial: 0, min: 0}),
alpha: new fields.AlphaField({initial: 0.5}),
angle: new fields.AngleField({initial: 360, normalize: false}),
bright: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
color: new fields.ColorField({}),
coloration: new fields.NumberField({required: true, integer: true, initial: 1}),
dim: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
attenuation: new fields.AlphaField({initial: 0.5}),
luminosity: new fields.NumberField({required: true, nullable: false, initial: 0.5, min: 0, max: 1}),
saturation: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1}),
contrast: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1}),
shadows: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, max: 1}),
animation: new fields.SchemaField({
type: new fields.StringField({nullable: true, blank: false, initial: null}),
speed: new fields.NumberField({required: true, nullable: false, integer: true, initial: 5, min: 0, max: 10,
validationError: "Light animation speed must be an integer between 0 and 10"}),
intensity: new fields.NumberField({required: true, nullable: false, integer: true, initial: 5, min: 1, max: 10,
validationError: "Light animation intensity must be an integer between 1 and 10"}),
reverse: new fields.BooleanField()
}),
darkness: new fields.SchemaField({
min: new fields.AlphaField({initial: 0}),
max: new fields.AlphaField({initial: 1})
}, {
validate: d => (d.min ?? 0) <= (d.max ?? 1),
validationError: "darkness.max may not be less than darkness.min"
})
}
}
/** @override */
static LOCALIZATION_PREFIXES = ["LIGHT"];
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/** @inheritDoc */
static migrateData(data) {
/**
* Migration of negative luminosity
* @deprecated since v12
*/
const luminosity = data.luminosity;
if ( luminosity < 0) {
data.luminosity = 1 - luminosity;
data.negative = true;
}
return super.migrateData(data);
}
}
/* ---------------------------------------- */
/**
* A data model intended to be used as an inner EmbeddedDataField which defines a geometric shape.
* @extends DataModel
* @memberof data
*
* @property {string} type The type of shape, a value in ShapeData.TYPES.
* For rectangles, the x/y coordinates are the top-left corner.
* For circles, the x/y coordinates are the center of the circle.
* For polygons, the x/y coordinates are the first point of the polygon.
* @property {number} [width] For rectangles, the pixel width of the shape.
* @property {number} [height] For rectangles, the pixel width of the shape.
* @property {number} [radius] For circles, the pixel radius of the shape.
* @property {number[]} [points] For polygons, the array of polygon coordinates which comprise the shape.
*/
class ShapeData extends DataModel {
static defineSchema() {
return {
type: new fields.StringField({required: true, blank: false, choices: Object.values(this.TYPES), initial: "r"}),
width: new fields.NumberField({required: false, integer: true, min: 0}),
height: new fields.NumberField({required: false, integer: true, min: 0}),
radius: new fields.NumberField({required: false, integer: true, positive: true}),
points: new fields.ArrayField(new fields.NumberField({nullable: false}))
}
}
/**
* The primitive shape types which are supported
* @enum {string}
*/
static TYPES = {
RECTANGLE: "r",
CIRCLE: "c",
ELLIPSE: "e",
POLYGON: "p"
}
}
/* ---------------------------------------- */
/**
* A data model intended to be used as an inner EmbeddedDataField which defines a geometric shape.
* @extends DataModel
* @memberof data
* @abstract
*
* @property {string} type The type of shape, a value in BaseShapeData.TYPES.
* @property {{bottom: number|null, top: number|null}} [elevation] The bottom and top elevation of the shape.
* A value of null means -/+Infinity.
* @property {boolean} [hole=false] Is this shape a hole?
*/
class BaseShapeData extends DataModel {
/**
* The possible shape types.
* @type {Readonly<{
* rectangle: RectangleShapeData,
* circle: CircleShapeData,
* ellipse: EllipseShapeData,
* polygon: PolygonShapeData
* }>}
*/
static get TYPES() {
return BaseShapeData.#TYPES ??= Object.freeze({
[RectangleShapeData.TYPE]: RectangleShapeData,
[CircleShapeData.TYPE]: CircleShapeData,
[EllipseShapeData.TYPE]: EllipseShapeData,
[PolygonShapeData.TYPE]: PolygonShapeData
});
}
static #TYPES;
/* -------------------------------------------- */
/**
* The type of this shape.
* @type {string}
*/
static TYPE = "";
/* -------------------------------------------- */
/** @override */
static defineSchema() {
return {
type: new fields.StringField({required: true, blank: false, initial: this.TYPE,
validate: value => value === this.TYPE, validationError: `must be equal to "${this.TYPE}"`}),
hole: new fields.BooleanField()
}
}
}
/* -------------------------------------------- */
/**
* The data model for a rectangular shape.
* @extends DataModel
* @memberof data
*
* @property {number} x The top-left x-coordinate in pixels before rotation.
* @property {number} y The top-left y-coordinate in pixels before rotation.
* @property {number} width The width of the rectangle in pixels.
* @property {number} height The height of the rectangle in pixels.
* @property {number} [rotation=0] The rotation around the center of the rectangle in degrees.
*/
class RectangleShapeData extends BaseShapeData {
static {
Object.defineProperty(this, "TYPE", {value: "rectangle"});
}
/** @inheritdoc */
static defineSchema() {
return Object.assign(super.defineSchema(), {
x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
width: new fields.NumberField({required: true, nullable: false, initial: undefined, positive: true}),
height: new fields.NumberField({required: true, nullable: false, initial: undefined, positive: true}),
rotation: new fields.AngleField()
});
}
}
/* -------------------------------------------- */
/**
* The data model for a circle shape.
* @extends DataModel
* @memberof data
*
* @property {number} x The x-coordinate of the center point in pixels.
* @property {number} y The y-coordinate of the center point in pixels.
* @property {number} radius The radius of the circle in pixels.
*/
class CircleShapeData extends BaseShapeData {
static {
Object.defineProperty(this, "TYPE", {value: "circle"});
}
/** @inheritdoc */
static defineSchema() {
return Object.assign(super.defineSchema(), {
x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
radius: new fields.NumberField({required: true, nullable: false, initial: undefined, positive: true})
});
}
}
/* -------------------------------------------- */
/**
* The data model for an ellipse shape.
* @extends DataModel
* @memberof data
*
* @property {number} x The x-coordinate of the center point in pixels.
* @property {number} y The y-coordinate of the center point in pixels.
* @property {number} radiusX The x-radius of the circle in pixels.
* @property {number} radiusY The y-radius of the circle in pixels.
* @property {number} [rotation=0] The rotation around the center of the rectangle in degrees.
*/
class EllipseShapeData extends BaseShapeData {
static {
Object.defineProperty(this, "TYPE", {value: "ellipse"});
}
/** @inheritdoc */
static defineSchema() {
return Object.assign(super.defineSchema(), {
x: new fields.NumberField({required: true, nullable: false, initial: undefined}),
y: new fields.NumberField({required: true, nullable: false, initial: undefined}),
radiusX: new fields.NumberField({required: true, nullable: false, initial: undefined, positive: true}),
radiusY: new fields.NumberField({required: true, nullable: false, initial: undefined, positive: true}),
rotation: new fields.AngleField()
});
}
}
/* -------------------------------------------- */
/**
* The data model for a polygon shape.
* @extends DataModel
* @memberof data
*
* @property {number[]} points The points of the polygon ([x0, y0, x1, y1, ...]).
* The polygon must not be self-intersecting.
*/
class PolygonShapeData extends BaseShapeData {
static {
Object.defineProperty(this, "TYPE", {value: "polygon"});
}
/** @inheritdoc */
static defineSchema() {
return Object.assign(super.defineSchema(), {
points: new fields.ArrayField(new fields.NumberField({required: true, nullable: false, initial: undefined}),
{validate: value => {
if ( value.length % 2 !== 0 ) throw new Error("must have an even length");
if ( value.length < 6 ) throw new Error("must have at least 3 points");
}}),
});
}
}
/* ---------------------------------------- */
/**
* A {@link fields.SchemaField} subclass used to represent texture data.
* @property {string|null} src The URL of the texture source.
* @property {number} [anchorX=0] The X coordinate of the texture anchor.
* @property {number} [anchorY=0] The Y coordinate of the texture anchor.
* @property {number} [scaleX=1] The scale of the texture in the X dimension.
* @property {number} [scaleY=1] The scale of the texture in the Y dimension.
* @property {number} [offsetX=0] The X offset of the texture with (0,0) in the top left.
* @property {number} [offsetY=0] The Y offset of the texture with (0,0) in the top left.
* @property {number} [rotation=0] An angle of rotation by which this texture is rotated around its center.
* @property {string} [tint="#ffffff"] The tint applied to the texture.
* @property {number} [alphaThreshold=0] Only pixels with an alpha value at or above this value are consider solid
* w.r.t. to occlusion testing and light/weather blocking.
*/
class TextureData extends fields.SchemaField {
/**
* @param {DataFieldOptions} options Options which are forwarded to the SchemaField constructor
* @param {FilePathFieldOptions} srcOptions Additional options for the src field
*/
constructor(options={}, {categories=["IMAGE", "VIDEO"], initial={}, wildcard=false, label=""}={}) {
/** @deprecated since v12 */
if ( typeof initial === "string" ) {
const msg = "Passing the initial value of the src field as a string is deprecated. Pass {src} instead.";
logCompatibilityWarning(msg, {since: 12, until: 14});
initial = {src: initial};
}
super({
src: new fields.FilePathField({categories, initial: initial.src ?? null, label, wildcard}),
anchorX: new fields.NumberField({nullable: false, initial: initial.anchorX ?? 0}),
anchorY: new fields.NumberField({nullable: false, initial: initial.anchorY ?? 0}),
offsetX: new fields.NumberField({nullable: false, integer: true, initial: initial.offsetX ?? 0}),
offsetY: new fields.NumberField({nullable: false, integer: true, initial: initial.offsetY ?? 0}),
fit: new fields.StringField({initial: initial.fit ?? "fill", choices: CONST.TEXTURE_DATA_FIT_MODES}),
scaleX: new fields.NumberField({nullable: false, initial: initial.scaleX ?? 1}),
scaleY: new fields.NumberField({nullable: false, initial: initial.scaleY ?? 1}),
rotation: new fields.AngleField({initial: initial.rotation ?? 0}),
tint: new fields.ColorField({nullable: false, initial: initial.tint ?? "#ffffff"}),
alphaThreshold: new fields.AlphaField({nullable: false, initial: initial.alphaThreshold ?? 0})
}, options);
}
}
/* ---------------------------------------- */
/**
* Extend the base TokenData to define a PrototypeToken which exists within a parent Actor.
* @extends abstract.DataModel
* @memberof data
* @property {boolean} randomImg Does the prototype token use a random wildcard image?
* @alias {PrototypeToken}
*/
class PrototypeToken extends DataModel {
constructor(data={}, options={}) {
super(data, options);
Object.defineProperty(this, "apps", {value: {}});
}
/** @override */
static defineSchema() {
const schema = documents.BaseToken.defineSchema();
const excluded = ["_id", "actorId", "delta", "x", "y", "elevation", "sort", "hidden", "locked", "_regions"];
for ( let x of excluded ) {
delete schema[x];
}
schema.name.textSearch = schema.name.options.textSearch = false;
schema.randomImg = new fields.BooleanField();
PrototypeToken.#applyDefaultTokenSettings(schema);
return schema;
}
/** @override */
static LOCALIZATION_PREFIXES = ["TOKEN"];
/**
* The Actor which owns this Prototype Token
* @type {documents.BaseActor}
*/
get actor() {
return this.parent;
}
/** @inheritdoc */
toObject(source=true) {
const data = super.toObject(source);
data["actorId"] = this.document?.id;
return data;
}
/**
* @see ClientDocument.database
* @ignore
*/
static get database() {
return globalThis.CONFIG.DatabaseBackend;
}
/* -------------------------------------------- */
/**
* Apply configured default token settings to the schema.
* @param {DataSchema} [schema] The schema to apply the settings to.
*/
static #applyDefaultTokenSettings(schema) {
if ( typeof DefaultTokenConfig === "undefined" ) return;
const settings = foundry.utils.flattenObject(game.settings.get("core", DefaultTokenConfig.SETTING) ?? {});
for ( const [k, v] of Object.entries(settings) ) {
const path = k.split(".");
let field = schema[path.shift()];
if ( path.length ) field = field._getField(path);
if ( field ) field.initial = v;
}
}
/* -------------------------------------------- */
/* Document Compatibility Methods */
/* -------------------------------------------- */
/**
* @see abstract.Document#update
* @ignore
*/
update(data, options) {
return this.actor.update({prototypeToken: data}, options);
}
/* -------------------------------------------- */
/**
* @see abstract.Document#getFlag
* @ignore
*/
getFlag(...args) {
return foundry.abstract.Document.prototype.getFlag.call(this, ...args);
}
/* -------------------------------------------- */
/**
* @see abstract.Document#getFlag
* @ignore
*/
setFlag(...args) {
return foundry.abstract.Document.prototype.setFlag.call(this, ...args);
}
/* -------------------------------------------- */
/**
* @see abstract.Document#unsetFlag
* @ignore
*/
async unsetFlag(...args) {
return foundry.abstract.Document.prototype.unsetFlag.call(this, ...args);
}
/* -------------------------------------------- */
/**
* @see abstract.Document#testUserPermission
* @ignore
*/
testUserPermission(user, permission, {exact=false}={}) {
return this.actor.testUserPermission(user, permission, {exact});
}
/* -------------------------------------------- */
/**
* @see documents.BaseActor#isOwner
* @ignore
*/
get isOwner() {
return this.actor.isOwner;
}
}
/* -------------------------------------------- */
/**
* A minimal data model used to represent a tombstone entry inside an EmbeddedCollectionDelta.
* @see {EmbeddedCollectionDelta}
* @extends DataModel
* @memberof data
*
* @property {string} _id The _id of the base Document that this tombstone represents.
* @property {boolean} _tombstone A property that identifies this entry as a tombstone.
*/
class TombstoneData extends DataModel {
/** @override */
static defineSchema() {
return {
_id: new fields.DocumentIdField(),
_tombstone: new fields.BooleanField({initial: true, validate: v => v === true, validationError: "must be true"})
};
}
}
// Exports need to be at the bottom so that class names appear correctly in JSDoc
export {
LightData,
PrototypeToken,
ShapeData,
BaseShapeData,
RectangleShapeData,
CircleShapeData,
EllipseShapeData,
PolygonShapeData,
TextureData,
TombstoneData
}