Working on Compatibility for FVTT v10
Updated the initiative behaviour for DP Added socket API `openDicePicker`
This commit is contained in:
54
CHANGELOG.md
54
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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user