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
|
## 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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user