790 lines
30 KiB
JavaScript
790 lines
30 KiB
JavaScript
/**
|
|
* 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
|
|
});
|
|
}
|
|
}
|