This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View 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";

View 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
*/

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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"];
}

View 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"];
}

View 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});
}
}

View 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);
}
}

View 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;
}
}

View 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.");
}
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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});
}
}

View 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;
}
}

View 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});
}
}

View 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});
}
}

View 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);
}
}

View 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;
}
}

View 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()
}
};
}

View 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);
}
}

View 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);
}
}

View 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));
}
}

View 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);
}
}

View 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;
}
}

View 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];
}
}

View 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);
}
}

View 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;
}
}