Initial
This commit is contained in:
33
resources/app/common/documents/_module.mjs
Normal file
33
resources/app/common/documents/_module.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
/** @module foundry.documents */
|
||||
export {default as BaseActiveEffect} from "./active-effect.mjs";
|
||||
export {default as BaseActorDelta} from "./actor-delta.mjs";
|
||||
export {default as BaseActor} from "./actor.mjs";
|
||||
export {default as BaseAdventure} from "./adventure.mjs";
|
||||
export {default as BaseAmbientLight} from "./ambient-light.mjs";
|
||||
export {default as BaseAmbientSound} from "./ambient-sound.mjs";
|
||||
export {default as BaseCard} from "./card.mjs";
|
||||
export {default as BaseCards} from "./cards.mjs";
|
||||
export {default as BaseChatMessage} from "./chat-message.mjs";
|
||||
export {default as BaseCombat} from "./combat.mjs";
|
||||
export {default as BaseCombatant} from "./combatant.mjs";
|
||||
export {default as BaseDrawing} from "./drawing.mjs";
|
||||
export {default as BaseFogExploration} from "./fog-exploration.mjs";
|
||||
export {default as BaseFolder} from "./folder.mjs";
|
||||
export {default as BaseItem} from "./item.mjs";
|
||||
export {default as BaseJournalEntry} from "./journal-entry.mjs";
|
||||
export {default as BaseJournalEntryPage} from "./journal-entry-page.mjs";
|
||||
export {default as BaseMacro} from "./macro.mjs";
|
||||
export {default as BaseMeasuredTemplate} from "./measured-template.mjs";
|
||||
export {default as BaseNote} from "./note.mjs";
|
||||
export {default as BasePlaylist} from "./playlist.mjs";
|
||||
export {default as BasePlaylistSound} from "./playlist-sound.mjs";
|
||||
export {default as BaseRollTable} from "./roll-table.mjs";
|
||||
export {default as BaseScene} from "./scene.mjs";
|
||||
export {default as BaseRegion} from "./region.mjs";
|
||||
export {default as BaseRegionBehavior} from "./region-behavior.mjs";
|
||||
export {default as BaseSetting} from "./setting.mjs";
|
||||
export {default as BaseTableResult} from "./table-result.mjs";
|
||||
export {default as BaseTile} from "./tile.mjs";
|
||||
export {default as BaseToken} from "./token.mjs";
|
||||
export {default as BaseUser} from "./user.mjs";
|
||||
export {default as BaseWall} from "./wall.mjs";
|
||||
727
resources/app/common/documents/_types.mjs
Normal file
727
resources/app/common/documents/_types.mjs
Normal file
@@ -0,0 +1,727 @@
|
||||
/**
|
||||
* @typedef {Object} ActiveEffectData
|
||||
* @property {string} _id The _id that uniquely identifies the ActiveEffect within its parent collection
|
||||
* @property {string} name The name of the which describes the name of the ActiveEffect
|
||||
* @property {string} img An image path used to depict the ActiveEffect as an icon
|
||||
* @property {EffectChangeData[]} changes The array of EffectChangeData objects which the ActiveEffect applies
|
||||
* @property {boolean} [disabled=false] Is this ActiveEffect currently disabled?
|
||||
* @property {EffectDurationData} [duration] An EffectDurationData object which describes the duration of the ActiveEffect
|
||||
* @property {string} [description] The HTML text description for this ActiveEffect document.
|
||||
* @property {string} [origin] A UUID reference to the document from which this ActiveEffect originated
|
||||
* @property {string} [tint=null] A color string which applies a tint to the ActiveEffect icon
|
||||
* @property {boolean} [transfer=false] Does this ActiveEffect automatically transfer from an Item to an Actor?
|
||||
* @property {Set<string>} [statuses] Special status IDs that pertain to this effect
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EffectDurationData
|
||||
* @property {number} [startTime] The world time when the active effect first started
|
||||
* @property {number} [seconds] The maximum duration of the effect, in seconds
|
||||
* @property {string} [combat] The _id of the CombatEncounter in which the effect first started
|
||||
* @property {number} [rounds] The maximum duration of the effect, in combat rounds
|
||||
* @property {number} [turns] The maximum duration of the effect, in combat turns
|
||||
* @property {number} [startRound] The round of the CombatEncounter in which the effect first started
|
||||
* @property {number} [startTurn] The turn of the CombatEncounter in which the effect first started
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EffectChangeData
|
||||
* @property {string} key The attribute path in the Actor or Item data which the change modifies
|
||||
* @property {string} value The value of the change effect
|
||||
* @property {number} mode The modification mode with which the change is applied
|
||||
* @property {number} priority The priority level with which this change is applied
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActorData
|
||||
* @property {string} _id The _id which uniquely identifies this Actor document
|
||||
* @property {string} name The name of this Actor
|
||||
* @property {string} type An Actor subtype which configures the system data model applied
|
||||
* @property {string} [img] An image file path which provides the artwork for this Actor
|
||||
* @property {object} [system] The system data object which is defined by the system template.json model
|
||||
* @property {data.PrototypeToken} [prototypeToken] Default Token settings which are used for Tokens created from
|
||||
* this Actor
|
||||
* @property {Collection<documents.BaseItem>} items A Collection of Item embedded Documents
|
||||
* @property {Collection<documents.BaseActiveEffect>} effects A Collection of ActiveEffect embedded Documents
|
||||
* @property {string|null} folder The _id of a Folder which contains this Actor
|
||||
* @property {number} [sort] The numeric sort value which orders this Actor relative to its siblings
|
||||
* @property {object} [ownership] An object which configures ownership of this Actor
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ActorDeltaData
|
||||
* @property {string} _id The _id which uniquely identifies this ActorDelta document
|
||||
* @property {string} [name] The name override, if any.
|
||||
* @property {string} [type] The type override, if any.
|
||||
* @property {string} [img] The image override, if any.
|
||||
* @property {object} [system] The system data model override.
|
||||
* @property {Collection<BaseItem>} [items] An array of embedded item data overrides.
|
||||
* @property {Collection<BaseActiveEffect>} [effects] An array of embedded active effect data overrides.
|
||||
* @property {object} [ownership] Ownership overrides.
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AdventureData
|
||||
* @property {string} _id The _id which uniquely identifies this Adventure document
|
||||
* @property {string} name The human-readable name of the Adventure
|
||||
* @property {string} img The file path for the primary image of the adventure
|
||||
* @property {string} caption A string caption displayed under the primary image banner
|
||||
* @property {string} description An HTML text description for the adventure
|
||||
* @property {foundry.documents.BaseActor[]} actors An array of included Actor documents
|
||||
* @property {foundry.documents.BaseCombat[]} combats An array of included Combat documents
|
||||
* @property {foundry.documents.BaseItem[]} items An array of included Item documents
|
||||
* @property {foundry.documents.BaseScene[]} scenes An array of included Scene documents
|
||||
* @property {foundry.documents.BaseJournalEntry[]} journal An array of included JournalEntry documents
|
||||
* @property {foundry.documents.BaseRollTable[]} tables An array of included RollTable documents
|
||||
* @property {foundry.documents.BaseMacro[]} macros An array of included Macro documents
|
||||
* @property {foundry.documents.BaseCards[]} cards An array of included Cards documents
|
||||
* @property {foundry.documents.BasePlaylist[]} playlists An array of included Playlist documents
|
||||
* @property {foundry.documents.BaseFolder[]} folders An array of included Folder documents
|
||||
* @property {number} sort The sort order of this adventure relative to its siblings
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AmbientLightData
|
||||
* @property {string} _id The _id which uniquely identifies this AmbientLight document
|
||||
* @property {number} x The x-coordinate position of the origin of the light
|
||||
* @property {number} y The y-coordinate position of the origin of the light
|
||||
* @property {number} [rotation=0] The angle of rotation for the tile between 0 and 360
|
||||
* @property {boolean} [walls=true] Whether or not this light source is constrained by Walls
|
||||
* @property {boolean} [vision=false] Whether or not this light source provides a source of vision
|
||||
* @property {LightData} config Light configuration data
|
||||
* @property {boolean} [hidden=false] Is the light source currently hidden?
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AmbientSoundData
|
||||
* @property {string} _id The _id which uniquely identifies this AmbientSound document
|
||||
* @property {number} x The x-coordinate position of the origin of the sound.
|
||||
* @property {number} y The y-coordinate position of the origin of the sound.
|
||||
* @property {number} radius The radius of the emitted sound.
|
||||
* @property {string} path The audio file path that is played by this sound
|
||||
* @property {boolean} [repeat=false] Does this sound loop?
|
||||
* @property {number} [volume=0.5] The audio volume of the sound, from 0 to 1
|
||||
* @property {boolean} walls Whether or not this sound source is constrained by Walls. True by default.
|
||||
* @property {boolean} easing Whether to adjust the volume of the sound heard by the listener based on how
|
||||
* close the listener is to the center of the sound source. True by default.
|
||||
* @property {boolean} hidden Is the sound source currently hidden? False by default.
|
||||
* @property {{min: number, max: number}} darkness A darkness range (min and max) for which the source should be active
|
||||
* @property {{base: AmbientSoundEffect, muffled: AmbientSoundEffect}} effects Special effects to apply to the sound
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AmbientSoundEffect
|
||||
* @param {string} type The type of effect in CONFIG.soundEffects
|
||||
* @param {number} intensity The intensity of the effect on the scale of [1, 10]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CardData
|
||||
* @property {string} _id The _id which uniquely identifies this Card document
|
||||
* @property {string} name The text name of this card
|
||||
* @property {string} [description] A text description of this card which applies to all faces
|
||||
* @property {string} type A category of card (for example, a suit) to which this card belongs
|
||||
* @property {object} [system] Game system data which is defined by the system template.json model
|
||||
* @property {string} [suit] An optional suit designation which is used by default sorting
|
||||
* @property {number} [value] An optional numeric value of the card which is used by default sorting
|
||||
* @property {CardFaceData} back An object of face data which describes the back of this card
|
||||
* @property {CardFaceData[]} faces An array of face data which represent displayable faces of this card
|
||||
* @property {number|null} face The index of the currently displayed face, or null if the card is face-down
|
||||
* @property {boolean} drawn Whether this card is currently drawn from its source deck
|
||||
* @property {string} origin The document ID of the origin deck to which this card belongs
|
||||
* @property {number} width The visible width of this card
|
||||
* @property {number} height The visible height of this card
|
||||
* @property {number} rotation The angle of rotation of this card
|
||||
* @property {number} sort The sort order of this card relative to others in the same stack
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CardFaceData
|
||||
* @property {string} [name] A name for this card face
|
||||
* @property {string} [text] Displayed text that belongs to this face
|
||||
* @property {string} [img] A displayed image or video file which depicts the face
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CardsData
|
||||
* @property {string} _id The _id which uniquely identifies this stack of Cards document
|
||||
* @property {string} name The text name of this stack
|
||||
* @property {string} type The type of this stack, in BaseCards.metadata.types
|
||||
* @property {object} [system] Game system data which is defined by the system template.json model
|
||||
* @property {string} [description] A text description of this stack
|
||||
* @property {string} [img] An image or video which is used to represent the stack of cards
|
||||
* @property {Collection<BaseCard>} cards A collection of Card documents which currently belong to this stack
|
||||
* @property {number} width The visible width of this stack
|
||||
* @property {number} height The visible height of this stack
|
||||
* @property {number} rotation The angle of rotation of this stack
|
||||
* @property {boolean} [displayCount] Whether or not to publicly display the number of cards in this stack
|
||||
* @property {string|null} folder The _id of a Folder which contains this document
|
||||
* @property {number} sort The sort order of this stack relative to others in its parent collection
|
||||
* @property {object} [ownership] An object which configures ownership of this Cards
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChatMessageData
|
||||
* @property {string} _id The _id which uniquely identifies this ChatMessage document
|
||||
* @property {number} [type=0] The message type from CONST.CHAT_MESSAGE_TYPES
|
||||
* @property {string} user The _id of the User document who generated this message
|
||||
* @property {number} timestamp The timestamp at which point this message was generated
|
||||
* @property {string} [flavor] An optional flavor text message which summarizes this message
|
||||
* @property {string} content The HTML content of this chat message
|
||||
* @property {ChatSpeakerData} speaker A ChatSpeakerData object which describes the origin of the ChatMessage
|
||||
* @property {string[]} whisper An array of User _id values to whom this message is privately whispered
|
||||
* @property {boolean} [blind=false] Is this message sent blindly where the creating User cannot see it?
|
||||
* @property {string[]} [rolls] Serialized content of any Roll instances attached to the ChatMessage
|
||||
* @property {string} [sound] The URL of an audio file which plays when this message is received
|
||||
* @property {boolean} [emote=false] Is this message styled as an emote?
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChatSpeakerData
|
||||
* @property {string} [scene] The _id of the Scene where this message was created
|
||||
* @property {string} [actor] The _id of the Actor who generated this message
|
||||
* @property {string} [token] The _id of the Token who generated this message
|
||||
* @property {string} [alias] An overridden alias name used instead of the Actor or Token name
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CombatData
|
||||
* @property {string} _id The _id which uniquely identifies this Combat document
|
||||
* @property {string} scene The _id of a Scene within which this Combat occurs
|
||||
* @property {Collection<BaseCombatant>} combatants A Collection of Combatant embedded Documents
|
||||
* @property {boolean} [active=false] Is the Combat encounter currently active?
|
||||
* @property {number} [round=0] The current round of the Combat encounter
|
||||
* @property {number|null} [turn=0] The current turn in the Combat round
|
||||
* @property {number} [sort=0] The current sort order of this Combat relative to others in the same Scene
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CombatantData
|
||||
* @property {string} _id The _id which uniquely identifies this Combatant embedded document
|
||||
* @property {string} [actorId] The _id of an Actor associated with this Combatant
|
||||
* @property {string} [tokenId] The _id of a Token associated with this Combatant
|
||||
* @property {string} [name] A customized name which replaces the name of the Token in the tracker
|
||||
* @property {string} [img] A customized image which replaces the Token image in the tracker
|
||||
* @property {number} [initiative] The initiative score for the Combatant which determines its turn order
|
||||
* @property {boolean} [hidden=false] Is this Combatant currently hidden?
|
||||
* @property {boolean} [defeated=false] Has this Combatant been defeated?
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DrawingData
|
||||
* @property {string} _id The _id which uniquely identifies this BaseDrawing embedded document
|
||||
* @property {string} author The _id of the user who created the drawing
|
||||
* @property {data.ShapeData} shape The geometric shape of the drawing
|
||||
* @property {number} x The x-coordinate position of the top-left corner of the drawn shape
|
||||
* @property {number} y The y-coordinate position of the top-left corner of the drawn shape
|
||||
* @property {number} [elevation=0] The elevation of the drawing
|
||||
* @property {number} [sort=0] The z-index of this drawing relative to other siblings
|
||||
* @property {number} [rotation=0] The angle of rotation for the drawing figure
|
||||
* @property {number} [bezierFactor=0] An amount of bezier smoothing applied, between 0 and 1
|
||||
* @property {number} [fillType=0] The fill type of the drawing shape, a value from CONST.DRAWING_FILL_TYPES
|
||||
* @property {string} [fillColor] An optional color string with which to fill the drawing geometry
|
||||
* @property {number} [fillAlpha=0.5] The opacity of the fill applied to the drawing geometry
|
||||
* @property {number} [strokeWidth=8] The width in pixels of the boundary lines of the drawing geometry
|
||||
* @property {number} [strokeColor] The color of the boundary lines of the drawing geometry
|
||||
* @property {number} [strokeAlpha=1] The opacity of the boundary lines of the drawing geometry
|
||||
* @property {string} [texture] The path to a tiling image texture used to fill the drawing geometry
|
||||
* @property {string} [text] Optional text which is displayed overtop of the drawing
|
||||
* @property {string} [fontFamily] The font family used to display text within this drawing, defaults to
|
||||
* CONFIG.defaultFontFamily
|
||||
* @property {number} [fontSize=48] The font size used to display text within this drawing
|
||||
* @property {string} [textColor=#FFFFFF] The color of text displayed within this drawing
|
||||
* @property {number} [textAlpha=1] The opacity of text displayed within this drawing
|
||||
* @property {boolean} [hidden=false] Is the drawing currently hidden?
|
||||
* @property {boolean} [locked=false] Is the drawing currently locked?
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FogExplorationData
|
||||
* @property {string} _id The _id which uniquely identifies this FogExploration document
|
||||
* @property {string} scene The _id of the Scene document to which this fog applies
|
||||
* @property {string} user The _id of the User document to which this fog applies
|
||||
* @property {string} explored The base64 image/jpeg of the explored fog polygon
|
||||
* @property {object} positions The object of scene positions which have been explored at a certain vision radius
|
||||
* @property {number} timestamp The timestamp at which this fog exploration was last updated
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} FolderData
|
||||
* @property {string} _id The _id which uniquely identifies this Folder document
|
||||
* @property {string} name The name of this Folder
|
||||
* @property {string} type The document type which this Folder contains, from CONST.FOLDER_DOCUMENT_TYPES
|
||||
* @property {string} description An HTML description of the contents of this folder
|
||||
* @property {string|null} [folder] The _id of a parent Folder which contains this Folder
|
||||
* @property {string} [sorting=a] The sorting mode used to organize documents within this Folder, in ["a", "m"]
|
||||
* @property {number} [sort] The numeric sort value which orders this Folder relative to its siblings
|
||||
* @property {string|null} [color] A color string used for the background color of this Folder
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ItemData
|
||||
* @property {string} _id The _id which uniquely identifies this Item document
|
||||
* @property {string} name The name of this Item
|
||||
* @property {string} type An Item subtype which configures the system data model applied
|
||||
* @property {string} [img] An image file path which provides the artwork for this Item
|
||||
* @property {object} [system] The system data object which is defined by the system template.json model
|
||||
* @property {Collection<BaseActiveEffect>} effects A collection of ActiveEffect embedded Documents
|
||||
* @property {string|null} folder The _id of a Folder which contains this Item
|
||||
* @property {number} [sort] The numeric sort value which orders this Item relative to its siblings
|
||||
* @property {object} [ownership] An object which configures ownership of this Item
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} JournalEntryData
|
||||
* @property {string} _id The _id which uniquely identifies this JournalEntry document
|
||||
* @property {string} name The name of this JournalEntry
|
||||
* @property {JournalEntryPageData[]} pages The pages contained within this JournalEntry document
|
||||
* @property {string|null} folder The _id of a Folder which contains this JournalEntry
|
||||
* @property {number} [sort] The numeric sort value which orders this JournalEntry relative to its siblings
|
||||
* @property {object} [ownership] An object which configures ownership of this JournalEntry
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} JournalEntryPageImageData
|
||||
* @property {string} [caption] A caption for the image.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JournalEntryPageTextData
|
||||
* @property {string} [content] The content of the JournalEntryPage in a format appropriate for its type.
|
||||
* @property {string} [markdown] The original markdown source, if applicable.
|
||||
* @property {number} format The format of the page's content, in CONST.JOURNAL_ENTRY_PAGE_FORMATS.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JournalEntryPageVideoData
|
||||
* @property {boolean} [loop] Automatically loop the video?
|
||||
* @property {boolean} [autoplay] Should the video play automatically?
|
||||
* @property {number} [volume] The volume level of any audio that the video file contains.
|
||||
* @property {number} [timestamp] The starting point of the video, in seconds.
|
||||
* @property {number} [width] The width of the video, otherwise it will fill the available container width.
|
||||
* @property {number} [height] The height of the video, otherwise it will use the aspect ratio of the source
|
||||
* video, or 16:9 if that aspect ratio is not available.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JournalEntryPageTitleData
|
||||
* @property {boolean} show Whether to render the page's title in the overall journal view.
|
||||
* @property {number} level The heading level to render this page's title at in the overall journal view.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} JournalEntryPageData
|
||||
* @property {string} _id The _id which uniquely identifies this JournalEntryPage embedded document.
|
||||
* @property {string} name The text name of this page.
|
||||
* @property {string} type The type of this page.
|
||||
* @property {JournalEntryPageTitleData} title Data that control's the display of this page's title.
|
||||
* @property {JournalEntryPageImageData} image Data particular to image journal entry pages.
|
||||
* @property {JournalEntryPageTextData} text Data particular to text journal entry pages.
|
||||
* @property {JournalEntryPageVideoData} video Data particular to video journal entry pages.
|
||||
* @property {string} [src] The URI of the image or other external media to be used for this page.
|
||||
* @property {object} system System-specific data.
|
||||
* @property {number} sort The numeric sort value which orders this page relative to its siblings.
|
||||
* @property {object} [ownership] An object which configures the ownership of this page.
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MacroData
|
||||
* @property {string} _id The _id which uniquely identifies this Macro document
|
||||
* @property {string} name The name of this Macro
|
||||
* @property {string} type A Macro subtype from CONST.MACRO_TYPES
|
||||
* @property {string} author The _id of a User document which created this Macro *
|
||||
* @property {string} [img] An image file path which provides the thumbnail artwork for this Macro
|
||||
* @property {string} [scope=global] The scope of this Macro application from CONST.MACRO_SCOPES
|
||||
* @property {string} command The string content of the macro command
|
||||
* @property {string|null} folder The _id of a Folder which contains this Macro
|
||||
* @property {number} [sort] The numeric sort value which orders this Macro relative to its siblings
|
||||
* @property {object} [ownership] An object which configures ownership of this Macro
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MeasuredTemplateData
|
||||
* @property {string} _id The _id which uniquely identifies this BaseMeasuredTemplate embedded document
|
||||
* @property {string} user The _id of the user who created this measured template
|
||||
* @property {string} [t=circle] The value in CONST.MEASURED_TEMPLATE_TYPES which defines the geometry type of this template
|
||||
* @property {number} [x=0] The x-coordinate position of the origin of the template effect
|
||||
* @property {number} [y=0] The y-coordinate position of the origin of the template effect
|
||||
* @property {number} [distance] The distance of the template effect
|
||||
* @property {number} [direction=0] The angle of rotation for the measured template
|
||||
* @property {number} [angle=360] The angle of effect of the measured template, applies to cone types
|
||||
* @property {number} [width] The width of the measured template, applies to ray types
|
||||
* @property {string} [borderColor=#000000] A color string used to tint the border of the template shape
|
||||
* @property {string} [fillColor=#FF0000] A color string used to tint the fill of the template shape
|
||||
* @property {string} [texture] A repeatable tiling texture used to add a texture fill to the template shape
|
||||
* @property {boolean} [hidden=false] Is the template currently hidden?
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} NoteData
|
||||
* @property {string} _id The _id which uniquely identifies this BaseNote embedded document
|
||||
* @property {string|null} [entryId=null] The _id of a JournalEntry document which this Note represents
|
||||
* @property {string|null} [pageId=null] The _id of a specific JournalEntryPage document which this Note represents
|
||||
* @property {number} [x=0] The x-coordinate position of the center of the note icon
|
||||
* @property {number} [y=0] The y-coordinate position of the center of the note icon
|
||||
* @property {TextureData} [texture] An image icon used to represent this note
|
||||
* @property {number} [iconSize=40] The pixel size of the map note icon
|
||||
* @property {string} [text] Optional text which overrides the title of the linked Journal Entry
|
||||
* @property {string} [fontFamily] The font family used to display the text label on this note, defaults to
|
||||
* CONFIG.defaultFontFamily
|
||||
* @property {number} [fontSize=36] The font size used to display the text label on this note
|
||||
* @property {number} [textAnchor=1] A value in CONST.TEXT_ANCHOR_POINTS which defines where the text label anchors
|
||||
* to the note icon.
|
||||
* @property {string} [textColor=#FFFFFF] The string that defines the color with which the note text is rendered
|
||||
* @property {boolean} [global=false] Whether this map pin is globally visible or requires LoS to see.
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PlaylistData
|
||||
* @property {string} _id The _id which uniquely identifies this Playlist document
|
||||
* @property {string} name The name of this playlist
|
||||
* @property {string} description The description of this playlist
|
||||
* @property {Collection<BasePlaylistSound>} sounds A Collection of PlaylistSounds embedded documents which belong to this playlist
|
||||
* @property {number} [mode=0] The playback mode for sounds in this playlist
|
||||
* @property {string} channel A channel in CONST.AUDIO_CHANNELS where all sounds in this playlist are played
|
||||
* @property {boolean} [playing=false] Is this playlist currently playing?
|
||||
* @property {number} [fade] A duration in milliseconds to fade volume transition
|
||||
* @property {string|null} folder The _id of a Folder which contains this playlist
|
||||
* @property {string} sorting The sorting mode used for this playlist.
|
||||
* @property {number} [sort] The numeric sort value which orders this playlist relative to its siblings
|
||||
* @property {number} [seed] A seed used for playlist randomization to guarantee that all clients generate the same random order.
|
||||
* @property {object} [ownership] An object which configures ownership of this Playlist
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PlaylistSoundData
|
||||
* @property {string} _id The _id which uniquely identifies this PlaylistSound document
|
||||
* @property {string} name The name of this sound
|
||||
* @property {string} description The description of this sound
|
||||
* @property {string} path The audio file path that is played by this sound
|
||||
* @property {string} channel A channel in CONST.AUDIO_CHANNELS where this sound is played
|
||||
* @property {boolean} [playing=false] Is this sound currently playing?
|
||||
* @property {number} [pausedTime=null] The time in seconds at which playback was paused
|
||||
* @property {boolean} [repeat=false] Does this sound loop?
|
||||
* @property {number} [volume=0.5] The audio volume of the sound, from 0 to 1
|
||||
* @property {number} [fade] A duration in milliseconds to fade volume transition
|
||||
* @property {number} [sort=0] The sort order of the PlaylistSound relative to others in the same collection
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RollTableData
|
||||
* @property {string} _id The _id which uniquely identifies this RollTable document
|
||||
* @property {string} name The name of this RollTable
|
||||
* @property {string} [img] An image file path which provides the thumbnail artwork for this RollTable
|
||||
* @property {string} [description] The HTML text description for this RollTable document
|
||||
* @property {Collection<BaseTableResult>} [results=[]] A Collection of TableResult embedded documents which belong to this RollTable
|
||||
* @property {string} formula The Roll formula which determines the results chosen from the table
|
||||
* @property {boolean} [replacement=true] Are results from this table drawn with replacement?
|
||||
* @property {boolean} [displayRoll=true] Is the Roll result used to draw from this RollTable displayed in chat?
|
||||
* @property {string|null} folder The _id of a Folder which contains this RollTable
|
||||
* @property {number} [sort] The numeric sort value which orders this RollTable relative to its siblings
|
||||
* @property {object} [ownership] An object which configures ownership of this RollTable
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SceneData
|
||||
* @property {string} _id The _id which uniquely identifies this Scene document
|
||||
* @property {string} name The name of this scene
|
||||
* @property {boolean} [active=false] Is this scene currently active? Only one scene may be active at a given time
|
||||
* @property {boolean} [navigation=false] Is this scene displayed in the top navigation bar?
|
||||
* @property {number} [navOrder] The sorting order of this Scene in the navigation bar relative to siblings
|
||||
* @property {string} [navName] A string which overrides Scene name for display in the navigation bar
|
||||
* @property {TextureData|null} [background] An image or video file that provides the background texture for the scene.
|
||||
* @property {string|null} [foreground] An image or video file path providing foreground media for the scene
|
||||
* @property {number} [foregroundElevation=20] The elevation of the foreground image
|
||||
*
|
||||
* @property {string|null} [thumb] A thumbnail image which depicts the scene at lower resolution
|
||||
* @property {number} [width=4000] The width of the scene canvas, normally the width of the background media
|
||||
* @property {number} [height=3000] The height of the scene canvas, normally the height of the background media
|
||||
* @property {number} [padding=0.25] The proportion of canvas padding applied around the outside of the scene
|
||||
* dimensions to provide additional buffer space
|
||||
* @property {{x: number, y: number, scale: number}|null} [initial=null] The initial view coordinates for the scene
|
||||
* @property {string|null} [backgroundColor=#999999] The color of the canvas displayed behind the scene background
|
||||
* @property {GridData} [grid] Grid configuration for the scene
|
||||
* @property {boolean} [tokenVision=true] Do Tokens require vision in order to see the Scene environment?
|
||||
* @property {number} [darkness=0] The ambient darkness level in this Scene, where 0 represents midday
|
||||
* (maximum illumination) and 1 represents midnight (maximum darkness)
|
||||
*
|
||||
* @property {boolean} [fogExploration=true] Should fog exploration progress be tracked for this Scene?
|
||||
* @property {number} [fogReset] The timestamp at which fog of war was last reset for this Scene.
|
||||
* @property {string|null} [fogOverlay] A special overlay image or video texture which is used for fog of war
|
||||
* @property {string|null} [fogExploredColor] A color tint applied to explored regions of fog of war
|
||||
* @property {string|null} [fogUnexploredColor] A color tint applied to unexplored regions of fog of war
|
||||
*
|
||||
* @property {SceneEnvironmentData} [environment] The environment data applied to the Scene.
|
||||
* @property {boolean} [environment.cycle] If cycling is activated for the Scene, between base and darkness environment data.
|
||||
* @property {EnvironmentData} [environment.base] The base ambience values pertaining to the Scene.
|
||||
* @property {EnvironmentData} [environment.darkness] The darkness ambience values pertaining to the Scene.
|
||||
*
|
||||
* @property {Collection<BaseDrawing>} [drawings=[]] A collection of embedded Drawing objects.
|
||||
* @property {Collection<BaseTile>} [tiles=[]] A collection of embedded Tile objects.
|
||||
* @property {Collection<BaseToken>} [tokens=[]] A collection of embedded Token objects.
|
||||
* @property {Collection<BaseAmbientLight>} [lights=[]] A collection of embedded AmbientLight objects.
|
||||
* @property {Collection<BaseNote>} [notes=[]] A collection of embedded Note objects.
|
||||
* @property {Collection<BaseAmbientSound>} [sounds=[]] A collection of embedded AmbientSound objects.
|
||||
* @property {Collection<BaseMeasuredTemplate>} [templates=[]] A collection of embedded MeasuredTemplate objects.
|
||||
* @property {Collection<BaseWall>} [walls=[]] A collection of embedded Wall objects
|
||||
* @property {BasePlaylist} [playlist] A linked Playlist document which should begin automatically playing when this
|
||||
* Scene becomes active.
|
||||
* @property {BasePlaylistSound} [playlistSound] A linked PlaylistSound document from the selected playlist that will
|
||||
* begin automatically playing when this Scene becomes active
|
||||
* @property {JournalEntry} [journal] A JournalEntry document which provides narrative details about this Scene
|
||||
* @property {string} [weather] A named weather effect which should be rendered in this Scene.
|
||||
|
||||
* @property {string|null} folder The _id of a Folder which contains this Actor
|
||||
* @property {number} [sort] The numeric sort value which orders this Actor relative to its siblings
|
||||
* @property {object} [ownership] An object which configures ownership of this Scene
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GridData
|
||||
* @property {number} [type=1] The type of grid, a number from CONST.GRID_TYPES.
|
||||
* @property {number} [size=100] The grid size which represents the width (or height) of a single grid space.
|
||||
* @property {string} [color=#000000] A string representing the color used to render the grid lines.
|
||||
* @property {number} [alpha=0.2] A number between 0 and 1 for the opacity of the grid lines.
|
||||
* @property {number} [distance] The number of distance units which are represented by a single grid space.
|
||||
* @property {string} [units] A label for the units of measure which are used for grid distance.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {EnvironmentData} EnvironmentData
|
||||
* @property {number} [hue] The normalized hue angle.
|
||||
* @property {number} [intensity] The intensity of the tinting (0 = no tinting).
|
||||
* @property {number} [luminosity] The luminosity.
|
||||
* @property {number} [saturation] The saturation.
|
||||
* @property {number} [shadows] The strength of the shadows.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} _GlobalLightData
|
||||
* @property {number} [enabled] Is the global light enabled?
|
||||
* @property {boolean} [bright] Is the global light in bright mode?
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Pick<LightData, "alpha" | "color" | "coloration" | "contrast" | "luminosity" | "saturation" | "shadows" | "darkness"> & _GlobalLightData} GlobalLightData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SceneEnvironmentData} SceneEnvironmentData
|
||||
* @property {number} [darknessLevel] The environment darkness level.
|
||||
* @property {boolean} [darknessLevelLock] The darkness level lock state.
|
||||
* @property {GlobalLightData} [globalLight] The global light data configuration.
|
||||
* @property {boolean} [cycle] If cycling between Night and Day is activated.
|
||||
* @property {EnvironmentData} [base] The base (darkness level 0) ambience lighting data.
|
||||
* @property {EnvironmentData} [dark] The dark (darkness level 1) ambience lighting data.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} RegionData
|
||||
* @property {string} _id The Region _id which uniquely identifies it within its parent Scene
|
||||
* @property {string} name The name used to describe the Region
|
||||
* @property {string} [color="#ffffff"] The color used to highlight the Region
|
||||
* @property {data.BaseShapeData[]} [shapes=[]] The shapes that make up the Region
|
||||
* @property {Collection<BaseRegionBehavior>} [behaviors=[]] A collection of embedded RegionBehavior objects
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} RegionBehaviorData
|
||||
* @property {string} _id The _id which uniquely identifies this RegionBehavior document
|
||||
* @property {string} [name=""] The name used to describe the RegionBehavior
|
||||
* @property {string} type An RegionBehavior subtype which configures the system data model applied
|
||||
* @property {object} [system] The system data object which is defined by the system template.json model
|
||||
* @property {boolean} [disabled=false] Is the RegionBehavior currently disabled?
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SettingData
|
||||
* @property {string} _id The _id which uniquely identifies this Setting document
|
||||
* @property {string} key The setting key, a composite of {scope}.{name}
|
||||
* @property {*} value The setting value, which is serialized to JSON
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TableResultData
|
||||
* @property {string} _id The _id which uniquely identifies this TableResult embedded document
|
||||
* @property {string} [type="text"] A result subtype from CONST.TABLE_RESULT_TYPES
|
||||
* @property {string} [text] The text which describes the table result
|
||||
* @property {string} [img] An image file url that represents the table result
|
||||
* @property {string} [documentCollection] A named collection from which this result is drawn
|
||||
* @property {string} [documentId] The _id of a Document within the collection this result references
|
||||
* @property {number} [weight=1] The probabilistic weight of this result relative to other results
|
||||
* @property {number[]} [range] A length 2 array of ascending integers which defines the range of dice roll
|
||||
* totals which produce this drawn result
|
||||
* @property {boolean} [drawn=false] Has this result already been drawn (without replacement)
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TileOcclusionData
|
||||
* @property {number} mode The occlusion mode from CONST.TILE_OCCLUSION_MODES
|
||||
* @property {number} alpha The occlusion alpha between 0 and 1
|
||||
* @property {number} [radius] An optional radius of occlusion used for RADIAL mode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TileVideoData
|
||||
* @property {boolean} loop Automatically loop the video?
|
||||
* @property {boolean} autoplay Should the video play automatically?
|
||||
* @property {number} volume The volume level of any audio that the video file contains
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TileData
|
||||
* @property {string} _id The _id which uniquely identifies this Tile embedded document
|
||||
* @property {TextureData} [texture] An image or video texture which this tile displays.
|
||||
* @property {number} [width=0] The pixel width of the tile
|
||||
* @property {number} [height=0] The pixel height of the tile
|
||||
* @property {number} [x=0] The x-coordinate position of the top-left corner of the tile
|
||||
* @property {number} [y=0] The y-coordinate position of the top-left corner of the tile
|
||||
* @property {number} [elevation=0] The elevation of the tile
|
||||
* @property {number} [sort=0] The z-index ordering of this tile relative to its siblings
|
||||
* @property {number} [rotation=0] The angle of rotation for the tile between 0 and 360
|
||||
* @property {number} [alpha=1] The tile opacity
|
||||
* @property {boolean} [hidden=false] Is the tile currently hidden?
|
||||
* @property {boolean} [locked=false] Is the tile currently locked?
|
||||
* @property {TileOcclusionData} [occlusion] The tile's occlusion settings
|
||||
* @property {TileVideoData} [video] The tile's video settings
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TokenData
|
||||
* @property {string} _id The Token _id which uniquely identifies it within its parent Scene
|
||||
* @property {string} name The name used to describe the Token
|
||||
* @property {number} [displayName=0] The display mode of the Token nameplate, from CONST.TOKEN_DISPLAY_MODES
|
||||
* @property {string|null} actorId The _id of an Actor document which this Token represents
|
||||
* @property {boolean} [actorLink=false] Does this Token uniquely represent a singular Actor, or is it one of many?
|
||||
* @property {BaseActorDelta} [delta] The ActorDelta embedded document which stores the differences between this
|
||||
* token and the base actor it represents.
|
||||
* @property {TextureData} texture The token's texture on the canvas.
|
||||
* @property {number} [width=1] The width of the Token in grid units
|
||||
* @property {number} [height=1] The height of the Token in grid units
|
||||
* @property {number} [x=0] The x-coordinate of the top-left corner of the Token
|
||||
* @property {number} [y=0] The y-coordinate of the top-left corner of the Token
|
||||
* @property {number} [elevation=0] The vertical elevation of the Token, in distance units
|
||||
* @property {boolean} [locked=false] Is the Token currently locked? A locked token cannot be moved or rotated via
|
||||
* standard keyboard or mouse interaction.
|
||||
* @property {boolean} [lockRotation=false] Prevent the Token image from visually rotating?
|
||||
* @property {number} [rotation=0] The rotation of the Token in degrees, from 0 to 360. A value of 0 represents a southward-facing Token.
|
||||
* @property {number} [alpha=1] The opacity of the token image
|
||||
* @property {boolean} [hidden=false] Is the Token currently hidden from player view?
|
||||
* @property {number} [disposition=-1] A displayed Token disposition from CONST.TOKEN_DISPOSITIONS
|
||||
* @property {number} [displayBars=0] The display mode of Token resource bars, from CONST.TOKEN_DISPLAY_MODES
|
||||
* @property {TokenBarData} [bar1] The configuration of the Token's primary resource bar
|
||||
* @property {TokenBarData} [bar2] The configuration of the Token's secondary resource bar
|
||||
* @property {data.LightData} [light] Configuration of the light source that this Token emits
|
||||
* @property {TokenSightData} sight Configuration of sight and vision properties for the Token
|
||||
* @property {TokenDetectionMode[]} detectionModes An array of detection modes which are available to this Token
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TokenSightData
|
||||
* @property {boolean} enabled Should vision computation and rendering be active for this Token?
|
||||
* @property {number|null} range How far in distance units the Token can see without the aid of a light source.
|
||||
* If null, the sight range is unlimited.
|
||||
* @property {number} [angle=360] An angle at which the Token can see relative to their direction of facing
|
||||
* @property {string} [visionMode=basic] The vision mode which is used to render the appearance of the visible area
|
||||
* @property {string} [color] A special color which applies a hue to the visible area
|
||||
* @property {number} [attenuation] A degree of attenuation which gradually fades the edges of the visible area
|
||||
* @property {number} [brightness=0] An advanced customization for the perceived brightness of the visible area
|
||||
* @property {number} [saturation=0] An advanced customization of color saturation within the visible area
|
||||
* @property {number} [contrast=0] An advanced customization for contrast within the visible area
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TokenDetectionMode
|
||||
* @property {string} id The id of the detection mode, a key from CONFIG.Canvas.detectionModes
|
||||
* @property {boolean} enabled Whether or not this detection mode is presently enabled
|
||||
* @property {number|null} range The maximum range in distance units at which this mode can detect targets.
|
||||
* If null, the detection range is unlimited.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TokenBarData
|
||||
* @property {string} [attribute] The attribute path within the Token's Actor data which should be displayed
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UserData
|
||||
* @property {string} _id The _id which uniquely identifies this User document.
|
||||
* @property {string} name The user's name.
|
||||
* @property {string} [password] The user's password. Available only on the Server side for security.
|
||||
* @property {string} [passwordSalt] The user's password salt. Available only on the Server side for security.
|
||||
* @property {string|null} [avatar] The user's avatar image.
|
||||
* @property {BaseActor} [character] A linked Actor document that is this user's impersonated character.
|
||||
* @property {string} color A color to represent this user.
|
||||
* @property {object} hotbar A mapping of hotbar slot number to Macro id for the user.
|
||||
* @property {object} permissions The user's individual permission configuration, see CONST.USER_PERMISSIONS.
|
||||
* @property {number} role The user's role, see CONST.USER_ROLES.
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
* @property {DocumentStats} _stats An object of creation and access information
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} WallData
|
||||
* @property {string} _id The _id which uniquely identifies the embedded Wall document
|
||||
* @property {number[]} c The wall coordinates, a length-4 array of finite numbers [x0,y0,x1,y1]
|
||||
* @property {number} [light=0] The illumination restriction type of this wall
|
||||
* @property {number} [move=0] The movement restriction type of this wall
|
||||
* @property {number} [sight=0] The visual restriction type of this wall
|
||||
* @property {number} [sound=0] The auditory restriction type of this wall
|
||||
* @property {number} [dir=0] The direction of effect imposed by this wall
|
||||
* @property {number} [door=0] The type of door which this wall contains, if any
|
||||
* @property {number} [ds=0] The state of the door this wall contains, if any
|
||||
* @property {WallThresholdData} threshold Configuration of threshold data for this wall
|
||||
* @property {object} flags An object of optional key/value flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} WallThresholdData
|
||||
* @property {number} [light=0] Minimum distance from a light source for which this wall blocks light
|
||||
* @property {number} [sight=0] Minimum distance from a vision source for which this wall blocks vision
|
||||
* @property {number} [sound=0] Minimum distance from a sound source for which this wall blocks sound
|
||||
* @property {boolean} [attenuation=true] Whether to attenuate the source radius when passing through the wall
|
||||
*/
|
||||
177
resources/app/common/documents/active-effect.mjs
Normal file
177
resources/app/common/documents/active-effect.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").ActiveEffectData} ActiveEffectData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The ActiveEffect Document.
|
||||
* Defines the DataSchema and common behaviors for an ActiveEffect which are shared between both client and server.
|
||||
* @mixes {@link ActiveEffectData}
|
||||
*/
|
||||
export default class BaseActiveEffect extends Document {
|
||||
/**
|
||||
* Construct an ActiveEffect document using provided data and context.
|
||||
* @param {Partial<ActiveEffectData>} data Initial data from which to construct the ActiveEffect
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "ActiveEffect",
|
||||
collection: "effects",
|
||||
hasTypeData: true,
|
||||
label: "DOCUMENT.ActiveEffect",
|
||||
labelPlural: "DOCUMENT.ActiveEffects",
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "EFFECT.Name", textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], label: "EFFECT.Image"}),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.BASE_DOCUMENT_TYPE}),
|
||||
system: new fields.TypeDataField(this),
|
||||
changes: new fields.ArrayField(new fields.SchemaField({
|
||||
key: new fields.StringField({required: true, label: "EFFECT.ChangeKey"}),
|
||||
value: new fields.StringField({required: true, label: "EFFECT.ChangeValue"}),
|
||||
mode: new fields.NumberField({integer: true, initial: CONST.ACTIVE_EFFECT_MODES.ADD,
|
||||
label: "EFFECT.ChangeMode"}),
|
||||
priority: new fields.NumberField()
|
||||
})),
|
||||
disabled: new fields.BooleanField(),
|
||||
duration: new fields.SchemaField({
|
||||
startTime: new fields.NumberField({initial: null, label: "EFFECT.StartTime"}),
|
||||
seconds: new fields.NumberField({integer: true, min: 0, label: "EFFECT.DurationSecs"}),
|
||||
combat: new fields.ForeignDocumentField(documents.BaseCombat, {label: "EFFECT.Combat"}),
|
||||
rounds: new fields.NumberField({integer: true, min: 0}),
|
||||
turns: new fields.NumberField({integer: true, min: 0, label: "EFFECT.DurationTurns"}),
|
||||
startRound: new fields.NumberField({integer: true, min: 0}),
|
||||
startTurn: new fields.NumberField({integer: true, min: 0, label: "EFFECT.StartTurns"})
|
||||
}),
|
||||
description: new fields.HTMLField({label: "EFFECT.Description", textSearch: true}),
|
||||
origin: new fields.StringField({nullable: true, blank: false, initial: null, label: "EFFECT.Origin"}),
|
||||
tint: new fields.ColorField({nullable: false, initial: "#ffffff", label: "EFFECT.Tint"}),
|
||||
transfer: new fields.BooleanField({initial: true, label: "EFFECT.Transfer"}),
|
||||
statuses: new fields.SetField(new fields.StringField({required: true, blank: false})),
|
||||
sort: new fields.IntegerSortField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
canUserModify(user, action, data={}) {
|
||||
if ( this.isEmbedded ) return this.parent.canUserModify(user, "update");
|
||||
return super.canUserModify(user, action, data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Database Event Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
if ( this.parent instanceof documents.BaseActor ) {
|
||||
this.updateSource({transfer: false});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(data) {
|
||||
/**
|
||||
* label -> name
|
||||
* @deprecated since v11
|
||||
*/
|
||||
this._addDataFieldMigration(data, "label", "name", d => d.label || "Unnamed Effect");
|
||||
/**
|
||||
* icon -> img
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "icon", "img");
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
this._addDataFieldShim(data, "label", "name", {since: 11, until: 13});
|
||||
this._addDataFieldShim(data, "icon", "img", {since: 12, until: 14});
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
get label() {
|
||||
this.constructor._logDataFieldMigration("label", "name", {since: 11, until: 13, once: true});
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v11
|
||||
* @ignore
|
||||
*/
|
||||
set label(value) {
|
||||
this.constructor._logDataFieldMigration("label", "name", {since: 11, until: 13, once: true});
|
||||
this.name = value;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get icon() {
|
||||
this.constructor._logDataFieldMigration("icon", "img", {since: 12, until: 14, once: true});
|
||||
return this.img;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
set icon(value) {
|
||||
this.constructor._logDataFieldMigration("icon", "img", {since: 12, until: 14, once: true});
|
||||
this.img = value;
|
||||
}
|
||||
}
|
||||
176
resources/app/common/documents/actor-delta.mjs
Normal file
176
resources/app/common/documents/actor-delta.mjs
Normal file
@@ -0,0 +1,176 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {deepClone, mergeObject} from "../utils/helpers.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import BaseActor from "./actor.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").ActorDeltaData} ActorDeltaData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The ActorDelta Document.
|
||||
* Defines the DataSchema and common behaviors for an ActorDelta which are shared between both client and server.
|
||||
* ActorDeltas store a delta that can be applied to a particular Actor in order to produce a new Actor.
|
||||
* @mixes ActorDeltaData
|
||||
*/
|
||||
export default class BaseActorDelta extends Document {
|
||||
/**
|
||||
* Construct an ActorDelta document using provided data and context.
|
||||
* @param {Partial<ActorDeltaData>} data Initial data used to construct the ActorDelta.
|
||||
* @param {DocumentConstructionContext} context Construction context options.
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "ActorDelta",
|
||||
collection: "delta",
|
||||
label: "DOCUMENT.ActorDelta",
|
||||
labelPlural: "DOCUMENT.ActorDeltas",
|
||||
isEmbedded: true,
|
||||
embedded: {
|
||||
Item: "items",
|
||||
ActiveEffect: "effects"
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: false, nullable: true, initial: null}),
|
||||
type: new fields.StringField({required: false, nullable: true, initial: null}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], nullable: true, initial: null, required: false}),
|
||||
system: new fields.ObjectField(),
|
||||
items: new fields.EmbeddedCollectionDeltaField(documents.BaseItem),
|
||||
effects: new fields.EmbeddedCollectionDeltaField(documents.BaseActiveEffect),
|
||||
ownership: new fields.DocumentOwnershipField({required: false, nullable: true, initial: null}),
|
||||
flags: new fields.ObjectField()
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
canUserModify(user, action, data={}) {
|
||||
return this.parent.canUserModify(user, action, data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
testUserPermission(user, permission, { exact=false }={}) {
|
||||
return this.parent.testUserPermission(user, permission, { exact });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve the base actor's collection, if it exists.
|
||||
* @param {string} collectionName The collection name.
|
||||
* @returns {Collection}
|
||||
*/
|
||||
getBaseCollection(collectionName) {
|
||||
const baseActor = this.parent?.baseActor;
|
||||
return baseActor?.getEmbeddedCollection(collectionName);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Apply an ActorDelta to an Actor and return the resultant synthetic Actor.
|
||||
* @param {ActorDelta} delta The ActorDelta.
|
||||
* @param {Actor} baseActor The base Actor.
|
||||
* @param {object} [context] Context to supply to synthetic Actor instantiation.
|
||||
* @returns {Actor|null}
|
||||
*/
|
||||
static applyDelta(delta, baseActor, context={}) {
|
||||
if ( !baseActor ) return null;
|
||||
if ( delta.parent?.isLinked ) return baseActor;
|
||||
|
||||
// Get base actor data.
|
||||
const cls = game?.actors?.documentClass ?? db.Actor;
|
||||
const actorData = baseActor.toObject();
|
||||
const deltaData = delta.toObject();
|
||||
delete deltaData._id;
|
||||
|
||||
// Merge embedded collections.
|
||||
BaseActorDelta.#mergeEmbeddedCollections(cls, actorData, deltaData);
|
||||
|
||||
// Merge the rest of the delta.
|
||||
mergeObject(actorData, deltaData);
|
||||
return new cls(actorData, {parent: delta.parent, ...context});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Merge delta Document embedded collections with the base Document.
|
||||
* @param {typeof Document} documentClass The parent Document class.
|
||||
* @param {object} baseData The base Document data.
|
||||
* @param {object} deltaData The delta Document data.
|
||||
*/
|
||||
static #mergeEmbeddedCollections(documentClass, baseData, deltaData) {
|
||||
for ( const collectionName of Object.keys(documentClass.hierarchy) ) {
|
||||
const baseCollection = baseData[collectionName];
|
||||
const deltaCollection = deltaData[collectionName];
|
||||
baseData[collectionName] = BaseActorDelta.#mergeEmbeddedCollection(baseCollection, deltaCollection);
|
||||
delete deltaData[collectionName];
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Apply an embedded collection delta.
|
||||
* @param {object[]} base The base embedded collection.
|
||||
* @param {object[]} delta The delta embedded collection.
|
||||
* @returns {object[]}
|
||||
*/
|
||||
static #mergeEmbeddedCollection(base=[], delta=[]) {
|
||||
const deltaIds = new Set();
|
||||
const records = [];
|
||||
for ( const record of delta ) {
|
||||
if ( !record._tombstone ) records.push(record);
|
||||
deltaIds.add(record._id);
|
||||
}
|
||||
for ( const record of base ) {
|
||||
if ( !deltaIds.has(record._id) ) records.push(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static migrateData(source) {
|
||||
return BaseActor.migrateData(source);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Serialization */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
toObject(source=true) {
|
||||
const data = {};
|
||||
const value = source ? this._source : this;
|
||||
for ( const [name, field] of this.schema.entries() ) {
|
||||
const v = value[name];
|
||||
if ( !field.required && ((v === undefined) || (v === null)) ) continue; // Drop optional fields
|
||||
data[name] = source ? deepClone(value[name]) : field.toObject(value[name]);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
188
resources/app/common/documents/actor.mjs
Normal file
188
resources/app/common/documents/actor.mjs
Normal file
@@ -0,0 +1,188 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {getProperty, mergeObject, setProperty} from "../utils/helpers.mjs";
|
||||
import {PrototypeToken} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").ActorData} ActorData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Actor Document.
|
||||
* Defines the DataSchema and common behaviors for an Actor which are shared between both client and server.
|
||||
* @mixes ActorData
|
||||
*/
|
||||
export default class BaseActor extends Document {
|
||||
/**
|
||||
* Construct an Actor document using provided data and context.
|
||||
* @param {Partial<ActorData>} data Initial data from which to construct the Actor
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Actor",
|
||||
collection: "actors",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "img", "type", "sort", "folder"],
|
||||
embedded: {ActiveEffect: "effects", Item: "items"},
|
||||
hasTypeData: true,
|
||||
label: "DOCUMENT.Actor",
|
||||
labelPlural: "DOCUMENT.Actors",
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], initial: data => {
|
||||
return this.implementation.getDefaultArtwork(data).img;
|
||||
}}),
|
||||
type: new fields.DocumentTypeField(this),
|
||||
system: new fields.TypeDataField(this),
|
||||
prototypeToken: new fields.EmbeddedDataField(PrototypeToken),
|
||||
items: new fields.EmbeddedCollectionField(documents.BaseItem),
|
||||
effects: new fields.EmbeddedCollectionField(documents.BaseActiveEffect),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The default icon used for newly created Actor documents.
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = CONST.DEFAULT_TOKEN;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine default artwork based on the provided actor data.
|
||||
* @param {ActorData} actorData The source actor data.
|
||||
* @returns {{img: string, texture: {src: string}}} Candidate actor image and prototype token artwork.
|
||||
*/
|
||||
static getDefaultArtwork(actorData) {
|
||||
return {
|
||||
img: this.DEFAULT_ICON,
|
||||
texture: {
|
||||
src: this.DEFAULT_ICON
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_initializeSource(source, options) {
|
||||
source = super._initializeSource(source, options);
|
||||
source.prototypeToken.name = source.prototypeToken.name || source.name;
|
||||
source.prototypeToken.texture.src = source.prototypeToken.texture.src || source.img;
|
||||
return source;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static canUserCreate(user) {
|
||||
return user.hasPermission("ACTOR_CREATE");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to create this actor?
|
||||
* @param {User} user The user attempting the creation operation.
|
||||
* @param {Actor} doc The Actor being created.
|
||||
*/
|
||||
static #canCreate(user, doc) {
|
||||
if ( !user.hasPermission("ACTOR_CREATE") ) return false; // User cannot create actors at all
|
||||
if ( doc._source.prototypeToken.randomImg && !user.hasPermission("FILES_BROWSE") ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing actor?
|
||||
* @param {User} user The user attempting the update operation.
|
||||
* @param {Actor} doc The Actor being updated.
|
||||
* @param {object} data The update delta being applied.
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( !doc.testUserPermission(user, "OWNER") ) return false; // Ownership is required.
|
||||
|
||||
// Users can only enable token wildcard images if they have FILES_BROWSE permission.
|
||||
const tokenChange = data?.prototypeToken || {};
|
||||
const enablingRandomImage = tokenChange.randomImg === true;
|
||||
if ( enablingRandomImage ) return user.hasPermission("FILES_BROWSE");
|
||||
|
||||
// Users can only change a token wildcard path if they have FILES_BROWSE permission.
|
||||
const randomImageEnabled = doc._source.prototypeToken.randomImg && (tokenChange.randomImg !== false);
|
||||
const changingRandomImage = ("img" in tokenChange) && randomImageEnabled;
|
||||
if ( changingRandomImage ) return user.hasPermission("FILES_BROWSE");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
if ( !this.prototypeToken.name ) this.prototypeToken.updateSource({name: this.name});
|
||||
if ( !this.prototypeToken.texture.src || (this.prototypeToken.texture.src === CONST.DEFAULT_TOKEN)) {
|
||||
const { texture } = this.constructor.getDefaultArtwork(this.toObject());
|
||||
this.prototypeToken.updateSource("img" in data ? { texture: { src: this.img } } : { texture });
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preUpdate(changed, options, user) {
|
||||
const allowed = await super._preUpdate(changed, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
if ( changed.img && !getProperty(changed, "prototypeToken.texture.src") ) {
|
||||
const { texture } = this.constructor.getDefaultArtwork(foundry.utils.mergeObject(this.toObject(), changed));
|
||||
if ( !this.prototypeToken.texture.src || (this.prototypeToken.texture.src === texture?.src) ) {
|
||||
setProperty(changed, "prototypeToken.texture.src", changed.img);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
88
resources/app/common/documents/adventure.mjs
Normal file
88
resources/app/common/documents/adventure.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs"
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").AdventureData} AdventureData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Adventure Document.
|
||||
* Defines the DataSchema and common behaviors for an Adventure which are shared between both client and server.
|
||||
* @mixes AdventureData
|
||||
*/
|
||||
export default class BaseAdventure extends Document {
|
||||
/**
|
||||
* Construct an Adventure document using provided data and context.
|
||||
* @param {Partial<AdventureData>} data Initial data used to construct the Adventure.
|
||||
* @param {DocumentConstructionContext} context Construction context options.
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Adventure",
|
||||
collection: "adventures",
|
||||
compendiumIndexFields: ["_id", "name", "description", "img", "sort", "folder"],
|
||||
label: "DOCUMENT.Adventure",
|
||||
labelPlural: "DOCUMENT.Adventures",
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "ADVENTURE.Name", hint: "ADVENTURE.NameHint", textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], label: "ADVENTURE.Image", hint: "ADVENTURE.ImageHint"}),
|
||||
caption: new fields.HTMLField({label: "ADVENTURE.Caption", hint: "ADVENTURE.CaptionHint"}),
|
||||
description: new fields.HTMLField({label: "ADVENTURE.Description", hint: "ADVENTURE.DescriptionHint", textSearch: true}),
|
||||
actors: new fields.SetField(new fields.EmbeddedDataField(documents.BaseActor)),
|
||||
combats: new fields.SetField(new fields.EmbeddedDataField(documents.BaseCombat)),
|
||||
items: new fields.SetField(new fields.EmbeddedDataField(documents.BaseItem)),
|
||||
journal: new fields.SetField(new fields.EmbeddedDataField(documents.BaseJournalEntry)),
|
||||
scenes: new fields.SetField(new fields.EmbeddedDataField(documents.BaseScene)),
|
||||
tables: new fields.SetField(new fields.EmbeddedDataField(documents.BaseRollTable)),
|
||||
macros: new fields.SetField(new fields.EmbeddedDataField(documents.BaseMacro)),
|
||||
cards: new fields.SetField(new fields.EmbeddedDataField(documents.BaseCards)),
|
||||
playlists: new fields.SetField(new fields.EmbeddedDataField(documents.BasePlaylist)),
|
||||
folders: new fields.SetField(new fields.EmbeddedDataField(documents.BaseFolder)),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of the fields which provide imported content from the Adventure.
|
||||
* @type {Record<string, typeof Document>}
|
||||
*/
|
||||
static get contentFields() {
|
||||
const content = {};
|
||||
for ( const field of this.schema ) {
|
||||
if ( field instanceof fields.SetField ) content[field.name] = field.element.model.implementation;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a thumbnail image path used to represent the Adventure document.
|
||||
* @type {string}
|
||||
*/
|
||||
get thumbnail() {
|
||||
return this.img;
|
||||
}
|
||||
}
|
||||
57
resources/app/common/documents/ambient-light.mjs
Normal file
57
resources/app/common/documents/ambient-light.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {LightData} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").AmbientLightData} AmbientLightData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The AmbientLight Document.
|
||||
* Defines the DataSchema and common behaviors for an AmbientLight which are shared between both client and server.
|
||||
* @mixes AmbientLightData
|
||||
*/
|
||||
export default class BaseAmbientLight extends Document {
|
||||
/**
|
||||
* Construct an AmbientLight document using provided data and context.
|
||||
* @param {Partial<AmbientLightData>} data Initial data from which to construct the AmbientLight
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "AmbientLight",
|
||||
collection: "lights",
|
||||
label: "DOCUMENT.AmbientLight",
|
||||
labelPlural: "DOCUMENT.AmbientLights",
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
x: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
y: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
rotation: new fields.AngleField(),
|
||||
walls: new fields.BooleanField({initial: true}),
|
||||
vision: new fields.BooleanField(),
|
||||
config: new fields.EmbeddedDataField(LightData),
|
||||
hidden: new fields.BooleanField(),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["AMBIENT_LIGHT"];
|
||||
}
|
||||
73
resources/app/common/documents/ambient-sound.mjs
Normal file
73
resources/app/common/documents/ambient-sound.mjs
Normal file
@@ -0,0 +1,73 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").AmbientSoundData} AmbientSoundData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The AmbientSound Document.
|
||||
* Defines the DataSchema and common behaviors for an AmbientSound which are shared between both client and server.
|
||||
* @mixes AmbientSoundData
|
||||
*/
|
||||
export default class BaseAmbientSound extends Document {
|
||||
/**
|
||||
* Construct an AmbientSound document using provided data and context.
|
||||
* @param {Partial<AmbientSoundData>} data Initial data from which to construct the AmbientSound
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "AmbientSound",
|
||||
collection: "sounds",
|
||||
label: "DOCUMENT.AmbientSound",
|
||||
labelPlural: "DOCUMENT.AmbientSounds",
|
||||
isEmbedded: true,
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
x: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
y: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
radius: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
|
||||
path: new fields.FilePathField({categories: ["AUDIO"]}),
|
||||
repeat: new fields.BooleanField(),
|
||||
volume: new fields.AlphaField({initial: 0.5, step: 0.01}),
|
||||
walls: new fields.BooleanField({initial: true}),
|
||||
easing: new fields.BooleanField({initial: true}),
|
||||
hidden: new fields.BooleanField(),
|
||||
darkness: new fields.SchemaField({
|
||||
min: new fields.AlphaField({initial: 0}),
|
||||
max: new fields.AlphaField({initial: 1})
|
||||
}),
|
||||
effects: new fields.SchemaField({
|
||||
base: new fields.SchemaField({
|
||||
type: new fields.StringField(),
|
||||
intensity: new fields.NumberField({required: true, integer: true, initial: 5, min: 1, max: 10, step: 1})
|
||||
}),
|
||||
muffled: new fields.SchemaField({
|
||||
type: new fields.StringField(),
|
||||
intensity: new fields.NumberField({required: true, integer: true, initial: 5, min: 1, max: 10, step: 1})
|
||||
})
|
||||
}),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["AMBIENT_SOUND"];
|
||||
}
|
||||
116
resources/app/common/documents/card.mjs
Normal file
116
resources/app/common/documents/card.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").CardData} CardData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Card Document.
|
||||
* Defines the DataSchema and common behaviors for a Card which are shared between both client and server.
|
||||
* @mixes CardData
|
||||
*/
|
||||
export default class BaseCard extends Document {
|
||||
/**
|
||||
* Construct a Card document using provided data and context.
|
||||
* @param {Partial<CardData>} data Initial data from which to construct the Card
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Card",
|
||||
collection: "cards",
|
||||
hasTypeData: true,
|
||||
indexed: true,
|
||||
label: "DOCUMENT.Card",
|
||||
labelPlural: "DOCUMENT.Cards",
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
compendiumIndexFields: ["name", "type", "suit", "sort"],
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "CARD.Name", textSearch: true}),
|
||||
description: new fields.HTMLField({label: "CARD.Description"}),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.BASE_DOCUMENT_TYPE}),
|
||||
system: new fields.TypeDataField(this),
|
||||
suit: new fields.StringField({label: "CARD.Suit"}),
|
||||
value: new fields.NumberField({label: "CARD.Value"}),
|
||||
back: new fields.SchemaField({
|
||||
name: new fields.StringField({label: "CARD.BackName"}),
|
||||
text: new fields.HTMLField({label: "CARD.BackText"}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE", "VIDEO"], label: "CARD.BackImage"}),
|
||||
}),
|
||||
faces: new fields.ArrayField(new fields.SchemaField({
|
||||
name: new fields.StringField({label: "CARD.FaceName"}),
|
||||
text: new fields.HTMLField({label: "CARD.FaceText"}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE", "VIDEO"], initial: () => this.DEFAULT_ICON,
|
||||
label: "CARD.FaceImage"}),
|
||||
})),
|
||||
face: new fields.NumberField({required: true, initial: null, integer: true, min: 0, label: "CARD.Face"}),
|
||||
drawn: new fields.BooleanField({label: "CARD.Drawn"}),
|
||||
origin: new fields.ForeignDocumentField(documents.BaseCards),
|
||||
width: new fields.NumberField({integer: true, positive: true, label: "Width"}),
|
||||
height: new fields.NumberField({integer: true, positive: true, label: "Height"}),
|
||||
rotation: new fields.AngleField({label: "Rotation"}),
|
||||
sort: new fields.IntegerSortField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default icon used for a Card face that does not have a custom image set
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = "icons/svg/card-joker.svg";
|
||||
|
||||
/**
|
||||
* Is a User able to create a new Card within this parent?
|
||||
* @private
|
||||
*/
|
||||
static #canCreate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can always create
|
||||
if ( doc.parent.type !== "deck" ) return true; // Users can pass cards to card hands or piles
|
||||
return doc.parent.canUserModify(user, "create", data); // Otherwise require parent document permission
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing Card?
|
||||
* @private
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can always update
|
||||
const wasDrawn = new Set(["drawn", "_id"]); // Users can draw cards from a deck
|
||||
if ( new Set(Object.keys(data)).equals(wasDrawn) ) return true;
|
||||
return doc.parent.canUserModify(user, "update", data); // Otherwise require parent document permission
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
}
|
||||
87
resources/app/common/documents/cards.mjs
Normal file
87
resources/app/common/documents/cards.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").CardsData} CardsData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Cards Document.
|
||||
* Defines the DataSchema and common behaviors for a Cards Document which are shared between both client and server.
|
||||
* @mixes CardsData
|
||||
*/
|
||||
export default class BaseCards extends Document {
|
||||
/**
|
||||
* Construct a Cards document using provided data and context.
|
||||
* @param {Partial<CardsData>} data Initial data from which to construct the Cards
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Cards",
|
||||
collection: "cards",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "description", "img", "type", "sort", "folder"],
|
||||
embedded: {Card: "cards"},
|
||||
hasTypeData: true,
|
||||
label: "DOCUMENT.Cards",
|
||||
labelPlural: "DOCUMENT.CardsPlural",
|
||||
permissions: {create: "CARDS_CREATE"},
|
||||
coreTypes: ["deck", "hand", "pile"],
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "CARDS.Name", textSearch: true}),
|
||||
type: new fields.DocumentTypeField(this),
|
||||
description: new fields.HTMLField({label: "CARDS.Description", textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE", "VIDEO"], initial: () => this.DEFAULT_ICON,
|
||||
label: "CARDS.Image"}),
|
||||
system: new fields.TypeDataField(this),
|
||||
cards: new fields.EmbeddedCollectionField(documents.BaseCard),
|
||||
width: new fields.NumberField({integer: true, positive: true, label: "Width"}),
|
||||
height: new fields.NumberField({integer: true, positive: true, label: "Height"}),
|
||||
rotation: new fields.AngleField({label: "Rotation"}),
|
||||
displayCount: new fields.BooleanField(),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default icon used for a cards stack that does not have a custom image set
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = "icons/svg/card-hand.svg";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
158
resources/app/common/documents/chat-message.mjs
Normal file
158
resources/app/common/documents/chat-message.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").ChatMessageData} ChatMessageData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The ChatMessage Document.
|
||||
* Defines the DataSchema and common behaviors for a ChatMessage which are shared between both client and server.
|
||||
* @mixes ChatMessageData
|
||||
*/
|
||||
export default class BaseChatMessage extends Document {
|
||||
/**
|
||||
* Construct a Cards document using provided data and context.
|
||||
* @param {Partial<ChatMessageData>} data Initial data from which to construct the ChatMessage
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "ChatMessage",
|
||||
collection: "messages",
|
||||
label: "DOCUMENT.ChatMessage",
|
||||
labelPlural: "DOCUMENT.ChatMessages",
|
||||
hasTypeData: true,
|
||||
isPrimary: true,
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.BASE_DOCUMENT_TYPE}),
|
||||
system: new fields.TypeDataField(this),
|
||||
style: new fields.NumberField({required: true, choices: Object.values(CONST.CHAT_MESSAGE_STYLES),
|
||||
initial: CONST.CHAT_MESSAGE_STYLES.OTHER, validationError: "must be a value in CONST.CHAT_MESSAGE_STYLES"}),
|
||||
author: new fields.ForeignDocumentField(documents.BaseUser, {nullable: false, initial: () => game?.user?.id}),
|
||||
timestamp: new fields.NumberField({required: true, nullable: false, initial: Date.now}),
|
||||
flavor: new fields.HTMLField(),
|
||||
content: new fields.HTMLField({textSearch: true}),
|
||||
speaker: new fields.SchemaField({
|
||||
scene: new fields.ForeignDocumentField(documents.BaseScene, {idOnly: true}),
|
||||
actor: new fields.ForeignDocumentField(documents.BaseActor, {idOnly: true}),
|
||||
token: new fields.ForeignDocumentField(documents.BaseToken, {idOnly: true}),
|
||||
alias: new fields.StringField()
|
||||
}),
|
||||
whisper: new fields.ArrayField(new fields.ForeignDocumentField(documents.BaseUser, {idOnly: true})),
|
||||
blind: new fields.BooleanField(),
|
||||
rolls: new fields.ArrayField(new fields.JSONField({validate: BaseChatMessage.#validateRoll})),
|
||||
sound: new fields.FilePathField({categories: ["AUDIO"]}),
|
||||
emote: new fields.BooleanField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to create a new chat message?
|
||||
*/
|
||||
static #canCreate(user, doc) {
|
||||
if ( user.isGM ) return true;
|
||||
if ( user.id !== doc._source.author ) return false; // You cannot impersonate a different user
|
||||
return user.hasRole("PLAYER"); // Any player can create messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing chat message?
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can do anything
|
||||
if ( user.id !== doc._source.author ) return false; // Otherwise, message authors
|
||||
if ( ("author" in data) && (data.author !== user.id) ) return false; // Message author is immutable
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate that Rolls belonging to the ChatMessage document are valid
|
||||
* @param {string} rollJSON The serialized Roll data
|
||||
*/
|
||||
static #validateRoll(rollJSON) {
|
||||
const roll = JSON.parse(rollJSON);
|
||||
if ( !roll.evaluated ) throw new Error(`Roll objects added to ChatMessage documents must be evaluated`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( !exact && (user.id === this._source.author) ) return true; // The user who created the chat message
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data) {
|
||||
/**
|
||||
* V12 migration from user to author
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "user", "author");
|
||||
BaseChatMessage.#migrateTypeToStyle(data);
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the type field to the style field in order to allow the type field to be used for system sub-types.
|
||||
* @param {Partial<ChatMessageData>} data
|
||||
*/
|
||||
static #migrateTypeToStyle(data) {
|
||||
if ( (typeof data.type !== "number") || ("style" in data) ) return;
|
||||
// WHISPER, ROLL, and any other invalid style are redirected to OTHER
|
||||
data.style = Object.values(CONST.CHAT_MESSAGE_STYLES).includes(data.type) ? data.type : 0;
|
||||
data.type = CONST.BASE_DOCUMENT_TYPE;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
this._addDataFieldShim(data, "user", "author", {since: 12, until: 14})
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get user() {
|
||||
this.constructor._logDataFieldMigration("user", "author", {since: 12, until: 14});
|
||||
return this.author;
|
||||
}
|
||||
}
|
||||
141
resources/app/common/documents/combat.mjs
Normal file
141
resources/app/common/documents/combat.mjs
Normal file
@@ -0,0 +1,141 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import {isValidId} from "../data/validators.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").CombatData} CombatData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Card Document.
|
||||
* Defines the DataSchema and common behaviors for a Combat which are shared between both client and server.
|
||||
* @mixes CombatData
|
||||
*/
|
||||
export default class BaseCombat extends Document {
|
||||
/**
|
||||
* Construct a Combat document using provided data and context.
|
||||
* @param {Partial<CombatData>} data Initial data from which to construct the Combat
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Combat",
|
||||
collection: "combats",
|
||||
label: "DOCUMENT.Combat",
|
||||
labelPlural: "DOCUMENT.Combats",
|
||||
embedded: {
|
||||
Combatant: "combatants"
|
||||
},
|
||||
hasTypeData: true,
|
||||
permissions: {
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.BASE_DOCUMENT_TYPE}),
|
||||
system: new fields.TypeDataField(this),
|
||||
scene: new fields.ForeignDocumentField(documents.BaseScene),
|
||||
combatants: new fields.EmbeddedCollectionField(documents.BaseCombatant),
|
||||
active: new fields.BooleanField(),
|
||||
round: new fields.NumberField({required: true, nullable: false, integer: true, min: 0, initial: 0,
|
||||
label: "COMBAT.Round"}),
|
||||
turn: new fields.NumberField({required: true, integer: true, min: 0, initial: null, label: "COMBAT.Turn"}),
|
||||
sort: new fields.IntegerSortField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing Combat?
|
||||
* @protected
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can do anything
|
||||
const turnOnly = ["_id", "round", "turn", "combatants"]; // Players may only modify a subset of fields
|
||||
if ( Object.keys(data).some(k => !turnOnly.includes(k)) ) return false;
|
||||
if ( ("round" in data) && !doc._canChangeRound(user) ) return false;
|
||||
if ( ("turn" in data) && !doc._canChangeTurn(user) ) return false;
|
||||
if ( ("combatants" in data) && !doc.#canModifyCombatants(user, data.combatants) ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Can a certain User change the Combat round?
|
||||
* @param {User} user The user attempting to change the round
|
||||
* @returns {boolean} Is the user allowed to change the round?
|
||||
* @protected
|
||||
*/
|
||||
_canChangeRound(user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Can a certain User change the Combat turn?
|
||||
* @param {User} user The user attempting to change the turn
|
||||
* @returns {boolean} Is the user allowed to change the turn?
|
||||
* @protected
|
||||
*/
|
||||
_canChangeTurn(user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Can a certain user make modifications to the array of Combatants?
|
||||
* @param {User} user The user attempting to modify combatants
|
||||
* @param {Partial<CombatantData>[]} combatants Proposed combatant changes
|
||||
* @returns {boolean} Is the user allowed to make this change?
|
||||
*/
|
||||
#canModifyCombatants(user, combatants) {
|
||||
for ( const {_id, ...change} of combatants ) {
|
||||
const c = this.combatants.get(_id);
|
||||
if ( !c ) return false;
|
||||
if ( !c.canUserModify(user, "update", change) ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preUpdate(changed, options, user) {
|
||||
const allowed = await super._preUpdate(changed, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
// Don't allow linking to a Scene that doesn't contain all its Combatants
|
||||
if ( !("scene" in changed) ) return;
|
||||
const sceneId = this.schema.fields.scene.clean(changed.scene);
|
||||
if ( (sceneId !== null) && isValidId(sceneId)
|
||||
&& this.combatants.some(c => c.sceneId && (c.sceneId !== sceneId)) ) {
|
||||
throw new Error("You cannot link the Combat to a Scene that doesn't contain all its Combatants.");
|
||||
}
|
||||
}
|
||||
}
|
||||
94
resources/app/common/documents/combatant.mjs
Normal file
94
resources/app/common/documents/combatant.mjs
Normal file
@@ -0,0 +1,94 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").CombatantData} CombatantData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Combatant Document.
|
||||
* Defines the DataSchema and common behaviors for a Combatant which are shared between both client and server.
|
||||
* @mixes CombatantData
|
||||
*/
|
||||
export default class BaseCombatant extends Document {
|
||||
/**
|
||||
* Construct a Combatant document using provided data and context.
|
||||
* @param {Partial<CombatantData>} data Initial data from which to construct the Combatant
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Combatant",
|
||||
collection: "combatants",
|
||||
label: "DOCUMENT.Combatant",
|
||||
labelPlural: "DOCUMENT.Combatants",
|
||||
isEmbedded: true,
|
||||
hasTypeData: true,
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.BASE_DOCUMENT_TYPE}),
|
||||
system: new fields.TypeDataField(this),
|
||||
actorId: new fields.ForeignDocumentField(documents.BaseActor, {label: "COMBAT.CombatantActor", idOnly: true}),
|
||||
tokenId: new fields.ForeignDocumentField(documents.BaseToken, {label: "COMBAT.CombatantToken", idOnly: true}),
|
||||
sceneId: new fields.ForeignDocumentField(documents.BaseScene, {label: "COMBAT.CombatantScene", idOnly: true}),
|
||||
name: new fields.StringField({label: "COMBAT.CombatantName", textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], label: "COMBAT.CombatantImage"}),
|
||||
initiative: new fields.NumberField({label: "COMBAT.CombatantInitiative"}),
|
||||
hidden: new fields.BooleanField({label: "COMBAT.CombatantHidden"}),
|
||||
defeated: new fields.BooleanField({label: "COMBAT.CombatantDefeated"}),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing Combatant?
|
||||
* @private
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can do anything
|
||||
if ( doc.actor && !doc.actor.canUserModify(user, "update", data) ) return false;
|
||||
const updateKeys = new Set(Object.keys(data));
|
||||
const allowedKeys = new Set(["_id", "initiative", "flags", "defeated"]);
|
||||
return updateKeys.isSubset(allowedKeys); // Players may only update initiative scores, flags, and the defeated state
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to create this Combatant?
|
||||
* @private
|
||||
*/
|
||||
static #canCreate(user, doc, data) {
|
||||
if ( user.isGM ) return true;
|
||||
if ( doc.actor ) return doc.actor.canUserModify(user, "update", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getUserLevel(user) {
|
||||
user = user || game.user;
|
||||
const {NONE, OWNER} = CONST.DOCUMENT_OWNERSHIP_LEVELS;
|
||||
if ( user.isGM ) return OWNER;
|
||||
return this.actor?.getUserLevel(user) ?? NONE;
|
||||
}
|
||||
}
|
||||
179
resources/app/common/documents/drawing.mjs
Normal file
179
resources/app/common/documents/drawing.mjs
Normal file
@@ -0,0 +1,179 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import {ShapeData} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").DrawingData} DrawingData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Drawing Document.
|
||||
* Defines the DataSchema and common behaviors for a Drawing which are shared between both client and server.
|
||||
* @mixes DrawingData
|
||||
*/
|
||||
export default class BaseDrawing extends Document {
|
||||
/**
|
||||
* Construct a Drawing document using provided data and context.
|
||||
* @param {Partial<DrawingData>} data Initial data from which to construct the Drawing
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Drawing",
|
||||
collection: "drawings",
|
||||
label: "DOCUMENT.Drawing",
|
||||
labelPlural: "DOCUMENT.Drawings",
|
||||
isEmbedded: true,
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
author: new fields.ForeignDocumentField(documents.BaseUser, {nullable: false, initial: () => game.user?.id}),
|
||||
shape: new fields.EmbeddedDataField(ShapeData),
|
||||
x: new fields.NumberField({required: true, nullable: false, initial: 0, label: "XCoord"}),
|
||||
y: new fields.NumberField({required: true, nullable: false, initial: 0, label: "YCoord"}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
sort: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
rotation: new fields.AngleField({label: "DRAWING.Rotation"}),
|
||||
bezierFactor: new fields.AlphaField({initial: 0, label: "DRAWING.SmoothingFactor", max: 0.5,
|
||||
hint: "DRAWING.SmoothingFactorHint"}),
|
||||
fillType: new fields.NumberField({required: true, nullable: false, initial: CONST.DRAWING_FILL_TYPES.NONE,
|
||||
choices: Object.values(CONST.DRAWING_FILL_TYPES), label: "DRAWING.FillTypes",
|
||||
validationError: "must be a value in CONST.DRAWING_FILL_TYPES"
|
||||
}),
|
||||
fillColor: new fields.ColorField({nullable: false, initial: () => game.user?.color.css || "#ffffff", label: "DRAWING.FillColor"}),
|
||||
fillAlpha: new fields.AlphaField({initial: 0.5, label: "DRAWING.FillOpacity"}),
|
||||
strokeWidth: new fields.NumberField({nullable: false, integer: true, initial: 8, min: 0, label: "DRAWING.LineWidth"}),
|
||||
strokeColor: new fields.ColorField({nullable: false, initial: () => game.user?.color.css || "#ffffff", label: "DRAWING.StrokeColor"}),
|
||||
strokeAlpha: new fields.AlphaField({initial: 1, label: "DRAWING.LineOpacity"}),
|
||||
texture: new fields.FilePathField({categories: ["IMAGE"], label: "DRAWING.FillTexture"}),
|
||||
text: new fields.StringField({label: "DRAWING.TextLabel"}),
|
||||
fontFamily: new fields.StringField({blank: false, label: "DRAWING.FontFamily",
|
||||
initial: () => globalThis.CONFIG?.defaultFontFamily || "Signika"}),
|
||||
fontSize: new fields.NumberField({nullable: false, integer: true, min: 8, max: 256, initial: 48, label: "DRAWING.FontSize",
|
||||
validationError: "must be an integer between 8 and 256"}),
|
||||
textColor: new fields.ColorField({nullable: false, initial: "#ffffff", label: "DRAWING.TextColor"}),
|
||||
textAlpha: new fields.AlphaField({label: "DRAWING.TextOpacity"}),
|
||||
hidden: new fields.BooleanField(),
|
||||
locked: new fields.BooleanField(),
|
||||
interface: new fields.BooleanField(),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate whether the drawing has some visible content (as required by validation).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static #validateVisibleContent(data) {
|
||||
const hasText = (data.text !== "") && (data.textAlpha > 0);
|
||||
const hasFill = (data.fillType !== CONST.DRAWING_FILL_TYPES.NONE) && (data.fillAlpha > 0);
|
||||
const hasLine = (data.strokeWidth > 0) && (data.strokeAlpha > 0);
|
||||
return hasText || hasFill || hasLine;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static validateJoint(data) {
|
||||
if ( !BaseDrawing.#validateVisibleContent(data) ) {
|
||||
throw new Error(game.i18n.localize("DRAWING.JointValidationError"));
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static canUserCreate(user) {
|
||||
return user.hasPermission("DRAWING_CREATE");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to create a new Drawing?
|
||||
* @param {User} user The user attempting the creation operation.
|
||||
* @param {BaseDrawing} doc The Drawing being created.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static #canCreate(user, doc) {
|
||||
if ( !user.isGM && (doc._source.author !== user.id) ) return false;
|
||||
return user.hasPermission("DRAWING_CREATE");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update the Drawing document?
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( !user.isGM && ("author" in data) && (data.author !== user.id) ) return false;
|
||||
return doc.testUserPermission(user, "OWNER");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( !exact && (user.id === this._source.author) ) return true; // The user who created the drawing
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data) {
|
||||
/**
|
||||
* V12 migration to elevation and sort fields
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "z", "elevation");
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
this._addDataFieldShim(data, "z", "elevation", {since: 12, until: 14});
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get z() {
|
||||
this.constructor._logDataFieldMigration("z", "elevation", {since: 12, until: 14});
|
||||
return this.elevation;
|
||||
}
|
||||
}
|
||||
76
resources/app/common/documents/fog-exploration.mjs
Normal file
76
resources/app/common/documents/fog-exploration.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").FogExplorationData} FogExplorationData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The FogExploration Document.
|
||||
* Defines the DataSchema and common behaviors for a FogExploration which are shared between both client and server.
|
||||
* @mixes FogExplorationData
|
||||
*/
|
||||
export default class BaseFogExploration extends Document {
|
||||
/**
|
||||
* Construct a FogExploration document using provided data and context.
|
||||
* @param {Partial<FogExplorationData>} data Initial data from which to construct the FogExploration
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "FogExploration",
|
||||
collection: "fog",
|
||||
label: "DOCUMENT.FogExploration",
|
||||
labelPlural: "DOCUMENT.FogExplorations",
|
||||
isPrimary: true,
|
||||
permissions: {
|
||||
create: "PLAYER",
|
||||
update: this.#canModify,
|
||||
delete: this.#canModify
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
scene: new fields.ForeignDocumentField(documents.BaseScene, {initial: () => canvas?.scene?.id}),
|
||||
user: new fields.ForeignDocumentField(documents.BaseUser, {initial: () => game?.user?.id}),
|
||||
explored: new fields.FilePathField({categories: ["IMAGE"], required: true, base64: true}),
|
||||
positions: new fields.ObjectField(),
|
||||
timestamp: new fields.NumberField({nullable: false, initial: Date.now}),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a User can modify a FogExploration document.
|
||||
*/
|
||||
static #canModify(user, doc) {
|
||||
return (user.id === doc._source.user) || user.hasRole("ASSISTANT");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Database Event Handlers */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preUpdate(changed, options, user) {
|
||||
const allowed = await super._preUpdate(changed, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
changed.timestamp = Date.now();
|
||||
}
|
||||
}
|
||||
82
resources/app/common/documents/folder.mjs
Normal file
82
resources/app/common/documents/folder.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").FolderData} FolderData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Folder Document.
|
||||
* Defines the DataSchema and common behaviors for a Folder which are shared between both client and server.
|
||||
* @mixes FolderData
|
||||
*/
|
||||
export default class BaseFolder extends Document {
|
||||
/**
|
||||
* Construct a Folder document using provided data and context.
|
||||
* @param {Partial<FolderData>} data Initial data from which to construct the Folder
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Folder",
|
||||
collection: "folders",
|
||||
label: "DOCUMENT.Folder",
|
||||
labelPlural: "DOCUMENT.Folders",
|
||||
coreTypes: CONST.FOLDER_DOCUMENT_TYPES,
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
type: new fields.DocumentTypeField(this),
|
||||
description: new fields.HTMLField({textSearch: true}),
|
||||
folder: new fields.ForeignDocumentField(BaseFolder),
|
||||
sorting: new fields.StringField({required: true, initial: "a", choices: this.SORTING_MODES}),
|
||||
sort: new fields.IntegerSortField(),
|
||||
color: new fields.ColorField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
static validateJoint(data) {
|
||||
if ( (data.folder !== null) && (data.folder === data._id) ) {
|
||||
throw new Error("A Folder may not contain itself");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow folder sorting modes
|
||||
* @type {string[]}
|
||||
*/
|
||||
static SORTING_MODES = ["a", "m"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get(documentId, options={}) {
|
||||
if ( !documentId ) return null;
|
||||
if ( !options.pack ) return super.get(documentId, options);
|
||||
const pack = game.packs.get(options.pack);
|
||||
if ( !pack ) {
|
||||
console.error(`The ${this.name} model references a non-existent pack ${options.pack}.`);
|
||||
return null;
|
||||
}
|
||||
return pack.folders.get(documentId);
|
||||
}
|
||||
}
|
||||
112
resources/app/common/documents/item.mjs
Normal file
112
resources/app/common/documents/item.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").ItemData} ItemData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Item Document.
|
||||
* Defines the DataSchema and common behaviors for a Item which are shared between both client and server.
|
||||
* @mixes ItemData
|
||||
*/
|
||||
export default class BaseItem extends Document {
|
||||
/**
|
||||
* Construct a Item document using provided data and context.
|
||||
* @param {Partial<ItemData>} data Initial data from which to construct the Item
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Item",
|
||||
collection: "items",
|
||||
hasTypeData: true,
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "img", "type", "sort", "folder"],
|
||||
embedded: {ActiveEffect: "effects"},
|
||||
label: "DOCUMENT.Item",
|
||||
labelPlural: "DOCUMENT.Items",
|
||||
permissions: {create: "ITEM_CREATE"},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
type: new fields.DocumentTypeField(this),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], initial: data => {
|
||||
return this.implementation.getDefaultArtwork(data).img;
|
||||
}}),
|
||||
system: new fields.TypeDataField(this),
|
||||
effects: new fields.EmbeddedCollectionField(documents.BaseActiveEffect),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* The default icon used for newly created Item documents
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = "icons/svg/item-bag.svg";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Determine default artwork based on the provided item data.
|
||||
* @param {ItemData} itemData The source item data.
|
||||
* @returns {{img: string}} Candidate item image.
|
||||
*/
|
||||
static getDefaultArtwork(itemData) {
|
||||
return { img: this.DEFAULT_ICON };
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
canUserModify(user, action, data={}) {
|
||||
if ( this.isEmbedded ) return this.parent.canUserModify(user, "update");
|
||||
return super.canUserModify(user, action, data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
88
resources/app/common/documents/journal-entry-page.mjs
Normal file
88
resources/app/common/documents/journal-entry-page.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").JournalEntryPageData} JournalEntryPageData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The JournalEntryPage Document.
|
||||
* Defines the DataSchema and common behaviors for a JournalEntryPage which are shared between both client and server.
|
||||
* @mixes JournalEntryPageData
|
||||
*/
|
||||
export default class BaseJournalEntryPage extends Document {
|
||||
/**
|
||||
* Construct a JournalEntryPage document using provided data and context.
|
||||
* @param {Partial<JournalEntryPageData>} data Initial data from which to construct the JournalEntryPage
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "JournalEntryPage",
|
||||
collection: "pages",
|
||||
hasTypeData: true,
|
||||
indexed: true,
|
||||
label: "DOCUMENT.JournalEntryPage",
|
||||
labelPlural: "DOCUMENT.JournalEntryPages",
|
||||
coreTypes: ["text", "image", "pdf", "video"],
|
||||
compendiumIndexFields: ["name", "type", "sort"],
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "JOURNALENTRYPAGE.PageTitle", textSearch: true}),
|
||||
type: new fields.DocumentTypeField(this, {initial: "text"}),
|
||||
system: new fields.TypeDataField(this),
|
||||
title: new fields.SchemaField({
|
||||
show: new fields.BooleanField({initial: true}),
|
||||
level: new fields.NumberField({required: true, initial: 1, min: 1, max: 6, integer: true, nullable: false})
|
||||
}),
|
||||
image: new fields.SchemaField({
|
||||
caption: new fields.StringField({required: false, initial: undefined})
|
||||
}),
|
||||
text: new fields.SchemaField({
|
||||
content: new fields.HTMLField({required: false, initial: undefined, textSearch: true}),
|
||||
markdown: new fields.StringField({required: false, initial: undefined}),
|
||||
format: new fields.NumberField({label: "JOURNALENTRYPAGE.Format",
|
||||
initial: CONST.JOURNAL_ENTRY_PAGE_FORMATS.HTML, choices: Object.values(CONST.JOURNAL_ENTRY_PAGE_FORMATS)})
|
||||
}),
|
||||
video: new fields.SchemaField({
|
||||
controls: new fields.BooleanField({initial: true}),
|
||||
loop: new fields.BooleanField({required: false, initial: undefined}),
|
||||
autoplay: new fields.BooleanField({required: false, initial: undefined}),
|
||||
volume: new fields.AlphaField({required: true, step: 0.01, initial: .5}),
|
||||
timestamp: new fields.NumberField({required: false, min: 0, initial: undefined}),
|
||||
width: new fields.NumberField({required: false, positive: true, integer: true, initial: undefined}),
|
||||
height: new fields.NumberField({required: false, positive: true, integer: true, initial: undefined})
|
||||
}),
|
||||
src: new fields.StringField({required: false, blank: false, nullable: true, initial: null,
|
||||
label: "JOURNALENTRYPAGE.Source"}),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField({initial: {default: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT}}),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
};
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getUserLevel(user) {
|
||||
user = user || game.user;
|
||||
const ownership = this.ownership[user.id] ?? this.ownership.default;
|
||||
const inherited = ownership === CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
|
||||
return inherited ? this.parent.getUserLevel(user) : ownership;
|
||||
}
|
||||
}
|
||||
71
resources/app/common/documents/journal-entry.mjs
Normal file
71
resources/app/common/documents/journal-entry.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").JournalEntryData} JournalEntryData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The JournalEntry Document.
|
||||
* Defines the DataSchema and common behaviors for a JournalEntry which are shared between both client and server.
|
||||
* @mixes JournalEntryData
|
||||
*/
|
||||
export default class BaseJournalEntry extends Document {
|
||||
/**
|
||||
* Construct a JournalEntry document using provided data and context.
|
||||
* @param {Partial<JournalEntryData>} data Initial data from which to construct the JournalEntry
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "JournalEntry",
|
||||
collection: "journal",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "sort", "folder"],
|
||||
embedded: {JournalEntryPage: "pages"},
|
||||
label: "DOCUMENT.JournalEntry",
|
||||
labelPlural: "DOCUMENT.JournalEntries",
|
||||
permissions: {
|
||||
create: "JOURNAL_CREATE"
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
pages: new fields.EmbeddedCollectionField(documents.BaseJournalEntryPage),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
147
resources/app/common/documents/macro.mjs
Normal file
147
resources/app/common/documents/macro.mjs
Normal file
@@ -0,0 +1,147 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").MacroData} MacroData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Macro Document.
|
||||
* Defines the DataSchema and common behaviors for a Macro which are shared between both client and server.
|
||||
* @mixes MacroData
|
||||
*/
|
||||
export default class BaseMacro extends Document {
|
||||
/**
|
||||
* Construct a Macro document using provided data and context.
|
||||
* @param {Partial<MacroData>} data Initial data from which to construct the Macro
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Macro",
|
||||
collection: "macros",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "img", "sort", "folder"],
|
||||
label: "DOCUMENT.Macro",
|
||||
labelPlural: "DOCUMENT.Macros",
|
||||
coreTypes: Object.values(CONST.MACRO_TYPES),
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "Name", textSearch: true}),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.MACRO_TYPES.CHAT, label: "Type"}),
|
||||
author: new fields.ForeignDocumentField(documents.BaseUser, {initial: () => game?.user?.id}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], initial: () => this.DEFAULT_ICON, label: "Image"}),
|
||||
scope: new fields.StringField({required: true, choices: CONST.MACRO_SCOPES, initial: CONST.MACRO_SCOPES[0],
|
||||
validationError: "must be a value in CONST.MACRO_SCOPES", label: "Scope"}),
|
||||
command: new fields.StringField({required: true, blank: true, label: "Command"}),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default icon used for newly created Macro documents.
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = "icons/svg/dice-target.svg";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static validateJoint(data) {
|
||||
if ( data.type !== CONST.MACRO_TYPES.SCRIPT ) return;
|
||||
const field = new fields.JavaScriptField({ async: true });
|
||||
const failure = field.validate(data.command);
|
||||
if ( failure ) throw failure.asError();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static canUserCreate(user) {
|
||||
return user.hasRole("PLAYER");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to create the Macro document?
|
||||
*/
|
||||
static #canCreate(user, doc) {
|
||||
if ( !user.isGM && (doc._source.author !== user.id) ) return false;
|
||||
if ( (doc._source.type === "script") && !user.hasPermission("MACRO_SCRIPT") ) return false;
|
||||
return user.hasRole("PLAYER");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update the Macro document?
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( !user.isGM && ("author" in data) && (data.author !== user.id) ) return false;
|
||||
if ( !user.hasPermission("MACRO_SCRIPT") ) {
|
||||
if ( data.type === "script" ) return false;
|
||||
if ( (doc._source.type === "script") && ("command" in data) ) return false;
|
||||
}
|
||||
return doc.testUserPermission(user, "OWNER");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( !exact && (user.id === this._source.author) ) return true; // Macro authors can edit
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Database Event Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
this.updateSource({author: user.id});
|
||||
}
|
||||
}
|
||||
135
resources/app/common/documents/measured-template.mjs
Normal file
135
resources/app/common/documents/measured-template.mjs
Normal file
@@ -0,0 +1,135 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").MeasuredTemplateData} MeasuredTemplateData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The MeasuredTemplate Document.
|
||||
* Defines the DataSchema and common behaviors for a MeasuredTemplate which are shared between both client and server.
|
||||
* @mixes MeasuredTemplateData
|
||||
*/
|
||||
export default class BaseMeasuredTemplate extends Document {
|
||||
/**
|
||||
* Construct a MeasuredTemplate document using provided data and context.
|
||||
* @param {Partial<MeasuredTemplateData>} data Initial data from which to construct the MeasuredTemplate
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = mergeObject(super.metadata, {
|
||||
name: "MeasuredTemplate",
|
||||
collection: "templates",
|
||||
label: "DOCUMENT.MeasuredTemplate",
|
||||
labelPlural: "DOCUMENT.MeasuredTemplates",
|
||||
isEmbedded: true,
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false});
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
author: new fields.ForeignDocumentField(documents.BaseUser, {initial: () => game?.user?.id}),
|
||||
t: new fields.StringField({required: true, choices: Object.values(CONST.MEASURED_TEMPLATE_TYPES), label: "Type",
|
||||
initial: CONST.MEASURED_TEMPLATE_TYPES.CIRCLE,
|
||||
validationError: "must be a value in CONST.MEASURED_TEMPLATE_TYPES",
|
||||
}),
|
||||
x: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
|
||||
y: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
sort: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
distance: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, label: "Distance"}),
|
||||
direction: new fields.AngleField({label: "Direction"}),
|
||||
angle: new fields.AngleField({normalize: false, label: "Angle"}),
|
||||
width: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01, label: "Width"}),
|
||||
borderColor: new fields.ColorField({nullable: false, initial: "#000000"}),
|
||||
fillColor: new fields.ColorField({nullable: false, initial: () => game.user?.color.css || "#ffffff"}),
|
||||
texture: new fields.FilePathField({categories: ["IMAGE", "VIDEO"]}),
|
||||
hidden: new fields.BooleanField({label: "Hidden"}),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to create a new MeasuredTemplate?
|
||||
* @param {User} user The user attempting the creation operation.
|
||||
* @param {BaseMeasuredTemplate} doc The MeasuredTemplate being created.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static #canCreate(user, doc) {
|
||||
if ( !user.isGM && (doc._source.author !== user.id) ) return false;
|
||||
return user.hasPermission("TEMPLATE_CREATE");
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update the MeasuredTemplate document?
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( !user.isGM && ("author" in data) && (data.author !== user.id) ) return false;
|
||||
return doc.testUserPermission(user, "OWNER");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( !exact && (user.id === this._source.author) ) return true; // The user who created the template
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data) {
|
||||
/**
|
||||
* V12 migration from user to author
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "user", "author");
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
this._addDataFieldShim(data, "user", "author", {since: 12, until: 14})
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get user() {
|
||||
this.constructor._logDataFieldMigration("user", "author", {since: 12, until: 14});
|
||||
return this.author;
|
||||
}
|
||||
}
|
||||
90
resources/app/common/documents/note.mjs
Normal file
90
resources/app/common/documents/note.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import {TextureData} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").NoteData} NoteData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Note Document.
|
||||
* Defines the DataSchema and common behaviors for a Note which are shared between both client and server.
|
||||
* @mixes NoteData
|
||||
*/
|
||||
export default class BaseNote extends Document {
|
||||
/**
|
||||
* Construct a Note document using provided data and context.
|
||||
* @param {Partial<NoteData>} data Initial data from which to construct the Note
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Note",
|
||||
collection: "notes",
|
||||
label: "DOCUMENT.Note",
|
||||
labelPlural: "DOCUMENT.Notes",
|
||||
permissions: {
|
||||
create: "NOTE_CREATE"
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
entryId: new fields.ForeignDocumentField(documents.BaseJournalEntry, {idOnly: true}),
|
||||
pageId: new fields.ForeignDocumentField(documents.BaseJournalEntryPage, {idOnly: true}),
|
||||
x: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
|
||||
y: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
sort: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
texture: new TextureData({}, {categories: ["IMAGE"],
|
||||
initial: {src: () => this.DEFAULT_ICON, anchorX: 0.5, anchorY: 0.5, fit: "contain"}, label: "NOTE.EntryIcon"}),
|
||||
iconSize: new fields.NumberField({required: true, nullable: false, integer: true, min: 32, initial: 40,
|
||||
validationError: "must be an integer greater than 32", label: "NOTE.IconSize"}),
|
||||
text: new fields.StringField({label: "NOTE.TextLabel", textSearch: true}),
|
||||
fontFamily: new fields.StringField({required: true, label: "NOTE.FontFamily",
|
||||
initial: () => globalThis.CONFIG?.defaultFontFamily || "Signika"}),
|
||||
fontSize: new fields.NumberField({required: true, integer: true, min: 8, max: 128, initial: 32,
|
||||
validationError: "must be an integer between 8 and 128", label: "NOTE.FontSize"}),
|
||||
textAnchor: new fields.NumberField({required: true, choices: Object.values(CONST.TEXT_ANCHOR_POINTS),
|
||||
initial: CONST.TEXT_ANCHOR_POINTS.BOTTOM, label: "NOTE.AnchorPoint",
|
||||
validationError: "must be a value in CONST.TEXT_ANCHOR_POINTS"}),
|
||||
textColor: new fields.ColorField({required: true, nullable: false, initial: "#ffffff", label: "NOTE.TextColor"}),
|
||||
global: new fields.BooleanField(),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default icon used for newly created Note documents.
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = "icons/svg/book.svg";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( user.isGM ) return true; // Game-masters always have control
|
||||
// Players can create and edit unlinked notes with the appropriate permission.
|
||||
if ( !this.entryId ) return user.hasPermission("NOTE_CREATE");
|
||||
if ( !this.entry ) return false; // Otherwise, permission comes through the JournalEntry
|
||||
return this.entry.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
}
|
||||
68
resources/app/common/documents/playlist-sound.mjs
Normal file
68
resources/app/common/documents/playlist-sound.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").PlaylistSoundData} PlaylistSoundData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The PlaylistSound Document.
|
||||
* Defines the DataSchema and common behaviors for a PlaylistSound which are shared between both client and server.
|
||||
* @mixes PlaylistSoundData
|
||||
*/
|
||||
export default class BasePlaylistSound extends Document {
|
||||
/**
|
||||
* Construct a PlaylistSound document using provided data and context.
|
||||
* @param {Partial<PlaylistSoundData>} data Initial data from which to construct the PlaylistSound
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "PlaylistSound",
|
||||
collection: "sounds",
|
||||
indexed: true,
|
||||
label: "DOCUMENT.PlaylistSound",
|
||||
labelPlural: "DOCUMENT.PlaylistSounds",
|
||||
compendiumIndexFields: ["name", "sort"],
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
description: new fields.StringField(),
|
||||
path: new fields.FilePathField({categories: ["AUDIO"]}),
|
||||
channel: new fields.StringField({choices: CONST.AUDIO_CHANNELS, initial: "music", blank: true}),
|
||||
playing: new fields.BooleanField(),
|
||||
pausedTime: new fields.NumberField({min: 0}),
|
||||
repeat: new fields.BooleanField(),
|
||||
volume: new fields.AlphaField({initial: 0.5, step: 0.01}),
|
||||
fade: new fields.NumberField({integer: true, min: 0}),
|
||||
sort: new fields.IntegerSortField(),
|
||||
flags: new fields.ObjectField(),
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact = false} = {}) {
|
||||
if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
}
|
||||
81
resources/app/common/documents/playlist.mjs
Normal file
81
resources/app/common/documents/playlist.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").PlaylistData} PlaylistData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Playlist Document.
|
||||
* Defines the DataSchema and common behaviors for a Playlist which are shared between both client and server.
|
||||
* @mixes PlaylistData
|
||||
*/
|
||||
export default class BasePlaylist extends Document {
|
||||
/**
|
||||
* Construct a Playlist document using provided data and context.
|
||||
* @param {Partial<PlaylistData>} data Initial data from which to construct the Playlist
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Playlist",
|
||||
collection: "playlists",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "description", "sort", "folder"],
|
||||
embedded: {PlaylistSound: "sounds"},
|
||||
label: "DOCUMENT.Playlist",
|
||||
labelPlural: "DOCUMENT.Playlists",
|
||||
permissions: {
|
||||
create: "PLAYLIST_CREATE"
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
description: new fields.StringField({textSearch: true}),
|
||||
sounds: new fields.EmbeddedCollectionField(documents.BasePlaylistSound),
|
||||
channel: new fields.StringField({choices: CONST.AUDIO_CHANNELS, initial: "music", blank: false}),
|
||||
mode: new fields.NumberField({required: true, choices: Object.values(CONST.PLAYLIST_MODES),
|
||||
initial: CONST.PLAYLIST_MODES.SEQUENTIAL, validationError: "must be a value in CONST.PLAYLIST_MODES"}),
|
||||
playing: new fields.BooleanField(),
|
||||
fade: new fields.NumberField({positive: true}),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sorting: new fields.StringField({required: true, choices: Object.values(CONST.PLAYLIST_SORT_MODES),
|
||||
initial: CONST.PLAYLIST_SORT_MODES.ALPHABETICAL,
|
||||
validationError: "must be a value in CONST.PLAYLIST_SORTING_MODES"}),
|
||||
seed: new fields.NumberField({integer: true, min: 0}),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
85
resources/app/common/documents/region-behavior.mjs
Normal file
85
resources/app/common/documents/region-behavior.mjs
Normal file
@@ -0,0 +1,85 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").RegionBehaviorData} RegionBehaviorData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The RegionBehavior Document.
|
||||
* Defines the DataSchema and common behaviors for a RegionBehavior which are shared between both client and server.
|
||||
* @mixes SceneRegionData
|
||||
*/
|
||||
export default class BaseRegionBehavior extends Document {
|
||||
/**
|
||||
* Construct a RegionBehavior document using provided data and context.
|
||||
* @param {Partial<RegionBehaviorData>} data Initial data from which to construct the RegionBehavior
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "RegionBehavior",
|
||||
collection: "behaviors",
|
||||
label: "DOCUMENT.RegionBehavior",
|
||||
labelPlural: "DOCUMENT.RegionBehaviors",
|
||||
coreTypes: ["adjustDarknessLevel", "displayScrollingText", "executeMacro", "executeScript", "pauseGame", "suppressWeather", "teleportToken", "toggleBehavior"],
|
||||
hasTypeData: true,
|
||||
isEmbedded: true,
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: true, label: "Name", textSearch: true}),
|
||||
type: new fields.DocumentTypeField(this),
|
||||
system: new fields.TypeDataField(this),
|
||||
disabled: new fields.BooleanField({label: "BEHAVIOR.FIELDS.disabled.label", hint: "BEHAVIOR.FIELDS.disabled.hint"}),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static canUserCreate(user) {
|
||||
return user.isGM;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to create the RegionBehavior document?
|
||||
*/
|
||||
static #canCreate(user, doc) {
|
||||
if ( (doc._source.type === "executeScript") && !user.hasPermission("MACRO_SCRIPT") ) return false;
|
||||
return user.isGM;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update the RegionBehavior document?
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( (((doc._source.type === "executeScript") && ("system" in data) && ("source" in data.system))
|
||||
|| (data.type === "executeScript")) && !user.hasPermission("MACRO_SCRIPT") ) return false;
|
||||
return user.isGM;
|
||||
}
|
||||
}
|
||||
81
resources/app/common/documents/region.mjs
Normal file
81
resources/app/common/documents/region.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import {BaseShapeData} from "../data/data.mjs";
|
||||
import Color from "../utils/color.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").RegionData} RegionData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Region Document.
|
||||
* Defines the DataSchema and common behaviors for a Region which are shared between both client and server.
|
||||
* @mixes RegionData
|
||||
*/
|
||||
export default class BaseRegion extends Document {
|
||||
/**
|
||||
* Construct a Region document using provided data and context.
|
||||
* @param {Partial<RegionData>} data Initial data from which to construct the Region
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Region",
|
||||
collection: "regions",
|
||||
label: "DOCUMENT.Region",
|
||||
labelPlural: "DOCUMENT.Regions",
|
||||
isEmbedded: true,
|
||||
embedded: {
|
||||
RegionBehavior: "behaviors"
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, label: "Name", textSearch: true}),
|
||||
color: new fields.ColorField({required: true, nullable: false,
|
||||
initial: () => Color.fromHSV([Math.random(), 0.8, 0.8]).css,
|
||||
label: "REGION.FIELDS.color.label",
|
||||
hint: "REGION.FIELDS.color.hint"}),
|
||||
shapes: new fields.ArrayField(new fields.TypedSchemaField(BaseShapeData.TYPES),
|
||||
{label: "REGION.FIELDS.shapes.label", hint: "REGION.FIELDS.shapes.hint"}),
|
||||
elevation: new fields.SchemaField({
|
||||
bottom: new fields.NumberField({required: true,
|
||||
label: "REGION.FIELDS.elevation.FIELDS.bottom.label",
|
||||
hint: "REGION.FIELDS.elevation.FIELDS.bottom.hint"}), // null -> -Infinity
|
||||
top: new fields.NumberField({required: true,
|
||||
label: "REGION.FIELDS.elevation.FIELDS.top.label",
|
||||
hint: "REGION.FIELDS.elevation.FIELDS.top.hint"}) // null -> +Infinity
|
||||
}, {
|
||||
label: "REGION.FIELDS.elevation.label",
|
||||
hint: "REGION.FIELDS.elevation.hint",
|
||||
validate: d => (d.bottom ?? -Infinity) <= (d.top ?? Infinity),
|
||||
validationError: "elevation.top may not be less than elevation.bottom"
|
||||
}),
|
||||
behaviors: new fields.EmbeddedCollectionField(documents.BaseRegionBehavior, {label: "REGION.FIELDS.behaviors.label",
|
||||
hint: "REGION.FIELDS.behaviors.hint"}),
|
||||
visibility: new fields.NumberField({required: true,
|
||||
initial: CONST.REGION_VISIBILITY.LAYER,
|
||||
choices: Object.fromEntries(Object.entries(CONST.REGION_VISIBILITY).map(([key, value]) =>
|
||||
[value, {label: `REGION.VISIBILITY.${key}.label`}])),
|
||||
label: "REGION.FIELDS.visibility.label",
|
||||
hint: "REGION.FIELDS.visibility.hint"}),
|
||||
locked: new fields.BooleanField(),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
};
|
||||
}
|
||||
79
resources/app/common/documents/roll-table.mjs
Normal file
79
resources/app/common/documents/roll-table.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").RollTableData} RollTableData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The RollTable Document.
|
||||
* Defines the DataSchema and common behaviors for a RollTable which are shared between both client and server.
|
||||
* @mixes RollTableData
|
||||
*/
|
||||
export default class BaseRollTable extends Document {
|
||||
/**
|
||||
* Construct a RollTable document using provided data and context.
|
||||
* @param {Partial<RollTableData>} data Initial data from which to construct the RollTable
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "RollTable",
|
||||
collection: "tables",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "description", "img", "sort", "folder"],
|
||||
embedded: {TableResult: "results"},
|
||||
label: "DOCUMENT.RollTable",
|
||||
labelPlural: "DOCUMENT.RollTables",
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritDoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"], initial: () => this.DEFAULT_ICON}),
|
||||
description: new fields.HTMLField({textSearch: true}),
|
||||
results: new fields.EmbeddedCollectionField(documents.BaseTableResult),
|
||||
formula: new fields.StringField(),
|
||||
replacement: new fields.BooleanField({initial: true}),
|
||||
displayRoll: new fields.BooleanField({initial: true}),
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default icon used for newly created Macro documents
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = "icons/svg/d20-grey.svg";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(source) {
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
262
resources/app/common/documents/scene.mjs
Normal file
262
resources/app/common/documents/scene.mjs
Normal file
@@ -0,0 +1,262 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {TextureData} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").SceneData} SceneData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Scene Document.
|
||||
* Defines the DataSchema and common behaviors for a Scene which are shared between both client and server.
|
||||
* @mixes SceneData
|
||||
*/
|
||||
export default class BaseScene extends Document {
|
||||
/**
|
||||
* Construct a Scene document using provided data and context.
|
||||
* @param {Partial<SceneData>} data Initial data from which to construct the Scene
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Scene",
|
||||
collection: "scenes",
|
||||
indexed: true,
|
||||
compendiumIndexFields: ["_id", "name", "thumb", "sort", "folder"],
|
||||
embedded: {
|
||||
AmbientLight: "lights",
|
||||
AmbientSound: "sounds",
|
||||
Drawing: "drawings",
|
||||
MeasuredTemplate: "templates",
|
||||
Note: "notes",
|
||||
Region: "regions",
|
||||
Tile: "tiles",
|
||||
Token: "tokens",
|
||||
Wall: "walls"
|
||||
},
|
||||
label: "DOCUMENT.Scene",
|
||||
labelPlural: "DOCUMENT.Scenes",
|
||||
preserveOnImport: [...super.metadata.preserveOnImport, "active"],
|
||||
schemaVersion: "12.325"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
// Define reusable ambience schema for environment
|
||||
const environmentData = defaults => new fields.SchemaField({
|
||||
hue: new fields.HueField({required: true, initial: defaults.hue,
|
||||
label: "SCENES.ENVIRONMENT.Hue", hint: "SCENES.ENVIRONMENT.HueHint"}),
|
||||
intensity: new fields.AlphaField({required: true, nullable: false, initial: defaults.intensity,
|
||||
label: "SCENES.ENVIRONMENT.Intensity", hint: "SCENES.ENVIRONMENT.IntensityHint"}),
|
||||
luminosity: new fields.NumberField({required: true, nullable: false, initial: defaults.luminosity, min: -1, max: 1,
|
||||
label: "SCENES.ENVIRONMENT.Luminosity", hint: "SCENES.ENVIRONMENT.LuminosityHint"}),
|
||||
saturation: new fields.NumberField({required: true, nullable: false, initial: defaults.saturation, min: -1, max: 1,
|
||||
label: "SCENES.ENVIRONMENT.Saturation", hint: "SCENES.ENVIRONMENT.SaturationHint"}),
|
||||
shadows: new fields.NumberField({required: true, nullable: false, initial: defaults.shadows, min: 0, max: 1,
|
||||
label: "SCENES.ENVIRONMENT.Shadows", hint: "SCENES.ENVIRONMENT.ShadowsHint"})
|
||||
});
|
||||
// Reuse parts of the LightData schema for the global light
|
||||
const lightDataSchema = foundry.data.LightData.defineSchema();
|
||||
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
|
||||
// Navigation
|
||||
active: new fields.BooleanField(),
|
||||
navigation: new fields.BooleanField({initial: true}),
|
||||
navOrder: new fields.NumberField({required: true, nullable: false, integer: true, initial: 0}),
|
||||
navName: new fields.HTMLField({textSearch: true}),
|
||||
|
||||
// Canvas Dimensions
|
||||
background: new TextureData(),
|
||||
foreground: new fields.FilePathField({categories: ["IMAGE", "VIDEO"]}),
|
||||
foregroundElevation: new fields.NumberField({required: true, positive: true, integer: true}),
|
||||
thumb: new fields.FilePathField({categories: ["IMAGE"]}),
|
||||
width: new fields.NumberField({integer: true, positive: true, initial: 4000}),
|
||||
height: new fields.NumberField({integer: true, positive: true, initial: 3000}),
|
||||
padding: new fields.NumberField({required: true, nullable: false, min: 0, max: 0.5, step: 0.05, initial: 0.25}),
|
||||
initial: new fields.SchemaField({
|
||||
x: new fields.NumberField({integer: true, required: true}),
|
||||
y: new fields.NumberField({integer: true, required: true}),
|
||||
scale: new fields.NumberField({required: true, max: 3, positive: true, initial: 0.5})
|
||||
}),
|
||||
backgroundColor: new fields.ColorField({nullable: false, initial: "#999999"}),
|
||||
|
||||
// Grid Configuration
|
||||
grid: new fields.SchemaField({
|
||||
type: new fields.NumberField({required: true, choices: Object.values(CONST.GRID_TYPES),
|
||||
initial: () => game.system.grid.type, validationError: "must be a value in CONST.GRID_TYPES"}),
|
||||
size: new fields.NumberField({required: true, nullable: false, integer: true, min: CONST.GRID_MIN_SIZE,
|
||||
initial: 100, validationError: `must be an integer number of pixels, ${CONST.GRID_MIN_SIZE} or greater`}),
|
||||
style: new fields.StringField({required: true, blank: false, initial: "solidLines"}),
|
||||
thickness: new fields.NumberField({required: true, nullable: false, positive: true, integer: true, initial: 1}),
|
||||
color: new fields.ColorField({required: true, nullable: false, initial: "#000000"}),
|
||||
alpha: new fields.AlphaField({initial: 0.2}),
|
||||
distance: new fields.NumberField({required: true, nullable: false, positive: true,
|
||||
initial: () => game.system.grid.distance}),
|
||||
units: new fields.StringField({required: true, initial: () => game.system.grid.units})
|
||||
}),
|
||||
|
||||
// Vision Configuration
|
||||
tokenVision: new fields.BooleanField({initial: true}),
|
||||
fog: new fields.SchemaField({
|
||||
exploration: new fields.BooleanField({initial: true}),
|
||||
reset: new fields.NumberField({required: false, initial: undefined}),
|
||||
overlay: new fields.FilePathField({categories: ["IMAGE", "VIDEO"]}),
|
||||
colors: new fields.SchemaField({
|
||||
explored: new fields.ColorField({label: "SCENES.FogExploredColor"}),
|
||||
unexplored: new fields.ColorField({label: "SCENES.FogUnexploredColor"})
|
||||
})
|
||||
}),
|
||||
|
||||
// Environment Configuration
|
||||
environment: new fields.SchemaField({
|
||||
darknessLevel: new fields.AlphaField({initial: 0}),
|
||||
darknessLock: new fields.BooleanField({initial: false}),
|
||||
globalLight: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({required: true, initial: false}),
|
||||
alpha: lightDataSchema.alpha,
|
||||
bright: new fields.BooleanField({required: true, initial: false}),
|
||||
color: lightDataSchema.color,
|
||||
coloration: lightDataSchema.coloration,
|
||||
luminosity: new fields.NumberField({required: true, nullable: false, initial: 0, min: 0, max: 1}),
|
||||
saturation: lightDataSchema.saturation,
|
||||
contrast: lightDataSchema.contrast,
|
||||
shadows: lightDataSchema.shadows,
|
||||
darkness: lightDataSchema.darkness
|
||||
}),
|
||||
cycle: new fields.BooleanField({initial: true}),
|
||||
base: environmentData({hue: 0, intensity: 0, luminosity: 0, saturation: 0, shadows: 0}),
|
||||
dark: environmentData({hue: 257/360, intensity: 0, luminosity: -0.25, saturation: 0, shadows: 0})
|
||||
}),
|
||||
|
||||
// Embedded Collections
|
||||
drawings: new fields.EmbeddedCollectionField(documents.BaseDrawing),
|
||||
tokens: new fields.EmbeddedCollectionField(documents.BaseToken),
|
||||
lights: new fields.EmbeddedCollectionField(documents.BaseAmbientLight),
|
||||
notes: new fields.EmbeddedCollectionField(documents.BaseNote),
|
||||
sounds: new fields.EmbeddedCollectionField(documents.BaseAmbientSound),
|
||||
regions: new fields.EmbeddedCollectionField(documents.BaseRegion),
|
||||
templates: new fields.EmbeddedCollectionField(documents.BaseMeasuredTemplate),
|
||||
tiles: new fields.EmbeddedCollectionField(documents.BaseTile),
|
||||
walls: new fields.EmbeddedCollectionField(documents.BaseWall),
|
||||
|
||||
// Linked Documents
|
||||
playlist: new fields.ForeignDocumentField(documents.BasePlaylist),
|
||||
playlistSound: new fields.ForeignDocumentField(documents.BasePlaylistSound, {idOnly: true}),
|
||||
journal: new fields.ForeignDocumentField(documents.BaseJournalEntry),
|
||||
journalEntryPage: new fields.ForeignDocumentField(documents.BaseJournalEntryPage, {idOnly: true}),
|
||||
weather: new fields.StringField({required: true}),
|
||||
|
||||
// Permissions
|
||||
folder: new fields.ForeignDocumentField(documents.BaseFolder),
|
||||
sort: new fields.IntegerSortField(),
|
||||
ownership: new fields.DocumentOwnershipField(),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Static Initializer Block for deprecated properties.
|
||||
* @see [Static Initialization Blocks](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks)
|
||||
*/
|
||||
static {
|
||||
const migrations = {
|
||||
fogExploration: "fog.exploration",
|
||||
fogReset: "fog.reset",
|
||||
fogOverlay: "fog.overlay",
|
||||
fogExploredColor: "fog.colors.explored",
|
||||
fogUnexploredColor: "fog.colors.unexplored",
|
||||
globalLight: "environment.globalLight.enabled",
|
||||
globalLightThreshold: "environment.globalLight.darkness.max",
|
||||
darkness: "environment.darknessLevel"
|
||||
};
|
||||
Object.defineProperties(this.prototype, Object.fromEntries(
|
||||
Object.entries(migrations).map(([o, n]) => [o, {
|
||||
get() {
|
||||
this.constructor._logDataFieldMigration(o, n, {since: 12, until: 14});
|
||||
return foundry.utils.getProperty(this, n);
|
||||
},
|
||||
set(v) {
|
||||
this.constructor._logDataFieldMigration(o, n, {since: 12, until: 14});
|
||||
return foundry.utils.setProperty(this, n, v);
|
||||
},
|
||||
configurable: true
|
||||
}])));
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data) {
|
||||
/**
|
||||
* Migration to fog schema fields. Can be safely removed in V14+
|
||||
* @deprecated since v12
|
||||
*/
|
||||
for ( const [oldKey, newKey] of Object.entries({
|
||||
"fogExploration": "fog.exploration",
|
||||
"fogReset": "fog.reset",
|
||||
"fogOverlay": "fog.overlay",
|
||||
"fogExploredColor": "fog.colors.explored",
|
||||
"fogUnexploredColor": "fog.colors.unexplored"
|
||||
}) ) this._addDataFieldMigration(data, oldKey, newKey);
|
||||
|
||||
/**
|
||||
* Migration to global light embedded fields. Can be safely removed in V14+
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "globalLight", "environment.globalLight.enabled");
|
||||
this._addDataFieldMigration(data, "globalLightThreshold", "environment.globalLight.darkness.max",
|
||||
d => d.globalLightThreshold ?? 1);
|
||||
|
||||
/**
|
||||
* Migration to environment darkness level. Can be safely removed in V14+
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "darkness", "environment.darknessLevel");
|
||||
|
||||
/**
|
||||
* Migrate sourceId.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "flags.core.sourceId", "_stats.compendiumSource");
|
||||
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
/** @deprecated since v12 */
|
||||
this._addDataFieldShims(data, {
|
||||
fogExploration: "fog.exploration",
|
||||
fogReset: "fog.reset",
|
||||
fogOverlay: "fog.overlay",
|
||||
fogExploredColor: "fog.colors.explored",
|
||||
fogUnexploredColor: "fog.colors.unexplored",
|
||||
globalLight: "environment.globalLight.enabled",
|
||||
globalLightThreshold: "environment.globalLight.darkness.max",
|
||||
darkness: "environment.darknessLevel"
|
||||
}, {since: 12, until: 14});
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
}
|
||||
93
resources/app/common/documents/setting.mjs
Normal file
93
resources/app/common/documents/setting.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").SettingData} SettingData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Setting Document.
|
||||
* Defines the DataSchema and common behaviors for a Setting which are shared between both client and server.
|
||||
* @mixes SettingData
|
||||
*/
|
||||
export default class BaseSetting extends Document {
|
||||
/**
|
||||
* Construct a Setting document using provided data and context.
|
||||
* @param {Partial<SettingData>} data Initial data from which to construct the Setting
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Setting",
|
||||
collection: "settings",
|
||||
label: "DOCUMENT.Setting",
|
||||
labelPlural: "DOCUMENT.Settings",
|
||||
permissions: {
|
||||
create: this.#canModify,
|
||||
update: this.#canModify,
|
||||
delete: this.#canModify
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
key: new fields.StringField({required: true, nullable: false, blank: false,
|
||||
validate: k => k.split(".").length >= 2,
|
||||
validationError: "must have the format {scope}.{field}"}),
|
||||
value: new fields.JSONField({required: true, nullable: true, initial: null}),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The settings that only full GMs can modify.
|
||||
* @type {string[]}
|
||||
*/
|
||||
static #GAMEMASTER_ONLY_KEYS = ["core.permissions"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The settings that assistant GMs can modify regardless of their permission.
|
||||
* @type {string[]}
|
||||
*/
|
||||
static #ALLOWED_ASSISTANT_KEYS = ["core.time", "core.combatTrackerConfig", "core.sheetClasses", "core.scrollingStatusText",
|
||||
"core.tokenDragPreview", "core.adventureImports", "core.gridDiagonals", "core.gridTemplates", "core.coneTemplateType"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static canUserCreate(user) {
|
||||
return user.hasPermission("SETTINGS_MODIFY");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Define special rules which allow certain settings to be updated.
|
||||
* @protected
|
||||
*/
|
||||
static #canModify(user, doc, data) {
|
||||
if ( BaseSetting.#GAMEMASTER_ONLY_KEYS.includes(doc._source.key)
|
||||
&& (!("key" in data) || BaseSetting.#GAMEMASTER_ONLY_KEYS.includes(data.key)) ) return user.hasRole("GAMEMASTER");
|
||||
if ( user.hasPermission("SETTINGS_MODIFY") ) return true;
|
||||
if ( !user.isGM ) return false;
|
||||
return BaseSetting.#ALLOWED_ASSISTANT_KEYS.includes(doc._source.key)
|
||||
&& (!("key" in data) || BaseSetting.#ALLOWED_ASSISTANT_KEYS.includes(data.key));
|
||||
}
|
||||
}
|
||||
104
resources/app/common/documents/table-result.mjs
Normal file
104
resources/app/common/documents/table-result.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").TableResultData} TableResultData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The TableResult Document.
|
||||
* Defines the DataSchema and common behaviors for a TableResult which are shared between both client and server.
|
||||
* @mixes TableResultData
|
||||
*/
|
||||
export default class BaseTableResult extends Document {
|
||||
/**
|
||||
* Construct a TableResult document using provided data and context.
|
||||
* @param {Partial<TableResultData>} data Initial data from which to construct the TableResult
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "TableResult",
|
||||
collection: "results",
|
||||
label: "DOCUMENT.TableResult",
|
||||
labelPlural: "DOCUMENT.TableResults",
|
||||
coreTypes: Object.values(CONST.TABLE_RESULT_TYPES),
|
||||
permissions: {
|
||||
update: this.#canUpdate
|
||||
},
|
||||
compendiumIndexFields: ["type"],
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
type: new fields.DocumentTypeField(this, {initial: CONST.TABLE_RESULT_TYPES.TEXT}),
|
||||
text: new fields.HTMLField({textSearch: true}),
|
||||
img: new fields.FilePathField({categories: ["IMAGE"]}),
|
||||
documentCollection: new fields.StringField(),
|
||||
documentId: new fields.ForeignDocumentField(Document, {idOnly: true}),
|
||||
weight: new fields.NumberField({required: true, integer: true, positive: true, nullable: false, initial: 1}),
|
||||
range: new fields.ArrayField(new fields.NumberField({integer: true}), {
|
||||
validate: r => (r.length === 2) && (r[1] >= r[0]),
|
||||
validationError: "must be a length-2 array of ascending integers"
|
||||
}),
|
||||
drawn: new fields.BooleanField(),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing TableResult?
|
||||
* @private
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can do anything
|
||||
const wasDrawn = new Set(["drawn", "_id"]); // Users can update the drawn status of a result
|
||||
if ( new Set(Object.keys(data)).equals(wasDrawn) ) return true;
|
||||
return doc.parent.canUserModify(user, "update", data); // Otherwise, go by parent document permission
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
|
||||
return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data) {
|
||||
|
||||
/**
|
||||
* V12 migration of type from number to string.
|
||||
* @deprecated since v12
|
||||
*/
|
||||
if ( typeof data.type === "number" ) {
|
||||
switch ( data.type ) {
|
||||
case 0: data.type = CONST.TABLE_RESULT_TYPES.TEXT; break;
|
||||
case 1: data.type = CONST.TABLE_RESULT_TYPES.DOCUMENT; break;
|
||||
case 2: data.type = CONST.TABLE_RESULT_TYPES.COMPENDIUM; break;
|
||||
}
|
||||
}
|
||||
return super.migrateData(data);
|
||||
}
|
||||
}
|
||||
151
resources/app/common/documents/tile.mjs
Normal file
151
resources/app/common/documents/tile.mjs
Normal file
@@ -0,0 +1,151 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {getProperty, hasProperty, mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {TextureData} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").TileData} TileData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Tile Document.
|
||||
* Defines the DataSchema and common behaviors for a Tile which are shared between both client and server.
|
||||
* @mixes TileData
|
||||
*/
|
||||
export default class BaseTile extends Document {
|
||||
/**
|
||||
* Construct a Tile document using provided data and context.
|
||||
* @param {Partial<TileData>} data Initial data from which to construct the Tile
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Tile",
|
||||
collection: "tiles",
|
||||
label: "DOCUMENT.Tile",
|
||||
labelPlural: "DOCUMENT.Tiles",
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
texture: new TextureData({}, {initial: {anchorX: 0.5, anchorY: 0.5, alphaThreshold: 0.75}}),
|
||||
width: new fields.NumberField({required: true, min: 0, nullable: false, step: 0.1}),
|
||||
height: new fields.NumberField({required: true, min: 0, nullable: false, step: 0.1}),
|
||||
x: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
|
||||
y: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
sort: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
rotation: new fields.AngleField(),
|
||||
alpha: new fields.AlphaField(),
|
||||
hidden: new fields.BooleanField(),
|
||||
locked: new fields.BooleanField(),
|
||||
restrictions: new fields.SchemaField({
|
||||
light: new fields.BooleanField(),
|
||||
weather: new fields.BooleanField()
|
||||
}),
|
||||
occlusion: new fields.SchemaField({
|
||||
mode: new fields.NumberField({choices: Object.values(CONST.OCCLUSION_MODES),
|
||||
initial: CONST.OCCLUSION_MODES.NONE,
|
||||
validationError: "must be a value in CONST.TILE_OCCLUSION_MODES"}),
|
||||
alpha: new fields.AlphaField({initial: 0})
|
||||
}),
|
||||
video: new fields.SchemaField({
|
||||
loop: new fields.BooleanField({initial: true}),
|
||||
autoplay: new fields.BooleanField({initial: true}),
|
||||
volume: new fields.AlphaField({initial: 0, step: 0.01})
|
||||
}),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static migrateData(data) {
|
||||
/**
|
||||
* V12 migration to elevation and sort
|
||||
* @deprecated since v12
|
||||
*/
|
||||
this._addDataFieldMigration(data, "z", "sort");
|
||||
|
||||
/**
|
||||
* V12 migration from roof to restrictions.light and restrictions.weather
|
||||
* @deprecated since v12
|
||||
*/
|
||||
if ( foundry.utils.hasProperty(data, "roof") ) {
|
||||
const value = foundry.utils.getProperty(data, "roof");
|
||||
if ( !foundry.utils.hasProperty(data, "restrictions.light") ) foundry.utils.setProperty(data, "restrictions.light", value);
|
||||
if ( !foundry.utils.hasProperty(data, "restrictions.weather") ) foundry.utils.setProperty(data, "restrictions.weather", value);
|
||||
delete data["roof"];
|
||||
}
|
||||
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
this._addDataFieldShim(data, "z", "sort", {since: 12, until: 14});
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
set roof(enabled) {
|
||||
this.constructor._logDataFieldMigration("roof", "restrictions.{light|weather}", {since: 12, until: 14});
|
||||
this.restrictions.light = enabled;
|
||||
this.restrictions.weather = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get roof() {
|
||||
this.constructor._logDataFieldMigration("roof", "restrictions.{light|weather}", {since: 12, until: 14});
|
||||
return this.restrictions.light && this.restrictions.weather;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get z() {
|
||||
this.constructor._logDataFieldMigration("z", "sort", {since: 12, until: 14});
|
||||
return this.sort;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get overhead() {
|
||||
foundry.utils.logCompatibilityWarning(`${this.constructor.name}#overhead is deprecated.`, {since: 12, until: 14})
|
||||
return this.elevation >= this.parent?.foregroundElevation;
|
||||
}
|
||||
}
|
||||
295
resources/app/common/documents/token.mjs
Normal file
295
resources/app/common/documents/token.mjs
Normal file
@@ -0,0 +1,295 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as documents from "./_module.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import {LightData, TextureData} from "../data/data.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").TokenData} TokenData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Token Document.
|
||||
* Defines the DataSchema and common behaviors for a Token which are shared between both client and server.
|
||||
* @mixes TokenData
|
||||
*/
|
||||
export default class BaseToken extends Document {
|
||||
/**
|
||||
* Construct a Token document using provided data and context.
|
||||
* @param {Partial<TokenData>} data Initial data from which to construct the Token
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Token",
|
||||
collection: "tokens",
|
||||
label: "DOCUMENT.Token",
|
||||
labelPlural: "DOCUMENT.Tokens",
|
||||
isEmbedded: true,
|
||||
embedded: {
|
||||
ActorDelta: "delta"
|
||||
},
|
||||
permissions: {
|
||||
create: "TOKEN_CREATE",
|
||||
update: this.#canUpdate,
|
||||
delete: "TOKEN_DELETE"
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: true, textSearch: true}),
|
||||
displayName: new fields.NumberField({required: true, initial: CONST.TOKEN_DISPLAY_MODES.NONE,
|
||||
choices: Object.values(CONST.TOKEN_DISPLAY_MODES),
|
||||
validationError: "must be a value in CONST.TOKEN_DISPLAY_MODES"
|
||||
}),
|
||||
actorId: new fields.ForeignDocumentField(documents.BaseActor, {idOnly: true}),
|
||||
actorLink: new fields.BooleanField(),
|
||||
delta: new ActorDeltaField(documents.BaseActorDelta),
|
||||
appendNumber: new fields.BooleanField(),
|
||||
prependAdjective: new fields.BooleanField(),
|
||||
width: new fields.NumberField({nullable: false, positive: true, initial: 1, step: 0.5, label: "Width"}),
|
||||
height: new fields.NumberField({nullable: false, positive: true, initial: 1, step: 0.5, label: "Height"}),
|
||||
texture: new TextureData({}, {initial: {src: () => this.DEFAULT_ICON, anchorX: 0.5, anchorY: 0.5, fit: "contain",
|
||||
alphaThreshold: 0.75}, wildcard: true}),
|
||||
hexagonalShape: new fields.NumberField({initial: CONST.TOKEN_HEXAGONAL_SHAPES.ELLIPSE_1,
|
||||
choices: Object.values(CONST.TOKEN_HEXAGONAL_SHAPES)}),
|
||||
x: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
|
||||
y: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
|
||||
elevation: new fields.NumberField({required: true, nullable: false, initial: 0}),
|
||||
sort: new fields.NumberField({required: true, integer: true, nullable: false, initial: 0}),
|
||||
locked: new fields.BooleanField(),
|
||||
lockRotation: new fields.BooleanField(),
|
||||
rotation: new fields.AngleField(),
|
||||
alpha: new fields.AlphaField(),
|
||||
hidden: new fields.BooleanField(),
|
||||
disposition: new fields.NumberField({required: true, choices: Object.values(CONST.TOKEN_DISPOSITIONS),
|
||||
initial: CONST.TOKEN_DISPOSITIONS.HOSTILE,
|
||||
validationError: "must be a value in CONST.TOKEN_DISPOSITIONS"
|
||||
}),
|
||||
displayBars: new fields.NumberField({required: true, choices: Object.values(CONST.TOKEN_DISPLAY_MODES),
|
||||
initial: CONST.TOKEN_DISPLAY_MODES.NONE,
|
||||
validationError: "must be a value in CONST.TOKEN_DISPLAY_MODES"
|
||||
}),
|
||||
bar1: new fields.SchemaField({
|
||||
attribute: new fields.StringField({required: true, nullable: true, blank: false,
|
||||
initial: () => game?.system.primaryTokenAttribute || null})
|
||||
}),
|
||||
bar2: new fields.SchemaField({
|
||||
attribute: new fields.StringField({required: true, nullable: true, blank: false,
|
||||
initial: () => game?.system.secondaryTokenAttribute || null})
|
||||
}),
|
||||
light: new fields.EmbeddedDataField(LightData),
|
||||
sight: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({initial: data => Number(data?.sight?.range) > 0}),
|
||||
range: new fields.NumberField({required: true, nullable: true, min: 0, step: 0.01, initial: 0}),
|
||||
angle: new fields.AngleField({initial: 360, normalize: false}),
|
||||
visionMode: new fields.StringField({required: true, blank: false, initial: "basic",
|
||||
label: "TOKEN.VisionMode", hint: "TOKEN.VisionModeHint"}),
|
||||
color: new fields.ColorField({label: "TOKEN.VisionColor"}),
|
||||
attenuation: new fields.AlphaField({initial: 0.1, label: "TOKEN.VisionAttenuation", hint: "TOKEN.VisionAttenuationHint"}),
|
||||
brightness: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1,
|
||||
label: "TOKEN.VisionBrightness", hint: "TOKEN.VisionBrightnessHint"}),
|
||||
saturation: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1,
|
||||
label: "TOKEN.VisionSaturation", hint: "TOKEN.VisionSaturationHint"}),
|
||||
contrast: new fields.NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1,
|
||||
label: "TOKEN.VisionContrast", hint: "TOKEN.VisionContrastHint"})
|
||||
}),
|
||||
detectionModes: new fields.ArrayField(new fields.SchemaField({
|
||||
id: new fields.StringField(),
|
||||
enabled: new fields.BooleanField({initial: true}),
|
||||
range: new fields.NumberField({required: true, min: 0, step: 0.01})
|
||||
}), {
|
||||
validate: BaseToken.#validateDetectionModes
|
||||
}),
|
||||
occludable: new fields.SchemaField({
|
||||
radius: new fields.NumberField({nullable: false, min: 0, step: 0.01, initial: 0})
|
||||
}),
|
||||
ring: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField(),
|
||||
colors: new fields.SchemaField({
|
||||
ring: new fields.ColorField(),
|
||||
background: new fields.ColorField()
|
||||
}),
|
||||
effects: new fields.NumberField({initial: 1, min: 0, max: 8388607, integer: true}),
|
||||
subject: new fields.SchemaField({
|
||||
scale: new fields.NumberField({initial: 1, min: 0.5}),
|
||||
texture: new fields.FilePathField({categories: ["IMAGE"]})
|
||||
})
|
||||
}),
|
||||
/** @internal */
|
||||
_regions: new fields.ArrayField(new fields.ForeignDocumentField(documents.BaseRegion, {idOnly: true})),
|
||||
flags: new fields.ObjectField()
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["TOKEN"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate the structure of the detection modes array
|
||||
* @param {object[]} modes Configured detection modes
|
||||
* @throws An error if the array is invalid
|
||||
*/
|
||||
static #validateDetectionModes(modes) {
|
||||
const seen = new Set();
|
||||
for ( const mode of modes ) {
|
||||
if ( mode.id === "" ) continue;
|
||||
if ( seen.has(mode.id) ) {
|
||||
throw new Error(`may not have more than one configured detection mode of type "${mode.id}"`);
|
||||
}
|
||||
seen.add(mode.id);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The default icon used for newly created Token documents
|
||||
* @type {string}
|
||||
*/
|
||||
static DEFAULT_ICON = CONST.DEFAULT_TOKEN;
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing Token?
|
||||
* @private
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can do anything
|
||||
if ( doc.actor ) { // You can update Tokens for Actors you control
|
||||
return doc.actor.canUserModify(user, "update", data);
|
||||
}
|
||||
return !!doc.actorId; // It would be good to harden this in the future
|
||||
}
|
||||
|
||||
/** @override */
|
||||
testUserPermission(user, permission, {exact=false} = {}) {
|
||||
if ( this.actor ) return this.actor.testUserPermission(user, permission, {exact});
|
||||
else return super.testUserPermission(user, permission, {exact});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
updateSource(changes={}, options={}) {
|
||||
const diff = super.updateSource(changes, options);
|
||||
|
||||
// A copy of the source data is taken for the _backup in updateSource. When this backup is applied as part of a dry-
|
||||
// run, if a child singleton embedded document was updated, the reference to its source is broken. We restore it
|
||||
// here.
|
||||
if ( options.dryRun && ("delta" in changes) ) this._source.delta = this.delta._source;
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
toObject(source=true) {
|
||||
const obj = super.toObject(source);
|
||||
obj.delta = this.delta ? this.delta.toObject(source) : null;
|
||||
return obj;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Deprecations and Compatibility */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
static migrateData(data) {
|
||||
|
||||
// Remember that any migrations defined here may also be required for the PrototypeToken model.
|
||||
|
||||
/**
|
||||
* Migration of actorData field to ActorDelta document.
|
||||
* @deprecated since v11
|
||||
*/
|
||||
if ( ("actorData" in data) && !("delta" in data) ) {
|
||||
data.delta = data.actorData;
|
||||
if ( "_id" in data ) data.delta._id = data._id;
|
||||
}
|
||||
return super.migrateData(data);
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static shimData(data, options) {
|
||||
|
||||
// Remember that any shims defined here may also be required for the PrototypeToken model.
|
||||
|
||||
this._addDataFieldShim(data, "actorData", "delta", {value: data.delta, since: 11, until: 13});
|
||||
this._addDataFieldShim(data, "effects", undefined, {value: [], since: 12, until: 14,
|
||||
warning: "TokenDocument#effects is deprecated in favor of using ActiveEffect"
|
||||
+ " documents on the associated Actor"});
|
||||
this._addDataFieldShim(data, "overlayEffect", undefined, {value: "", since: 12, until: 14,
|
||||
warning: "TokenDocument#overlayEffect is deprecated in favor of using" +
|
||||
" ActiveEffect documents on the associated Actor"});
|
||||
return super.shimData(data, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get effects() {
|
||||
foundry.utils.logCompatibilityWarning("TokenDocument#effects is deprecated in favor of using ActiveEffect"
|
||||
+ " documents on the associated Actor", {since: 12, until: 14, once: true});
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v12
|
||||
* @ignore
|
||||
*/
|
||||
get overlayEffect() {
|
||||
foundry.utils.logCompatibilityWarning("TokenDocument#overlayEffect is deprecated in favor of using" +
|
||||
" ActiveEffect documents on the associated Actor", {since: 12, until: 14, once: true});
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A special subclass of EmbeddedDocumentField which allows construction of the ActorDelta to be lazily evaluated.
|
||||
*/
|
||||
export class ActorDeltaField extends fields.EmbeddedDocumentField {
|
||||
/** @inheritdoc */
|
||||
initialize(value, model, options = {}) {
|
||||
if ( !value ) return value;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(model, this.name);
|
||||
if ( (descriptor === undefined) || (!descriptor.get && !descriptor.value) ) {
|
||||
return () => {
|
||||
const m = new this.model(value, {...options, parent: model, parentCollection: this.name});
|
||||
Object.defineProperty(m, "schema", {value: this});
|
||||
Object.defineProperty(model, this.name, {
|
||||
value: m,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
return m;
|
||||
};
|
||||
}
|
||||
else if ( descriptor.get instanceof Function ) return descriptor.get;
|
||||
model[this.name]._initialize(options);
|
||||
return model[this.name];
|
||||
}
|
||||
}
|
||||
248
resources/app/common/documents/user.mjs
Normal file
248
resources/app/common/documents/user.mjs
Normal file
@@ -0,0 +1,248 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import {isEmpty, mergeObject} from "../utils/helpers.mjs";
|
||||
import {isValidId} from "../data/validators.mjs";
|
||||
import Color from "../utils/color.mjs";
|
||||
import BaseActor from "./actor.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").UserData} UserData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The User Document.
|
||||
* Defines the DataSchema and common behaviors for a User which are shared between both client and server.
|
||||
* @mixes UserData
|
||||
*/
|
||||
export default class BaseUser extends Document {
|
||||
/**
|
||||
* Construct a User document using provided data and context.
|
||||
* @param {Partial<UserData>} data Initial data from which to construct the User
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "User",
|
||||
collection: "users",
|
||||
label: "DOCUMENT.User",
|
||||
labelPlural: "DOCUMENT.Users",
|
||||
permissions: {
|
||||
create: this.#canCreate,
|
||||
update: this.#canUpdate,
|
||||
delete: this.#canDelete
|
||||
},
|
||||
schemaVersion: "12.324",
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["USER"];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
name: new fields.StringField({required: true, blank: false, textSearch: true}),
|
||||
role: new fields.NumberField({required: true, choices: Object.values(CONST.USER_ROLES),
|
||||
initial: CONST.USER_ROLES.PLAYER, readonly: true}),
|
||||
password: new fields.StringField({required: true, blank: true}),
|
||||
passwordSalt: new fields.StringField(),
|
||||
avatar: new fields.FilePathField({categories: ["IMAGE"]}),
|
||||
character: new fields.ForeignDocumentField(BaseActor),
|
||||
color: new fields.ColorField({required: true, nullable: false,
|
||||
initial: () => Color.fromHSV([Math.random(), 0.8, 0.8]).css
|
||||
}),
|
||||
pronouns: new fields.StringField({required: true}),
|
||||
hotbar: new fields.ObjectField({required: true, validate: BaseUser.#validateHotbar,
|
||||
validationError: "must be a mapping of slots to macro identifiers"}),
|
||||
permissions: new fields.ObjectField({required: true, validate: BaseUser.#validatePermissions,
|
||||
validationError: "must be a mapping of permission names to booleans"}),
|
||||
flags: new fields.ObjectField(),
|
||||
_stats: new fields.DocumentStatsField()
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate the structure of the User hotbar object
|
||||
* @param {object} bar The attempted hotbar data
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
static #validateHotbar(bar) {
|
||||
if ( typeof bar !== "object" ) return false;
|
||||
for ( let [k, v] of Object.entries(bar) ) {
|
||||
let slot = parseInt(k);
|
||||
if ( !slot || slot < 1 || slot > 50 ) return false;
|
||||
if ( !isValidId(v) ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Validate the structure of the User permissions object
|
||||
* @param {object} perms The attempted permissions data
|
||||
* @return {boolean}
|
||||
*/
|
||||
static #validatePermissions(perms) {
|
||||
for ( let [k, v] of Object.entries(perms) ) {
|
||||
if ( typeof k !== "string" ) return false;
|
||||
if ( k.startsWith("-=") ) {
|
||||
if ( v !== null ) return false;
|
||||
} else {
|
||||
if ( typeof v !== "boolean" ) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A convenience test for whether this User has the NONE role.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isBanned() {
|
||||
return this.role === CONST.USER_ROLES.NONE;
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether the User has a GAMEMASTER or ASSISTANT role in this World?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isGM() {
|
||||
return this.hasRole(CONST.USER_ROLES.ASSISTANT);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether the User is able to perform a certain permission action.
|
||||
* The provided permission string may pertain to an explicit permission setting or a named user role.
|
||||
*
|
||||
* @param {string} action The action to test
|
||||
* @return {boolean} Does the user have the ability to perform this action?
|
||||
*/
|
||||
can(action) {
|
||||
if ( action in CONST.USER_PERMISSIONS ) return this.hasPermission(action);
|
||||
return this.hasRole(action);
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
getUserLevel(user) {
|
||||
return CONST.DOCUMENT_OWNERSHIP_LEVELS[user.id === this.id ? "OWNER" : "NONE"];
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether the User has at least a specific permission
|
||||
* @param {string} permission The permission name from USER_PERMISSIONS to test
|
||||
* @return {boolean} Does the user have at least this permission
|
||||
*/
|
||||
hasPermission(permission) {
|
||||
if ( this.isBanned ) return false;
|
||||
|
||||
// CASE 1: The user has the permission set explicitly
|
||||
const explicit = this.permissions[permission];
|
||||
if (explicit !== undefined) return explicit;
|
||||
|
||||
// CASE 2: Permission defined by the user's role
|
||||
const rolePerms = game.permissions[permission];
|
||||
return rolePerms ? rolePerms.includes(this.role) : false;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test whether the User has at least the permission level of a certain role
|
||||
* @param {string|number} role The role name from USER_ROLES to test
|
||||
* @param {boolean} [exact] Require the role match to be exact
|
||||
* @return {boolean} Does the user have at this role level (or greater)?
|
||||
*/
|
||||
hasRole(role, {exact = false} = {}) {
|
||||
const level = typeof role === "string" ? CONST.USER_ROLES[role] : role;
|
||||
if (level === undefined) return false;
|
||||
return exact ? this.role === level : this.role >= level;
|
||||
}
|
||||
|
||||
/* ---------------------------------------- */
|
||||
/* Model Permissions */
|
||||
/* ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to create an existing User?
|
||||
* @param {BaseUser} user The user attempting the creation.
|
||||
* @param {BaseUser} doc The User document being created.
|
||||
* @param {object} data The supplied creation data.
|
||||
* @private
|
||||
*/
|
||||
static #canCreate(user, doc, data) {
|
||||
if ( !user.isGM ) return false; // Only Assistants and above can create users.
|
||||
// Do not allow Assistants to create a new user with special permissions which might be greater than their own.
|
||||
if ( !isEmpty(doc.permissions) ) return user.hasRole(CONST.USER_ROLES.GAMEMASTER);
|
||||
return user.hasRole(doc.role);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing User?
|
||||
* @param {BaseUser} user The user attempting the update.
|
||||
* @param {BaseUser} doc The User document being updated.
|
||||
* @param {object} changes Proposed changes.
|
||||
* @private
|
||||
*/
|
||||
static #canUpdate(user, doc, changes) {
|
||||
const roles = CONST.USER_ROLES;
|
||||
if ( user.role === roles.GAMEMASTER ) return true; // Full GMs can do everything
|
||||
if ( user.role === roles.NONE ) return false; // Banned users can do nothing
|
||||
|
||||
// Non-GMs cannot update certain fields.
|
||||
const restricted = ["permissions", "passwordSalt"];
|
||||
if ( user.role < roles.ASSISTANT ) restricted.push("name", "role");
|
||||
if ( doc.role === roles.GAMEMASTER ) restricted.push("password");
|
||||
if ( restricted.some(k => k in changes) ) return false;
|
||||
|
||||
// Role changes may not escalate
|
||||
if ( ("role" in changes) && !user.hasRole(changes.role) ) return false;
|
||||
|
||||
// Assistant GMs may modify other users. Players may only modify themselves
|
||||
return user.isGM || (user.id === doc.id);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is a user able to delete an existing User?
|
||||
* Only Assistants and Gamemasters can delete users, and only if the target user has a lesser or equal role.
|
||||
* @param {BaseUser} user The user attempting the deletion.
|
||||
* @param {BaseUser} doc The User document being deleted.
|
||||
* @private
|
||||
*/
|
||||
static #canDelete(user, doc) {
|
||||
const role = Math.max(CONST.USER_ROLES.ASSISTANT, doc.role);
|
||||
return user.hasRole(role);
|
||||
}
|
||||
}
|
||||
93
resources/app/common/documents/wall.mjs
Normal file
93
resources/app/common/documents/wall.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
import Document from "../abstract/document.mjs";
|
||||
import {mergeObject} from "../utils/helpers.mjs";
|
||||
import * as CONST from "../constants.mjs";
|
||||
import * as fields from "../data/fields.mjs";
|
||||
|
||||
/**
|
||||
* @typedef {import("./_types.mjs").WallData} WallData
|
||||
* @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Wall Document.
|
||||
* Defines the DataSchema and common behaviors for a Wall which are shared between both client and server.
|
||||
* @mixes WallData
|
||||
*/
|
||||
export default class BaseWall extends Document {
|
||||
/**
|
||||
* Construct a Wall document using provided data and context.
|
||||
* @param {Partial<WallData>} data Initial data from which to construct the Wall
|
||||
* @param {DocumentConstructionContext} context Construction context options
|
||||
*/
|
||||
constructor(data, context) {
|
||||
super(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Model Configuration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static metadata = Object.freeze(mergeObject(super.metadata, {
|
||||
name: "Wall",
|
||||
collection: "walls",
|
||||
label: "DOCUMENT.Wall",
|
||||
labelPlural: "DOCUMENT.Walls",
|
||||
permissions: {
|
||||
update: this.#canUpdate
|
||||
},
|
||||
schemaVersion: "12.324"
|
||||
}, {inplace: false}));
|
||||
|
||||
/** @inheritdoc */
|
||||
static defineSchema() {
|
||||
return {
|
||||
_id: new fields.DocumentIdField(),
|
||||
c: new fields.ArrayField(new fields.NumberField({required: true, integer: true, nullable: false}), {
|
||||
validate: c => (c.length === 4),
|
||||
validationError: "must be a length-4 array of integer coordinates"}),
|
||||
light: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_SENSE_TYPES),
|
||||
initial: CONST.WALL_SENSE_TYPES.NORMAL,
|
||||
validationError: "must be a value in CONST.WALL_SENSE_TYPES"}),
|
||||
move: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_MOVEMENT_TYPES),
|
||||
initial: CONST.WALL_MOVEMENT_TYPES.NORMAL,
|
||||
validationError: "must be a value in CONST.WALL_MOVEMENT_TYPES"}),
|
||||
sight: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_SENSE_TYPES),
|
||||
initial: CONST.WALL_SENSE_TYPES.NORMAL,
|
||||
validationError: "must be a value in CONST.WALL_SENSE_TYPES"}),
|
||||
sound: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_SENSE_TYPES),
|
||||
initial: CONST.WALL_SENSE_TYPES.NORMAL,
|
||||
validationError: "must be a value in CONST.WALL_SENSE_TYPES"}),
|
||||
dir: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_DIRECTIONS),
|
||||
initial: CONST.WALL_DIRECTIONS.BOTH,
|
||||
validationError: "must be a value in CONST.WALL_DIRECTIONS"}),
|
||||
door: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_DOOR_TYPES),
|
||||
initial: CONST.WALL_DOOR_TYPES.NONE,
|
||||
validationError: "must be a value in CONST.WALL_DOOR_TYPES"}),
|
||||
ds: new fields.NumberField({required: true, choices: Object.values(CONST.WALL_DOOR_STATES),
|
||||
initial: CONST.WALL_DOOR_STATES.CLOSED,
|
||||
validationError: "must be a value in CONST.WALL_DOOR_STATES"}),
|
||||
doorSound: new fields.StringField({required: false, blank: true, initial: undefined}),
|
||||
threshold: new fields.SchemaField({
|
||||
light: new fields.NumberField({required: true, nullable: true, initial: null, positive: true}),
|
||||
sight: new fields.NumberField({required: true, nullable: true, initial: null, positive: true}),
|
||||
sound: new fields.NumberField({required: true, nullable: true, initial: null, positive: true}),
|
||||
attenuation: new fields.BooleanField()
|
||||
}),
|
||||
flags: new fields.ObjectField()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a user able to update an existing Wall?
|
||||
* @private
|
||||
*/
|
||||
static #canUpdate(user, doc, data) {
|
||||
if ( user.isGM ) return true; // GM users can do anything
|
||||
const dsOnly = Object.keys(data).every(k => ["_id", "ds"].includes(k));
|
||||
if ( dsOnly && (doc.ds !== CONST.WALL_DOOR_STATES.LOCKED) && (data.ds !== CONST.WALL_DOOR_STATES.LOCKED) ) {
|
||||
return user.hasRole("PLAYER"); // Players may open and close unlocked doors
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user