From 9d33be15fb33440c8ee45aa08f448bf2c7fb18b3 Mon Sep 17 00:00:00 2001 From: Vlyan Date: Mon, 25 Jul 2022 20:36:13 +0200 Subject: [PATCH] Working on Compatibility for FVTT v10 Updated the initiative behaviour for DP Added socket API `openDicePicker` --- CHANGELOG.md | 54 +++++++++- system/scripts/combat.js | 69 ++++++++++--- system/scripts/dice/dice-picker-dialog.js | 14 ++- system/scripts/socket-handler.js | 115 ++++++++++++++++++---- 4 files changed, 213 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6813857..22f8cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,60 @@ Date format : day/month/year ## 1.9.0 - ??/??/2022 - Foundry v10 Compatibility - Updated the System to FoundryVTT v10. -- Removed restriction on technique types on drop (Sheet and 20Q). +- Removed restriction on technique types when dropping a technique (Sheet and 20Q). - Added a `game.user.isFirstGM` property for some traitements (socket and migration) to prevent multiple executions with multiple GM connected. +- Updated the initiative behaviour, he now open the DicePicker for connected players. +- Added socket API `openDicePicker` to remotely open the DicePicker (see usage below). + +### OpenDicePicker API usage +#### Fitness skill roll for the all combat targets +```js +game.l5r5e.sockets.openDicePicker({ + actors: Array.from(game.user.targets).map(t => t.document.actor), + dpOptions: { + skillId: 'fitness', + difficulty: 3, + } +}); +``` + +#### Initiative roll (skirmish) for all player's character who are in combat tracker +```js +game.l5r5e.sockets.openDicePicker({ + actors: game.combat.combatants.filter(c => c.hasPlayerOwner && !c.isDefeated && !c.initiative).map(c => c.actor), + dpOptions: { + skillId: 'tactics', + difficulty: 1, + isInitiativeRoll: true, + } +}); +``` + +#### Melee skill roll with "fire" ring, pre-selected for all the selected tokens +```js +game.l5r5e.sockets.openDicePicker({ + actors: canvas.tokens.controlled.map(t => t.actor), + dpOptions: { + ringId: 'fire', + skillId: 'melee', + difficulty: 2, + } +}); +``` + +#### Skill roll with skill list for all active players (but GM) +```js +game.l5r5e.sockets.openDicePicker({ + users: game.users.players.filter(u => u.active && u.hasPlayerOwner), + dpOptions: { + ringId: 'water', + skillId: 'unarmed', + skillsList: 'melee,ranged,unarmed', + difficulty: 3, + difficultyHidden: true, + } +}); +``` ## 1.8.2 - 24/06/2022 - Unofficial Italian translation - Added Unofficial Italian translation (Corebook only for compendiums), all thanks to EldritchTranslator. diff --git a/system/scripts/combat.js b/system/scripts/combat.js index 2fe2201..55cc730 100644 --- a/system/scripts/combat.js +++ b/system/scripts/combat.js @@ -35,9 +35,13 @@ export class CombatL5r5e extends Combat { const skillCat = CONFIG.l5r5e.skills.get(skillId); // Get score for each combatant + const networkActors = []; const updatedCombatants = []; for (const combatantId of ids) { const combatant = game.combat.combatants.find((c) => c.id === combatantId); + if (!combatant || !combatant.actor || combatant.isDefeated) { + continue; + } // Skip non character types (army) if (!combatant.actor.isCharacter) { @@ -48,13 +52,33 @@ export class CombatL5r5e extends Combat { continue; } - // Skip if combatant already have an initiative value - if (!messageOptions.rerollInitiative && (!combatant || !combatant.actor)) { - return; - } + // Shortcut to system + const actorSystem = combatant.actor.system; - // Shortcut to data - const data = combatant.actor.system; + // DicePicker management + // formula is empty on the fist call (combat tab buttons) + // only select if a player is active for this actor + if ( + !formula && + !combatant.initiative && + combatant.hasPlayerOwner && + combatant.players.some((u) => u.active && !u.isGM) + ) { + if (game.user.isGM) { + // Open the DP on player side + networkActors.push(combatant.actor); + } else { + // Open the DP locally + new game.l5r5e.DicePickerDialog({ + actor: combatant.actor, + skillId: skillId, + difficulty: cfg.difficulty, + difficultyHidden: cfg.difficultyHidden, + isInitiativeRoll: true, + }).render(true); + } + continue; + } // Prepared is a boolean or if null we get the info in the actor sheet const isPc = combatant.actor.type === "character"; @@ -64,18 +88,19 @@ export class CombatL5r5e extends Combat { // If the character was ready for the conflict, their base initiative value is their focus attribute. // If the character was unprepared (such as when surprised), their base initiative value is their vigilance attribute. // Minion NPCs can generate initiative value without a check, using their focus or vigilance attribute - let initiative = isPrepared === "true" ? data.focus : data.is_compromised ? 1 : data.vigilance; + let initiative = + isPrepared === "true" ? actorSystem.focus : actorSystem.is_compromised ? 1 : actorSystem.vigilance; // Roll only for PC and Adversary - if (isPc || data.type === "adversary") { + if (isPc || actorSystem.type === "adversary") { // Roll formula + const createFormula = []; if (!formula) { - const createFormula = [`${data.rings[data.stance]}dr`]; - const skillValue = isPc ? data.skills[skillCat][skillId] : data.skills[skillCat]; + createFormula.push(`${actorSystem.rings[actorSystem.stance]}dr`); + const skillValue = isPc ? actorSystem.skills[skillCat][skillId] : actorSystem.skills[skillCat]; if (skillValue > 0) { createFormula.push(`${skillValue}ds`); } - formula = createFormula.join("+"); } let roll; @@ -92,10 +117,10 @@ export class CombatL5r5e extends Combat { rnkMessage = await roll.toMessage({ flavor }, { rollMode: messageOptions.rollMode || null }); } else { // Regular - roll = new game.l5r5e.RollL5r5e(formula); + roll = new game.l5r5e.RollL5r5e(formula ?? createFormula.join("+")); roll.actor = combatant.actor; roll.l5r5e.isInitiativeRoll = true; - roll.l5r5e.stance = data.stance; + roll.l5r5e.stance = actorSystem.stance; roll.l5r5e.skillId = skillId; roll.l5r5e.skillCatId = skillCat; roll.l5r5e.difficulty = @@ -130,6 +155,20 @@ export class CombatL5r5e extends Combat { }); } + // If any network actor users to notify + if (!foundry.utils.isEmpty(networkActors)) { + console.log(networkActors); + game.l5r5e.sockets.openDicePicker({ + actors: networkActors, + dpOptions: { + skillId: skillId, + difficulty: cfg.difficulty, + difficultyHidden: cfg.difficultyHidden, + isInitiativeRoll: true, + }, + }); + } + // Update all combatants at once await this.updateEmbeddedDocuments("Combatant", updatedCombatants); return this; @@ -162,7 +201,7 @@ export class CombatL5r5e extends Combat { * Basic weight system for sorting Character > Adversary > Minion * @private */ - static _getWeightByActorType(data) { - return data.type === "npc" ? (data.type === "minion" ? 3 : 2) : 1; + static _getWeightByActorType(actor) { + return actor.type === "npc" ? (actor.type === "minion" ? 3 : 2) : 1; } } diff --git a/system/scripts/dice/dice-picker-dialog.js b/system/scripts/dice/dice-picker-dialog.js index 07c82c2..0a0aa10 100644 --- a/system/scripts/dice/dice-picker-dialog.js +++ b/system/scripts/dice/dice-picker-dialog.js @@ -65,6 +65,13 @@ export class DicePickerDialog extends FormApplication { }); } + /** + * Define a unique and dynamic element ID for the rendered application + */ + get id() { + return `l5r5e-dice-picker-dialog-${this._actor?.id ?? "no-actor"}`; + } + /** * Add a create macro button on top of sheet * @override @@ -177,6 +184,7 @@ export class DicePickerDialog extends FormApplication { */ set actor(actor) { if (!actor || !(actor instanceof Actor) || !actor.isOwner) { + console.log("L5R5E | DP | Actor rejected", actor); return; } this._actor = actor; @@ -709,7 +717,7 @@ export class DicePickerDialog extends FormApplication { // 4: "statusRank" const infos = difficulty.match(CONFIG.l5r5e.regex.techniqueDifficulty); if (!infos) { - console.log("L5R5E | Fail to parse difficulty", difficulty); + console.log("L5R5E | DP | Fail to parse difficulty", difficulty); return false; } @@ -730,14 +738,14 @@ export class DicePickerDialog extends FormApplication { } // Wrong syntax or no target set, do manual TN if (!targetActor) { - console.log("L5R5E | Fail to get actor from target selection, or no target selected"); + console.log("L5R5E | DP | Fail to get actor from target selection, or no target selected"); return false; } // Check in actor. or actor.system. difficulty = targetActor[infos[2]] || targetActor.system[infos[2]] || null; if (difficulty < 1) { - console.log("L5R5E | Fail to parse difficulty from target"); + console.log("L5R5E | DP | Fail to parse difficulty from target"); return false; } diff --git a/system/scripts/socket-handler.js b/system/scripts/socket-handler.js index e063d44..4ab16f1 100644 --- a/system/scripts/socket-handler.js +++ b/system/scripts/socket-handler.js @@ -15,27 +15,35 @@ export class SocketHandlerL5r5e { * registers all the socket listeners */ registerSocketListeners() { - game.socket.on(SocketHandlerL5r5e.SOCKET_NAME, (data) => { - switch (data.type) { + game.socket.on(SocketHandlerL5r5e.SOCKET_NAME, (payload) => { + switch (payload.type) { case "deleteChatMessage": - this._onDeleteChatMessage(data); + this._onDeleteChatMessage(payload); break; case "refreshAppId": - this._onRefreshAppId(data); + this._onRefreshAppId(payload); break; case "updateMessageIdAndRefresh": - this._onUpdateMessageIdAndRefresh(data); + this._onUpdateMessageIdAndRefresh(payload); + break; + + case "openDicePicker": + this._onOpenDicePicker(payload); break; default: - console.warn(new Error("L5R5E | This socket event is not supported"), data); + console.warn(new Error("L5R5E | This socket event is not supported"), payload); break; } }); } + /** + * Delete ChatMessage by ID, the GM permission is required (used in RnK). + * @param {String} messageId + */ deleteChatMessage(messageId) { game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, { type: "deleteChatMessage", @@ -43,22 +51,21 @@ export class SocketHandlerL5r5e { userId: game.userId, }); } - _onDeleteChatMessage(data) { + _onDeleteChatMessage(payload) { // Only delete the message if the user is a GM (otherwise it has no real effect) // Currently only used in RnK if (!game.user.isFirstGM || !game.settings.get("l5r5e", "rnk-deleteOldMessage")) { return; } - const message = game.messages.get(data.messageId); - if (message) { - message.delete(); - } + game.messages.get(payload.messageId)?.delete(); } /** * Refresh a app by it's htmlId, not windowsId (ex "l5r5e-twenty-questions-dialog-kZHczAFghMNYFRWe", not "65") - * usage : game.l5r5e.sockets.refreshAppId(appId); - * @param appId + * + * Usage : game.l5r5e.sockets.refreshAppId(appId); + * + * @param {String} appId */ refreshAppId(appId) { game.l5r5e.HelpersL5r5e.debounce(appId, () => { @@ -68,8 +75,8 @@ export class SocketHandlerL5r5e { }); })(); } - _onRefreshAppId(data) { - const app = Object.values(ui.windows).find((e) => e.id === data.appId); + _onRefreshAppId(payload) { + const app = Object.values(ui.windows).find((e) => e.id === payload.appId); if (!app || typeof app.refresh !== "function") { return; } @@ -78,8 +85,8 @@ export class SocketHandlerL5r5e { /** * Change in app message and refresh (used in RnK) - * @param appId - * @param msgId + * @param {String} appId + * @param {String} msgId */ updateMessageIdAndRefresh(appId, msgId) { game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, { @@ -88,12 +95,80 @@ export class SocketHandlerL5r5e { msgId, }); } - _onUpdateMessageIdAndRefresh(data) { - const app = Object.values(ui.windows).find((e) => e.id === data.appId); + _onUpdateMessageIdAndRefresh(payload) { + const app = Object.values(ui.windows).find((e) => e.id === payload.appId); if (!app || !app.message || typeof app.refresh !== "function") { return; } - app.message = game.messages.get(data.msgId); + app.message = game.messages.get(payload.msgId); app.refresh(); } + + /** + * Remotely open the DicePicker + * + * Usage : game.l5r5e.sockets.openDicePicker({ + * users: game.users.players.filter(u => u.active && u.hasPlayerOwner), + * dpOptions: { + * ringId: 'water', + * skillId: 'unarmed', + * skillList: 'melee,range,unarmed', + * difficulty: 3, + * difficultyHidden: true, + * } + * }); + * + * @param {User[]} users Users list to trigger the DP (will be reduce to id for network perf.) + * @param {Actor[]} actors Actors list to trigger the DP (will be reduce to uuid for network perf.) + * @param {Object} dpOptions Any DicePickerDialog.options + */ + openDicePicker({ users = [], actors = [], dpOptions = {} }) { + // At least one user or one actor + if (foundry.utils.isEmpty(users) && foundry.utils.isEmpty(actors)) { + console.error("L5R5E | openDicePicker - 'users' and 'actors' are both empty, use at least one."); + return; + } + // Fail if dpOptions.actor* provided + if (!foundry.utils.isEmpty(dpOptions?.actorName)) { + console.error("L5R5E | openDicePicker - Do not use 'dpOptions.actorName', use 'actors' list instead."); + return; + } + if (!foundry.utils.isEmpty(dpOptions?.actorId)) { + console.error("L5R5E | openDicePicker - Do not use 'dpOptions.actorId', use 'actors' list instead."); + return; + } + if (!foundry.utils.isEmpty(dpOptions?.actor)) { + console.error("L5R5E | openDicePicker - Do not use 'dpOptions.actor', use 'actors' list instead."); + return; + } + + game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, { + type: "openDicePicker", + users: users?.map((u) => u.id), + actors: actors?.map((a) => a.uuid), + dpOptions, + }); + } + _onOpenDicePicker(payload) { + if (!foundry.utils.isEmpty(payload.users) && !payload.users.includes(game.user.id)) { + return; + } + + // Actors + if (!foundry.utils.isEmpty(payload.actors)) { + payload.actors.forEach((uuid) => { + const actor = fromUuidSync(uuid); + if (actor && actor.testUserPermission(game.user, "OWNER")) { + new game.l5r5e.DicePickerDialog({ + ...payload.dpOptions, + actor: actor, + }).render(true); + } + }); + return; + } + + // User Only : Let the DP select the actor + new game.l5r5e.DicePickerDialog(payload.dpOptions).render(true); + } }