Implement support for a queried group check as a prerequisite for soc…#699
Implement support for a queried group check as a prerequisite for soc…#699Plushtoast wants to merge 1 commit intofoundryvtt:masterfrom
Conversation
…ial and exploration challenge play Fixes foundryvtt#97
aaclayton
left a comment
There was a problem hiding this comment.
I still need to finish reviewing the group-check.mjs file, but I have to go AFK now for a couple hours so I'm posting the comments that I have so far and I'll finish the review later.
| @@ -359,40 +368,8 @@ export default class StandardCheckDialog extends DialogV2 { | |||
| * @this {StandardCheckDialog} | |||
| */ | |||
| static async #requestSubmit(_event, _target) { | |||
There was a problem hiding this comment.
Maybe this should become a GroupCheck instance method somehow? Is this.roll a GroupCheck instance in this case? If so it could be:
| static async #requestSubmit(_event, _target) { | |
| static async #requestSubmit(_event, _target) { | |
| await this.roll.requestSubmit(); | |
| await this.close(); | |
| } |
| * @property {string} actorImg Image path (cached) | ||
| * @property {string|null} userId User ID responsible for rolling | ||
| * @property {string} status Actor status enum value | ||
| * @property {object|null} rollData Serialized StandardCheck data (after completion) |
There was a problem hiding this comment.
Would this be StandardCheckData?
| */ | ||
|
|
||
| /** | ||
| * @typedef {object} GroupCheckActorResult |
There was a problem hiding this comment.
I wonder if this type can be generalized as something like a StandardCheckResult. The data structure here looks like it could apply to regular checks also
| activeUsers ??= game.users.filter(u => u.active && !u.isSelf); | ||
| let user = activeUsers.find(u => u.character === actor); | ||
| user ||= activeUsers.find(u => actor.testUserPermission(u, "OWNER")); | ||
| return user; |
There was a problem hiding this comment.
- I think we should do this with fewer rounds of iteration
- We shouldn't necessarily rule out
user.isSelf - We should prefer non-GM owners to GM-owners
- Prefer explicit
nulltoundefined
Suggestion:
const users = [];
for ( const user of game.users ) {
if ( !user.active ) continue;
if ( user.character === actor ) users.push({user, priority: 1});
else if ( user.isGM ) users.push({user, priority: 3});
else if ( actor.testUserPermission(user, "OWNER") ) users.push({user, priority: 2});
}
users.sort((a, b) => a.priority - b.priority);
return users[0] ?? null;| * @param {CrucibleActor[]} options.requestedActors The actors to include in the group check | ||
| * @returns {Promise<void>} | ||
| */ | ||
| static async requestSubmit({ roll, requestedActors }) { |
There was a problem hiding this comment.
Could this be reworked as an instance method where we construct the GroupCheck first and then from the constructed instance we submit it?
| const isComplete = entry.status === ACTOR_STATUS.COMPLETE; | ||
| const isPending = entry.status === ACTOR_STATUS.PENDING; | ||
| const isAborted = entry.status === ACTOR_STATUS.ABORTED; | ||
| const isSkipped = entry.status === ACTOR_STATUS.SKIPPED; |
There was a problem hiding this comment.
Instead of preparing these booleans and doing logic in the template, it would be better to prepare some data for status icon in javascript and render that icon directly in the template.
| if (entry.result.isCriticalSuccess || entry.result.isCriticalFailure) cssClasses.push("critical"); | ||
| cssClasses.push(entry.result.isSuccess ? "success" : "failure"); | ||
| } | ||
| if (isAborted) cssClasses.push("aborted"); |
There was a problem hiding this comment.
Instead of lots of conditional logic throughout this method, I think we should use a switch statement on entry.status since the statuses are mutually exclusive. Within each case of the switch statement we can modify the template data accordingly.
| * @param {string} params.title - The dialog title | ||
| * @returns {Promise<{rollData: object, result: GroupCheckActorResult}|{aborted: boolean}>} | ||
| */ | ||
| static async handle({checkId, actorId, skillId, dc, sharedBoons, sharedBanes, title}={}) { |
There was a problem hiding this comment.
We could consider whether the receiving client needs to be told the dc of the check, or whether that piece of data could be reserved for only the requesting client (and the persisted chat message) to know.
This means we might not need the handling client to prepare the result data structure, the result could be determined only by the requesting client. All that the handling client needs to provide back is the result of their roll.
| const btn = foundry.utils.parseHTML( | ||
| `<button type="button" class="icon frame-brown fa-solid ${icon}" data-tooltip="${game.i18n.localize(tooltipKey)}"></button>` | ||
| ); |
There was a problem hiding this comment.
Prefer constructing the HTML element natively rather than parsing html:
| const btn = foundry.utils.parseHTML( | |
| `<button type="button" class="icon frame-brown fa-solid ${icon}" data-tooltip="${game.i18n.localize(tooltipKey)}"></button>` | |
| ); | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.className = `frame-brown icon fa-solid ${icon}`; | |
| btn.dataset.tooltip = game.i18n.localize(tooltipKey); |
| btn.addEventListener("click", async event => { | ||
| event.preventDefault(); | ||
| await handler(); | ||
| }); |
There was a problem hiding this comment.
Not sure there is any default click behavior that needs to be prevented here, so we can probably just bind the handler directly:
| btn.addEventListener("click", async event => { | |
| event.preventDefault(); | |
| await handler(); | |
| }); | |
| btn.addEventListener("click", handler); |
| if ( section ) { | ||
| const wrapper = document.createElement("div"); | ||
| wrapper.classList.add("flexrow"); | ||
| const finalize = foundry.utils.parseHTML(`<button class="confirm frame-brown" type="button"><i class="fa-solid fa-check"></i> ${game.i18n.localize("DICE.GROUP_CHECK.Finalize")}</button>`); |
There was a problem hiding this comment.
Perhaps you can use makeButton here too
| if ( !flags ) return; | ||
| const entry = flags.actors[actorId]; | ||
| if ( !entry ) return; | ||
|
|
| if (!flags) return; | ||
| flags.finalized = true; | ||
| const content = await this.#renderGroupCheckCard(flags); | ||
| await message.update({ content, whisper: [], [`flags.crucible.${FLAG_KEY}`]: flags }); |
There was a problem hiding this comment.
| await message.update({ content, whisper: [], [`flags.crucible.${FLAG_KEY}`]: flags }); | |
| await message.update({content, whisper: [], [`flags.crucible.${FLAG_KEY}`]: flags}); |
|
ok good. very thorough review and good suggestion. before i proceed: do you want to have the interaction via a chatcard or should i bringt that back into the dialog. since i expect players to be slow in rolling for many reasons and potentially future skills could allow a reroll on a failed check or sth, i would think so that the chatcard is the better long term solution and has the better ux but up to your decision next:
|
I'm happy with the chat card proposal.
Yes, but let's keep that out of scope for this PR. I'd like to address the feedback here and get it merged and then we can take a next step to determine the aggregate group success or failure.
We will need to make sure this integrates in with the actor hooks system for skill checks, but this should happen automatically if you are careful to go through
Can be out of scope for this PR. |
Ok here is a revised pull request for the group check feature.
I extracted the whole group check logic into its own class, so it is easier to review.
I mainly removed:
What is still there that is not api is the rendering of the corresponding chat card. I felt like I need that for better testing.
In the corresponding ticket you proposed to keep an dialog open which is updating once results come in.
Instead i chose a chat card which is fire and forget (dialogs not in the way) and has a persistent storage for the data.
It seems to be also a more common pattern among systems to control this via chat buttons
Currently the request is still the same like before from within the StandardCheckDialog.
On Request it creates this in the chat:
All actor owners receive a roll request dialog.
Actors without an owner have to be rolled by the gm
After each roll the card is updated
The buttons next to the actor provide these functions.
If you still want me to remove the chat implementation, I can do that as well. But i think from here on the PR is less confusing.
Fixes #97