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
- 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.

View File

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

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
* @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.<prop> or actor.system.<prop>
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;
}

View File

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