Initial
This commit is contained in:
789
resources/app/client/data/documents/cards.js
Normal file
789
resources/app/client/data/documents/cards.js
Normal file
@@ -0,0 +1,789 @@
|
||||
/**
|
||||
* The client-side Cards document which extends the common BaseCards model.
|
||||
* Each Cards document contains CardsData which defines its data schema.
|
||||
* @extends foundry.documents.BaseCards
|
||||
* @mixes ClientDocumentMixin
|
||||
*
|
||||
* @see {@link CardStacks} The world-level collection of Cards documents
|
||||
* @see {@link CardsConfig} The Cards configuration application
|
||||
*/
|
||||
class Cards extends ClientDocumentMixin(foundry.documents.BaseCards) {
|
||||
|
||||
/**
|
||||
* Provide a thumbnail image path used to represent this document.
|
||||
* @type {string}
|
||||
*/
|
||||
get thumbnail() {
|
||||
return this.img;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Card documents within this stack which are available to be drawn.
|
||||
* @type {Card[]}
|
||||
*/
|
||||
get availableCards() {
|
||||
return this.cards.filter(c => (this.type !== "deck") || !c.drawn);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Card documents which belong to this stack but have already been drawn.
|
||||
* @type {Card[]}
|
||||
*/
|
||||
get drawnCards() {
|
||||
return this.cards.filter(c => c.drawn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized Label for the type of Card Stack this is
|
||||
* @type {string}
|
||||
*/
|
||||
get typeLabel() {
|
||||
switch ( this.type ) {
|
||||
case "deck": return game.i18n.localize("CARDS.TypeDeck");
|
||||
case "hand": return game.i18n.localize("CARDS.TypeHand");
|
||||
case "pile": return game.i18n.localize("CARDS.TypePile");
|
||||
default: throw new Error(`Unexpected type ${this.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this Cards document be cloned in a duplicate workflow?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canClone() {
|
||||
if ( this.type === "deck" ) return true;
|
||||
else return this.cards.size === 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* API Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
static async createDocuments(data=[], context={}) {
|
||||
if ( context.keepEmbeddedIds === undefined ) context.keepEmbeddedIds = false;
|
||||
return super.createDocuments(data, context);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Deal one or more cards from this Cards document to each of a provided array of Cards destinations.
|
||||
* Cards are allocated from the top of the deck in cyclical order until the required number of Cards have been dealt.
|
||||
* @param {Cards[]} to An array of other Cards documents to which cards are dealt
|
||||
* @param {number} [number=1] The number of cards to deal to each other document
|
||||
* @param {object} [options={}] Options which modify how the deal operation is performed
|
||||
* @param {number} [options.how=0] How to draw, a value from CONST.CARD_DRAW_MODES
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the deal operation,
|
||||
* for example the displayed face
|
||||
* @param {string} [options.action=deal] The name of the action being performed, used as part of the dispatched
|
||||
* Hook event
|
||||
* @param {boolean} [options.chatNotification=true] Create a ChatMessage which notifies that this action has occurred
|
||||
* @returns {Promise<Cards>} This Cards document after the deal operation has completed
|
||||
*/
|
||||
async deal(to, number=1, {action="deal", how=0, updateData={}, chatNotification=true}={}) {
|
||||
|
||||
// Validate the request
|
||||
if ( !to.every(d => d instanceof Cards) ) {
|
||||
throw new Error("You must provide an array of Cards documents as the destinations for the Cards#deal operation");
|
||||
}
|
||||
|
||||
// Draw from the sorted stack
|
||||
const total = number * to.length;
|
||||
const drawn = this._drawCards(total, how);
|
||||
|
||||
// Allocate cards to each destination
|
||||
const toCreate = to.map(() => []);
|
||||
const toUpdate = [];
|
||||
const toDelete = [];
|
||||
for ( let i=0; i<total; i++ ) {
|
||||
const n = i % to.length;
|
||||
const card = drawn[i];
|
||||
const createData = foundry.utils.mergeObject(card.toObject(), updateData);
|
||||
if ( card.isHome || !createData.origin ) createData.origin = this.id;
|
||||
createData.drawn = true;
|
||||
toCreate[n].push(createData);
|
||||
if ( card.isHome ) toUpdate.push({_id: card.id, drawn: true});
|
||||
else toDelete.push(card.id);
|
||||
}
|
||||
|
||||
const allowed = Hooks.call("dealCards", this, to, {
|
||||
action: action,
|
||||
toCreate: toCreate,
|
||||
fromUpdate: toUpdate,
|
||||
fromDelete: toDelete
|
||||
});
|
||||
if ( allowed === false ) {
|
||||
console.debug(`${vtt} | The Cards#deal operation was prevented by a hooked function`);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Perform database operations
|
||||
const promises = to.map((cards, i) => {
|
||||
return cards.createEmbeddedDocuments("Card", toCreate[i], {keepId: true});
|
||||
});
|
||||
promises.push(this.updateEmbeddedDocuments("Card", toUpdate));
|
||||
promises.push(this.deleteEmbeddedDocuments("Card", toDelete));
|
||||
await Promise.all(promises);
|
||||
|
||||
// Dispatch chat notification
|
||||
if ( chatNotification ) {
|
||||
const chatActions = {
|
||||
deal: "CARDS.NotifyDeal",
|
||||
pass: "CARDS.NotifyPass"
|
||||
};
|
||||
this._postChatNotification(this, chatActions[action], {number, link: to.map(t => t.link).join(", ")});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Pass an array of specific Card documents from this document to some other Cards stack.
|
||||
* @param {Cards} to Some other Cards document that is the destination for the pass operation
|
||||
* @param {string[]} ids The embedded Card ids which should be passed
|
||||
* @param {object} [options={}] Additional options which modify the pass operation
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the pass operation,
|
||||
* for example the displayed face
|
||||
* @param {string} [options.action=pass] The name of the action being performed, used as part of the dispatched
|
||||
* Hook event
|
||||
* @param {boolean} [options.chatNotification=true] Create a ChatMessage which notifies that this action has occurred
|
||||
* @returns {Promise<Card[]>} An array of the Card embedded documents created within the destination stack
|
||||
*/
|
||||
async pass(to, ids, {updateData={}, action="pass", chatNotification=true}={}) {
|
||||
if ( !(to instanceof Cards) ) {
|
||||
throw new Error("You must provide a Cards document as the recipient for the Cards#pass operation");
|
||||
}
|
||||
|
||||
// Allocate cards to different required operations
|
||||
const toCreate = [];
|
||||
const toUpdate = [];
|
||||
const fromUpdate = [];
|
||||
const fromDelete = [];
|
||||
|
||||
// Validate the provided cards
|
||||
for ( let id of ids ) {
|
||||
const card = this.cards.get(id, {strict: true});
|
||||
const deletedFromOrigin = card.origin && !card.origin.cards.get(id);
|
||||
|
||||
// Prevent drawing cards from decks multiple times
|
||||
if ( (this.type === "deck") && card.isHome && card.drawn ) {
|
||||
throw new Error(`You may not pass Card ${id} which has already been drawn`);
|
||||
}
|
||||
|
||||
// Return drawn cards to their origin deck
|
||||
if ( (card.origin === to) && !deletedFromOrigin ) {
|
||||
toUpdate.push({_id: card.id, drawn: false});
|
||||
}
|
||||
|
||||
// Create cards in a new destination
|
||||
else {
|
||||
const createData = foundry.utils.mergeObject(card.toObject(), updateData);
|
||||
const copyCard = (card.isHome && (to.type === "deck"));
|
||||
if ( copyCard ) createData.origin = to.id;
|
||||
else if ( card.isHome || !createData.origin ) createData.origin = this.id;
|
||||
createData.drawn = !copyCard && !deletedFromOrigin;
|
||||
toCreate.push(createData);
|
||||
}
|
||||
|
||||
// Update cards in their home deck
|
||||
if ( card.isHome && (to.type !== "deck") ) fromUpdate.push({_id: card.id, drawn: true});
|
||||
|
||||
// Remove cards from their current stack
|
||||
else if ( !card.isHome ) fromDelete.push(card.id);
|
||||
}
|
||||
|
||||
const allowed = Hooks.call("passCards", this, to, {action, toCreate, toUpdate, fromUpdate, fromDelete});
|
||||
if ( allowed === false ) {
|
||||
console.debug(`${vtt} | The Cards#pass operation was prevented by a hooked function`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Perform database operations
|
||||
const created = to.createEmbeddedDocuments("Card", toCreate, {keepId: true});
|
||||
await Promise.all([
|
||||
created,
|
||||
to.updateEmbeddedDocuments("Card", toUpdate),
|
||||
this.updateEmbeddedDocuments("Card", fromUpdate),
|
||||
this.deleteEmbeddedDocuments("Card", fromDelete)
|
||||
]);
|
||||
|
||||
// Dispatch chat notification
|
||||
if ( chatNotification ) {
|
||||
const chatActions = {
|
||||
pass: "CARDS.NotifyPass",
|
||||
play: "CARDS.NotifyPlay",
|
||||
discard: "CARDS.NotifyDiscard",
|
||||
draw: "CARDS.NotifyDraw"
|
||||
};
|
||||
const chatFrom = action === "draw" ? to : this;
|
||||
const chatTo = action === "draw" ? this : to;
|
||||
this._postChatNotification(chatFrom, chatActions[action], {number: ids.length, link: chatTo.link});
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Draw one or more cards from some other Cards document.
|
||||
* @param {Cards} from Some other Cards document from which to draw
|
||||
* @param {number} [number=1] The number of cards to draw
|
||||
* @param {object} [options={}] Options which modify how the draw operation is performed
|
||||
* @param {number} [options.how=0] How to draw, a value from CONST.CARD_DRAW_MODES
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the draw operation,
|
||||
* for example the displayed face
|
||||
* @returns {Promise<Card[]>} An array of the Card documents which were drawn
|
||||
*/
|
||||
async draw(from, number=1, {how=0, updateData={}, ...options}={}) {
|
||||
if ( !(from instanceof Cards) || (from === this) ) {
|
||||
throw new Error("You must provide some other Cards document as the source for the Cards#draw operation");
|
||||
}
|
||||
const toDraw = from._drawCards(number, how);
|
||||
return from.pass(this, toDraw.map(c => c.id), {updateData, action: "draw", ...options});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Shuffle this Cards stack, randomizing the sort order of all the cards it contains.
|
||||
* @param {object} [options={}] Options which modify how the shuffle operation is performed.
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the shuffle operation,
|
||||
* for example the displayed face.
|
||||
* @param {boolean} [options.chatNotification=true] Create a ChatMessage which notifies that this action has occurred
|
||||
* @returns {Promise<Cards>} The Cards document after the shuffle operation has completed
|
||||
*/
|
||||
async shuffle({updateData={}, chatNotification=true}={}) {
|
||||
const order = this.cards.map(c => [foundry.dice.MersenneTwister.random(), c]);
|
||||
order.sort((a, b) => a[0] - b[0]);
|
||||
const toUpdate = order.map((x, i) => {
|
||||
const card = x[1];
|
||||
return foundry.utils.mergeObject({_id: card.id, sort: i}, updateData);
|
||||
});
|
||||
|
||||
// Post a chat notification and return
|
||||
await this.updateEmbeddedDocuments("Card", toUpdate);
|
||||
if ( chatNotification ) {
|
||||
this._postChatNotification(this, "CARDS.NotifyShuffle", {link: this.link});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Recall the Cards stack, retrieving all original cards from other stacks where they may have been drawn if this is a
|
||||
* deck, otherwise returning all the cards in this stack to the decks where they originated.
|
||||
* @param {object} [options={}] Options which modify the recall operation
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the recall operation,
|
||||
* for example the displayed face
|
||||
* @param {boolean} [options.chatNotification=true] Create a ChatMessage which notifies that this action has occurred
|
||||
* @returns {Promise<Cards>} The Cards document after the recall operation has completed.
|
||||
*/
|
||||
async recall(options) {
|
||||
if ( this.type === "deck" ) return this._resetDeck(options);
|
||||
return this._resetStack(options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Perform a reset operation for a deck, retrieving all original cards from other stacks where they may have been
|
||||
* drawn.
|
||||
* @param {object} [options={}] Options which modify the reset operation.
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the reset operation
|
||||
* @param {boolean} [options.chatNotification=true] Create a ChatMessage which notifies that this action has occurred
|
||||
* @returns {Promise<Cards>} The Cards document after the reset operation has completed.
|
||||
* @private
|
||||
*/
|
||||
async _resetDeck({updateData={}, chatNotification=true}={}) {
|
||||
|
||||
// Recover all cards which belong to this stack
|
||||
for ( let cards of game.cards ) {
|
||||
if ( cards === this ) continue;
|
||||
const toDelete = [];
|
||||
for ( let c of cards.cards ) {
|
||||
if ( c.origin === this ) {
|
||||
toDelete.push(c.id);
|
||||
}
|
||||
}
|
||||
if ( toDelete.length ) await cards.deleteEmbeddedDocuments("Card", toDelete);
|
||||
}
|
||||
|
||||
// Mark all cards as not drawn
|
||||
const cards = this.cards.contents;
|
||||
cards.sort(this.sortStandard.bind(this));
|
||||
const toUpdate = cards.map(card => {
|
||||
return foundry.utils.mergeObject({_id: card.id, drawn: false}, updateData);
|
||||
});
|
||||
|
||||
// Post a chat notification and return
|
||||
await this.updateEmbeddedDocuments("Card", toUpdate);
|
||||
if ( chatNotification ) {
|
||||
this._postChatNotification(this, "CARDS.NotifyReset", {link: this.link});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return all cards in this stack to their original decks.
|
||||
* @param {object} [options={}] Options which modify the return operation.
|
||||
* @param {object} [options.updateData={}] Modifications to make to each Card as part of the return operation
|
||||
* @param {boolean} [options.chatNotification=true] Create a ChatMessage which notifies that this action has occurred
|
||||
* @returns {Promise<Cards>} The Cards document after the return operation has completed.
|
||||
* @private
|
||||
*/
|
||||
async _resetStack({updateData={}, chatNotification=true}={}) {
|
||||
|
||||
// Allocate cards to different required operations.
|
||||
const toUpdate = {};
|
||||
const fromDelete = [];
|
||||
for ( const card of this.cards ) {
|
||||
if ( card.isHome || !card.origin ) continue;
|
||||
|
||||
// Return drawn cards to their origin deck
|
||||
if ( card.origin.cards.get(card.id) ) {
|
||||
if ( !toUpdate[card.origin.id] ) toUpdate[card.origin.id] = [];
|
||||
const update = foundry.utils.mergeObject(updateData, {_id: card.id, drawn: false}, {inplace: false});
|
||||
toUpdate[card.origin.id].push(update);
|
||||
}
|
||||
|
||||
// Remove cards from the current stack.
|
||||
fromDelete.push(card.id);
|
||||
}
|
||||
|
||||
const allowed = Hooks.call("returnCards", this, fromDelete.map(id => this.cards.get(id)), {toUpdate, fromDelete});
|
||||
if ( allowed === false ) {
|
||||
console.debug(`${vtt} | The Cards#return operation was prevented by a hooked function.`);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Perform database operations.
|
||||
const updates = Object.entries(toUpdate).map(([origin, u]) => {
|
||||
return game.cards.get(origin).updateEmbeddedDocuments("Card", u);
|
||||
});
|
||||
await Promise.all([...updates, this.deleteEmbeddedDocuments("Card", fromDelete)]);
|
||||
|
||||
// Dispatch chat notification
|
||||
if ( chatNotification ) this._postChatNotification(this, "CARDS.NotifyReturn", {link: this.link});
|
||||
return this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A sorting function that is used to determine the standard order of Card documents within an un-shuffled stack.
|
||||
* Sorting with "en" locale to ensure the same order regardless of which client sorts the deck.
|
||||
* @param {Card} a The card being sorted
|
||||
* @param {Card} b Another card being sorted against
|
||||
* @returns {number}
|
||||
* @protected
|
||||
*/
|
||||
sortStandard(a, b) {
|
||||
if ( (a.suit ?? "") === (b.suit ?? "") ) return ((a.value ?? -Infinity) - (b.value ?? -Infinity)) || 0;
|
||||
return (a.suit ?? "").compare(b.suit ?? "");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A sorting function that is used to determine the order of Card documents within a shuffled stack.
|
||||
* @param {Card} a The card being sorted
|
||||
* @param {Card} b Another card being sorted against
|
||||
* @returns {number}
|
||||
* @protected
|
||||
*/
|
||||
sortShuffled(a, b) {
|
||||
return a.sort - b.sort;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An internal helper method for drawing a certain number of Card documents from this Cards stack.
|
||||
* @param {number} number The number of cards to draw
|
||||
* @param {number} how A draw mode from CONST.CARD_DRAW_MODES
|
||||
* @returns {Card[]} An array of drawn Card documents
|
||||
* @protected
|
||||
*/
|
||||
_drawCards(number, how) {
|
||||
|
||||
// Confirm that sufficient cards are available
|
||||
let available = this.availableCards;
|
||||
if ( available.length < number ) {
|
||||
throw new Error(`There are not ${number} available cards remaining in Cards [${this.id}]`);
|
||||
}
|
||||
|
||||
// Draw from the stack
|
||||
let drawn;
|
||||
switch ( how ) {
|
||||
case CONST.CARD_DRAW_MODES.FIRST:
|
||||
available.sort(this.sortShuffled.bind(this));
|
||||
drawn = available.slice(0, number);
|
||||
break;
|
||||
case CONST.CARD_DRAW_MODES.LAST:
|
||||
available.sort(this.sortShuffled.bind(this));
|
||||
drawn = available.slice(-number);
|
||||
break;
|
||||
case CONST.CARD_DRAW_MODES.RANDOM:
|
||||
const shuffle = available.map(c => [Math.random(), c]);
|
||||
shuffle.sort((a, b) => a[0] - b[0]);
|
||||
drawn = shuffle.slice(-number).map(x => x[1]);
|
||||
break;
|
||||
}
|
||||
return drawn;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a ChatMessage which provides a notification of the operation which was just performed.
|
||||
* Visibility of the resulting message is linked to the default roll mode selected in the chat log dropdown.
|
||||
* @param {Cards} source The source Cards document from which the action originated
|
||||
* @param {string} action The localization key which formats the chat message notification
|
||||
* @param {object} context Data passed to the Localization#format method for the localization key
|
||||
* @returns {ChatMessage} A created ChatMessage document
|
||||
* @private
|
||||
*/
|
||||
_postChatNotification(source, action, context) {
|
||||
const messageData = {
|
||||
style: CONST.CHAT_MESSAGE_STYLES.OTHER,
|
||||
speaker: {user: game.user},
|
||||
content: `
|
||||
<div class="cards-notification flexrow">
|
||||
<img class="icon" src="${source.thumbnail}" alt="${source.name}">
|
||||
<p>${game.i18n.format(action, context)}</p>
|
||||
</div>`
|
||||
};
|
||||
ChatMessage.applyRollMode(messageData, game.settings.get("core", "rollMode"));
|
||||
return ChatMessage.implementation.create(messageData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preCreate(data, options, user) {
|
||||
const allowed = await super._preCreate(data, options, user);
|
||||
if ( allowed === false ) return false;
|
||||
for ( const card of this.cards ) {
|
||||
card.updateSource({drawn: false});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
_onUpdate(changed, options, userId) {
|
||||
if ( "type" in changed ) {
|
||||
this.sheet?.close();
|
||||
this._sheet = undefined;
|
||||
}
|
||||
super._onUpdate(changed, options, userId);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritDoc */
|
||||
async _preDelete(options, user) {
|
||||
await this.recall();
|
||||
return super._preDelete(options, user);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Interaction Dialogs */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display a dialog which prompts the user to deal cards to some number of hand-type Cards documents.
|
||||
* @see {@link Cards#deal}
|
||||
* @returns {Promise<Cards|null>}
|
||||
*/
|
||||
async dealDialog() {
|
||||
const hands = game.cards.filter(c => (c.type !== "deck") && c.testUserPermission(game.user, "LIMITED"));
|
||||
if ( !hands.length ) {
|
||||
ui.notifications.warn("CARDS.DealWarnNoTargets", {localize: true});
|
||||
return this;
|
||||
}
|
||||
|
||||
// Construct the dialog HTML
|
||||
const html = await renderTemplate("templates/cards/dialog-deal.html", {
|
||||
hands: hands,
|
||||
modes: {
|
||||
[CONST.CARD_DRAW_MODES.TOP]: "CARDS.DrawModeTop",
|
||||
[CONST.CARD_DRAW_MODES.BOTTOM]: "CARDS.DrawModeBottom",
|
||||
[CONST.CARD_DRAW_MODES.RANDOM]: "CARDS.DrawModeRandom"
|
||||
}
|
||||
});
|
||||
|
||||
// Display the prompt
|
||||
return Dialog.prompt({
|
||||
title: game.i18n.localize("CARDS.DealTitle"),
|
||||
label: game.i18n.localize("CARDS.Deal"),
|
||||
content: html,
|
||||
callback: html => {
|
||||
const form = html.querySelector("form.cards-dialog");
|
||||
const fd = new FormDataExtended(form).object;
|
||||
if ( !fd.to ) return this;
|
||||
const toIds = fd.to instanceof Array ? fd.to : [fd.to];
|
||||
const to = toIds.reduce((arr, id) => {
|
||||
const c = game.cards.get(id);
|
||||
if ( c ) arr.push(c);
|
||||
return arr;
|
||||
}, []);
|
||||
const options = {how: fd.how, updateData: fd.down ? {face: null} : {}};
|
||||
return this.deal(to, fd.number, options).catch(err => {
|
||||
ui.notifications.error(err.message);
|
||||
return this;
|
||||
});
|
||||
},
|
||||
rejectClose: false,
|
||||
options: {jQuery: false}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display a dialog which prompts the user to draw cards from some other deck-type Cards documents.
|
||||
* @see {@link Cards#draw}
|
||||
* @returns {Promise<Card[]|null>}
|
||||
*/
|
||||
async drawDialog() {
|
||||
const decks = game.cards.filter(c => (c.type === "deck") && c.testUserPermission(game.user, "LIMITED"));
|
||||
if ( !decks.length ) {
|
||||
ui.notifications.warn("CARDS.DrawWarnNoSources", {localize: true});
|
||||
return [];
|
||||
}
|
||||
|
||||
// Construct the dialog HTML
|
||||
const html = await renderTemplate("templates/cards/dialog-draw.html", {
|
||||
decks: decks,
|
||||
modes: {
|
||||
[CONST.CARD_DRAW_MODES.TOP]: "CARDS.DrawModeTop",
|
||||
[CONST.CARD_DRAW_MODES.BOTTOM]: "CARDS.DrawModeBottom",
|
||||
[CONST.CARD_DRAW_MODES.RANDOM]: "CARDS.DrawModeRandom"
|
||||
}
|
||||
});
|
||||
|
||||
// Display the prompt
|
||||
return Dialog.prompt({
|
||||
title: game.i18n.localize("CARDS.DrawTitle"),
|
||||
label: game.i18n.localize("CARDS.Draw"),
|
||||
content: html,
|
||||
callback: html => {
|
||||
const form = html.querySelector("form.cards-dialog");
|
||||
const fd = new FormDataExtended(form).object;
|
||||
const from = game.cards.get(fd.from);
|
||||
const options = {how: fd.how, updateData: fd.down ? {face: null} : {}};
|
||||
return this.draw(from, fd.number, options).catch(err => {
|
||||
ui.notifications.error(err.message);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
rejectClose: false,
|
||||
options: {jQuery: false}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display a dialog which prompts the user to pass cards from this document to some other Cards document.
|
||||
* @see {@link Cards#deal}
|
||||
* @returns {Promise<Cards|null>}
|
||||
*/
|
||||
async passDialog() {
|
||||
const cards = game.cards.filter(c => (c !== this) && (c.type !== "deck") && c.testUserPermission(game.user, "LIMITED"));
|
||||
if ( !cards.length ) {
|
||||
ui.notifications.warn("CARDS.PassWarnNoTargets", {localize: true});
|
||||
return this;
|
||||
}
|
||||
|
||||
// Construct the dialog HTML
|
||||
const html = await renderTemplate("templates/cards/dialog-pass.html", {
|
||||
cards: cards,
|
||||
modes: {
|
||||
[CONST.CARD_DRAW_MODES.TOP]: "CARDS.DrawModeTop",
|
||||
[CONST.CARD_DRAW_MODES.BOTTOM]: "CARDS.DrawModeBottom",
|
||||
[CONST.CARD_DRAW_MODES.RANDOM]: "CARDS.DrawModeRandom"
|
||||
}
|
||||
});
|
||||
|
||||
// Display the prompt
|
||||
return Dialog.prompt({
|
||||
title: game.i18n.localize("CARDS.PassTitle"),
|
||||
label: game.i18n.localize("CARDS.Pass"),
|
||||
content: html,
|
||||
callback: html => {
|
||||
const form = html.querySelector("form.cards-dialog");
|
||||
const fd = new FormDataExtended(form).object;
|
||||
const to = game.cards.get(fd.to);
|
||||
const options = {action: "pass", how: fd.how, updateData: fd.down ? {face: null} : {}};
|
||||
return this.deal([to], fd.number, options).catch(err => {
|
||||
ui.notifications.error(err.message);
|
||||
return this;
|
||||
});
|
||||
},
|
||||
rejectClose: false,
|
||||
options: {jQuery: false}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display a dialog which prompts the user to play a specific Card to some other Cards document
|
||||
* @see {@link Cards#pass}
|
||||
* @param {Card} card The specific card being played as part of this dialog
|
||||
* @returns {Promise<Card[]|null>}
|
||||
*/
|
||||
async playDialog(card) {
|
||||
const cards = game.cards.filter(c => (c !== this) && (c.type !== "deck") && c.testUserPermission(game.user, "LIMITED"));
|
||||
if ( !cards.length ) {
|
||||
ui.notifications.warn("CARDS.PassWarnNoTargets", {localize: true});
|
||||
return [];
|
||||
}
|
||||
|
||||
// Construct the dialog HTML
|
||||
const html = await renderTemplate("templates/cards/dialog-play.html", {card, cards});
|
||||
|
||||
// Display the prompt
|
||||
return Dialog.prompt({
|
||||
title: game.i18n.localize("CARD.Play"),
|
||||
label: game.i18n.localize("CARD.Play"),
|
||||
content: html,
|
||||
callback: html => {
|
||||
const form = html.querySelector("form.cards-dialog");
|
||||
const fd = new FormDataExtended(form).object;
|
||||
const to = game.cards.get(fd.to);
|
||||
const options = {action: "play", updateData: fd.down ? {face: null} : {}};
|
||||
return this.pass(to, [card.id], options).catch(err => {
|
||||
ui.notifications.error(err.message);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
rejectClose: false,
|
||||
options: {jQuery: false}
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display a confirmation dialog for whether or not the user wishes to reset a Cards stack
|
||||
* @see {@link Cards#recall}
|
||||
* @returns {Promise<Cards|false|null>}
|
||||
*/
|
||||
async resetDialog() {
|
||||
return Dialog.confirm({
|
||||
title: game.i18n.localize("CARDS.Reset"),
|
||||
content: `<p>${game.i18n.format(`CARDS.${this.type === "deck" ? "Reset" : "Return"}Confirm`, {name: this.name})}</p>`,
|
||||
yes: () => this.recall()
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
async deleteDialog(options={}) {
|
||||
if ( !this.drawnCards.length ) return super.deleteDialog(options);
|
||||
const type = this.typeLabel;
|
||||
return new Promise(resolve => {
|
||||
const dialog = new Dialog({
|
||||
title: `${game.i18n.format("DOCUMENT.Delete", {type})}: ${this.name}`,
|
||||
content: `
|
||||
<h4>${game.i18n.localize("CARDS.DeleteCannot")}</h4>
|
||||
<p>${game.i18n.format("CARDS.DeleteMustReset", {type})}</p>
|
||||
`,
|
||||
buttons: {
|
||||
reset: {
|
||||
icon: '<i class="fas fa-undo"></i>',
|
||||
label: game.i18n.localize("CARDS.DeleteReset"),
|
||||
callback: () => resolve(this.delete())
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: () => resolve(false)
|
||||
}
|
||||
},
|
||||
close: () => resolve(null),
|
||||
default: "reset"
|
||||
}, options);
|
||||
dialog.render(true);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async createDialog(data={}, {parent=null, pack=null, types, ...options}={}) {
|
||||
if ( types ) {
|
||||
if ( types.length === 0 ) throw new Error("The array of sub-types to restrict to must not be empty");
|
||||
for ( const type of types ) {
|
||||
if ( !this.TYPES.includes(type) ) throw new Error(`Invalid ${this.documentName} sub-type: "${type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect data
|
||||
const documentTypes = this.TYPES.filter(t => types?.includes(t) !== false);
|
||||
let collection;
|
||||
if ( !parent ) {
|
||||
if ( pack ) collection = game.packs.get(pack);
|
||||
else collection = game.collections.get(this.documentName);
|
||||
}
|
||||
const folders = collection?._formatFolderSelectOptions() ?? [];
|
||||
const label = game.i18n.localize(this.metadata.label);
|
||||
const title = game.i18n.format("DOCUMENT.Create", {type: label});
|
||||
const type = data.type || documentTypes[0];
|
||||
|
||||
// Render the document creation form
|
||||
const html = await renderTemplate("templates/sidebar/cards-create.html", {
|
||||
folders,
|
||||
name: data.name || "",
|
||||
defaultName: this.implementation.defaultName({type, parent, pack}),
|
||||
folder: data.folder,
|
||||
hasFolders: folders.length >= 1,
|
||||
type,
|
||||
types: Object.fromEntries(documentTypes.map(type => {
|
||||
const label = CONFIG[this.documentName]?.typeLabels?.[type];
|
||||
return [type, label && game.i18n.has(label) ? game.i18n.localize(label) : type];
|
||||
}).sort((a, b) => a[1].localeCompare(b[1], game.i18n.lang))),
|
||||
hasTypes: true,
|
||||
presets: CONFIG.Cards.presets
|
||||
});
|
||||
|
||||
// Render the confirmation dialog window
|
||||
return Dialog.prompt({
|
||||
title: title,
|
||||
content: html,
|
||||
label: title,
|
||||
render: html => {
|
||||
html[0].querySelector('[name="type"]').addEventListener("change", e => {
|
||||
html[0].querySelector('[name="name"]').placeholder = this.implementation.defaultName(
|
||||
{type: e.target.value, parent, pack});
|
||||
});
|
||||
},
|
||||
callback: async html => {
|
||||
const form = html[0].querySelector("form");
|
||||
const fd = new FormDataExtended(form);
|
||||
foundry.utils.mergeObject(data, fd.object, {inplace: true});
|
||||
if ( !data.folder ) delete data.folder;
|
||||
if ( !data.name?.trim() ) data.name = this.implementation.defaultName({type: data.type, parent, pack});
|
||||
const preset = CONFIG.Cards.presets[data.preset];
|
||||
if ( preset && (preset.type === data.type) ) {
|
||||
const presetData = await fetch(preset.src).then(r => r.json());
|
||||
data = foundry.utils.mergeObject(presetData, data);
|
||||
}
|
||||
return this.implementation.create(data, {parent, pack, renderSheet: true});
|
||||
},
|
||||
rejectClose: false,
|
||||
options
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user