Working on Compatibility for FVTT v10

Updated the initiative behaviour for DP
Added socket API `openDicePicker`
This commit is contained in:
Vlyan
2022-07-25 20:36:13 +02:00
parent 71eed1b26d
commit 9d33be15fb
4 changed files with 213 additions and 39 deletions

View File

@@ -3,8 +3,60 @@ Date format : day/month/year
## 1.9.0 - ??/??/2022 - Foundry v10 Compatibility ## 1.9.0 - ??/??/2022 - Foundry v10 Compatibility
- Updated the System to FoundryVTT v10. - 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. - 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 ## 1.8.2 - 24/06/2022 - Unofficial Italian translation
- Added Unofficial Italian translation (Corebook only for compendiums), all thanks to EldritchTranslator. - Added Unofficial Italian translation (Corebook only for compendiums), all thanks to EldritchTranslator.

View File

@@ -35,9 +35,13 @@ export class CombatL5r5e extends Combat {
const skillCat = CONFIG.l5r5e.skills.get(skillId); const skillCat = CONFIG.l5r5e.skills.get(skillId);
// Get score for each combatant // Get score for each combatant
const networkActors = [];
const updatedCombatants = []; const updatedCombatants = [];
for (const combatantId of ids) { for (const combatantId of ids) {
const combatant = game.combat.combatants.find((c) => c.id === combatantId); const combatant = game.combat.combatants.find((c) => c.id === combatantId);
if (!combatant || !combatant.actor || combatant.isDefeated) {
continue;
}
// Skip non character types (army) // Skip non character types (army)
if (!combatant.actor.isCharacter) { if (!combatant.actor.isCharacter) {
@@ -48,13 +52,33 @@ export class CombatL5r5e extends Combat {
continue; continue;
} }
// Skip if combatant already have an initiative value // Shortcut to system
if (!messageOptions.rerollInitiative && (!combatant || !combatant.actor)) { const actorSystem = combatant.actor.system;
return;
}
// Shortcut to data // DicePicker management
const data = combatant.actor.system; // 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 // Prepared is a boolean or if null we get the info in the actor sheet
const isPc = combatant.actor.type === "character"; 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 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. // 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 // 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 // Roll only for PC and Adversary
if (isPc || data.type === "adversary") { if (isPc || actorSystem.type === "adversary") {
// Roll formula // Roll formula
const createFormula = [];
if (!formula) { if (!formula) {
const createFormula = [`${data.rings[data.stance]}dr`]; createFormula.push(`${actorSystem.rings[actorSystem.stance]}dr`);
const skillValue = isPc ? data.skills[skillCat][skillId] : data.skills[skillCat]; const skillValue = isPc ? actorSystem.skills[skillCat][skillId] : actorSystem.skills[skillCat];
if (skillValue > 0) { if (skillValue > 0) {
createFormula.push(`${skillValue}ds`); createFormula.push(`${skillValue}ds`);
} }
formula = createFormula.join("+");
} }
let roll; let roll;
@@ -92,10 +117,10 @@ export class CombatL5r5e extends Combat {
rnkMessage = await roll.toMessage({ flavor }, { rollMode: messageOptions.rollMode || null }); rnkMessage = await roll.toMessage({ flavor }, { rollMode: messageOptions.rollMode || null });
} else { } else {
// Regular // Regular
roll = new game.l5r5e.RollL5r5e(formula); roll = new game.l5r5e.RollL5r5e(formula ?? createFormula.join("+"));
roll.actor = combatant.actor; roll.actor = combatant.actor;
roll.l5r5e.isInitiativeRoll = true; roll.l5r5e.isInitiativeRoll = true;
roll.l5r5e.stance = data.stance; roll.l5r5e.stance = actorSystem.stance;
roll.l5r5e.skillId = skillId; roll.l5r5e.skillId = skillId;
roll.l5r5e.skillCatId = skillCat; roll.l5r5e.skillCatId = skillCat;
roll.l5r5e.difficulty = 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 // Update all combatants at once
await this.updateEmbeddedDocuments("Combatant", updatedCombatants); await this.updateEmbeddedDocuments("Combatant", updatedCombatants);
return this; return this;
@@ -162,7 +201,7 @@ export class CombatL5r5e extends Combat {
* Basic weight system for sorting Character > Adversary > Minion * Basic weight system for sorting Character > Adversary > Minion
* @private * @private
*/ */
static _getWeightByActorType(data) { static _getWeightByActorType(actor) {
return data.type === "npc" ? (data.type === "minion" ? 3 : 2) : 1; return actor.type === "npc" ? (actor.type === "minion" ? 3 : 2) : 1;
} }
} }

View File

@@ -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 * Add a create macro button on top of sheet
* @override * @override
@@ -177,6 +184,7 @@ export class DicePickerDialog extends FormApplication {
*/ */
set actor(actor) { set actor(actor) {
if (!actor || !(actor instanceof Actor) || !actor.isOwner) { if (!actor || !(actor instanceof Actor) || !actor.isOwner) {
console.log("L5R5E | DP | Actor rejected", actor);
return; return;
} }
this._actor = actor; this._actor = actor;
@@ -709,7 +717,7 @@ export class DicePickerDialog extends FormApplication {
// 4: "statusRank" // 4: "statusRank"
const infos = difficulty.match(CONFIG.l5r5e.regex.techniqueDifficulty); const infos = difficulty.match(CONFIG.l5r5e.regex.techniqueDifficulty);
if (!infos) { if (!infos) {
console.log("L5R5E | Fail to parse difficulty", difficulty); console.log("L5R5E | DP | Fail to parse difficulty", difficulty);
return false; return false;
} }
@@ -730,14 +738,14 @@ export class DicePickerDialog extends FormApplication {
} }
// Wrong syntax or no target set, do manual TN // Wrong syntax or no target set, do manual TN
if (!targetActor) { 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; return false;
} }
// Check in actor.<prop> or actor.system.<prop> // Check in actor.<prop> or actor.system.<prop>
difficulty = targetActor[infos[2]] || targetActor.system[infos[2]] || null; difficulty = targetActor[infos[2]] || targetActor.system[infos[2]] || null;
if (difficulty < 1) { if (difficulty < 1) {
console.log("L5R5E | Fail to parse difficulty from target"); console.log("L5R5E | DP | Fail to parse difficulty from target");
return false; return false;
} }

View File

@@ -15,27 +15,35 @@ export class SocketHandlerL5r5e {
* registers all the socket listeners * registers all the socket listeners
*/ */
registerSocketListeners() { registerSocketListeners() {
game.socket.on(SocketHandlerL5r5e.SOCKET_NAME, (data) => { game.socket.on(SocketHandlerL5r5e.SOCKET_NAME, (payload) => {
switch (data.type) { switch (payload.type) {
case "deleteChatMessage": case "deleteChatMessage":
this._onDeleteChatMessage(data); this._onDeleteChatMessage(payload);
break; break;
case "refreshAppId": case "refreshAppId":
this._onRefreshAppId(data); this._onRefreshAppId(payload);
break; break;
case "updateMessageIdAndRefresh": case "updateMessageIdAndRefresh":
this._onUpdateMessageIdAndRefresh(data); this._onUpdateMessageIdAndRefresh(payload);
break;
case "openDicePicker":
this._onOpenDicePicker(payload);
break; break;
default: 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; break;
} }
}); });
} }
/**
* Delete ChatMessage by ID, the GM permission is required (used in RnK).
* @param {String} messageId
*/
deleteChatMessage(messageId) { deleteChatMessage(messageId) {
game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, { game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, {
type: "deleteChatMessage", type: "deleteChatMessage",
@@ -43,22 +51,21 @@ export class SocketHandlerL5r5e {
userId: game.userId, userId: game.userId,
}); });
} }
_onDeleteChatMessage(data) { _onDeleteChatMessage(payload) {
// Only delete the message if the user is a GM (otherwise it has no real effect) // Only delete the message if the user is a GM (otherwise it has no real effect)
// Currently only used in RnK // Currently only used in RnK
if (!game.user.isFirstGM || !game.settings.get("l5r5e", "rnk-deleteOldMessage")) { if (!game.user.isFirstGM || !game.settings.get("l5r5e", "rnk-deleteOldMessage")) {
return; return;
} }
const message = game.messages.get(data.messageId); game.messages.get(payload.messageId)?.delete();
if (message) {
message.delete();
}
} }
/** /**
* Refresh a app by it's htmlId, not windowsId (ex "l5r5e-twenty-questions-dialog-kZHczAFghMNYFRWe", not "65") * 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) { refreshAppId(appId) {
game.l5r5e.HelpersL5r5e.debounce(appId, () => { game.l5r5e.HelpersL5r5e.debounce(appId, () => {
@@ -68,8 +75,8 @@ export class SocketHandlerL5r5e {
}); });
})(); })();
} }
_onRefreshAppId(data) { _onRefreshAppId(payload) {
const app = Object.values(ui.windows).find((e) => e.id === data.appId); const app = Object.values(ui.windows).find((e) => e.id === payload.appId);
if (!app || typeof app.refresh !== "function") { if (!app || typeof app.refresh !== "function") {
return; return;
} }
@@ -78,8 +85,8 @@ export class SocketHandlerL5r5e {
/** /**
* Change in app message and refresh (used in RnK) * Change in app message and refresh (used in RnK)
* @param appId * @param {String} appId
* @param msgId * @param {String} msgId
*/ */
updateMessageIdAndRefresh(appId, msgId) { updateMessageIdAndRefresh(appId, msgId) {
game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, { game.socket.emit(SocketHandlerL5r5e.SOCKET_NAME, {
@@ -88,12 +95,80 @@ export class SocketHandlerL5r5e {
msgId, msgId,
}); });
} }
_onUpdateMessageIdAndRefresh(data) { _onUpdateMessageIdAndRefresh(payload) {
const app = Object.values(ui.windows).find((e) => e.id === data.appId); const app = Object.values(ui.windows).find((e) => e.id === payload.appId);
if (!app || !app.message || typeof app.refresh !== "function") { if (!app || !app.message || typeof app.refresh !== "function") {
return; return;
} }
app.message = game.messages.get(data.msgId); app.message = game.messages.get(payload.msgId);
app.refresh(); 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);
}
} }