diff --git a/css/style.min.css b/css/style.min.css index f2853193..e72ab15b 100644 --- a/css/style.min.css +++ b/css/style.min.css @@ -14007,7 +14007,8 @@ template { z-index: 4; width: 100%; position: absolute; - pointer-events: none; } + pointer-events: none; + margin-top: 20px; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-effects-block:not(.inactive-mod-block) { flex-direction: column; width: unset; @@ -14023,12 +14024,34 @@ template { width: 100%; border-bottom-left-radius: 30px; border-bottom-right-radius: 30px; + overflow: hidden; + position: relative; } + :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-button .roll-odds-strip .roll-odds-section-container { + height: 500%; + width: 100%; + position: absolute; + z-index: 5; + display: flex; + justify-content: stretch; + align-items: stretch; + flex-wrap: nowrap; + filter: blur(50px); } + :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-button .roll-odds-strip .roll-odds-section-container > * { + flex-grow: 1; + flex-shrink: 1; } + :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-button .roll-odds-label-container { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; text-align: center; line-height: 28px; color: var(--blades-gold-dark); font-family: var(--font-emphasis); font-size: 18px; - text-shadow: 1.5px 1.5px 0px var(--blades-black-dark); } + text-shadow: 1.5px 1.5px 0px var(--blades-black-dark); + z-index: 5; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-button .roll-button { display: block; padding: 0; @@ -14048,7 +14071,8 @@ template { transform-origin: 50% 50%; transition: 0.25s; filter: drop-shadow(3px 3px 5px var(--blades-black)); - opacity: 1; } + opacity: 1; + z-index: 5; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab .window-content form .sheet-root section.sheet-footer .roll-sheet-float-block.roll-button .roll-button:hover { scale: 1.2; color: var(--blades-gold-bright); diff --git a/module/BladesRollCollab.js b/module/BladesRollCollab.js index 106b5502..8b0ae52f 100644 --- a/module/BladesRollCollab.js +++ b/module/BladesRollCollab.js @@ -405,50 +405,20 @@ class BladesRollMod { }); } get tooltip() { - if (this.sideString) { - return this._tooltip - .replace(/%COLON%/g, ":") - .replace(/%DOC_NAME%/g, this.sideString); - } - return this._tooltip.replace(/%COLON%/g, ":"); + return this._tooltip.replace(/%COLON%/g, ":") + .replace(/%DOC_NAME%/g, this.sideString ?? "an Ally") + .replace(/@OPPOSITION_NAME@/g, this.rollInstance.rollOpposition?.rollOppName ?? "Your Opposition"); } get sideString() { if (this._sideString) { return this._sideString; } - switch (this.category) { - case RollModSection.roll: { - if (this.name === "Assist") { - const docID = this.rollInstance.document.getFlag("eunos-blades", "rollCollab.docSelections.roll.Assist"); - if (!docID) { - return undefined; - } - return (game.actors.get(docID) ?? game.items.get(docID))?.name ?? undefined; - } - return undefined; - } - case RollModSection.position: { - if (this.name === "Setup") { - const docID = this.rollInstance.document.getFlag("eunos-blades", "rollCollab.docSelections.position.Setup"); - if (!docID) { - return undefined; - } - return (game.actors.get(docID) ?? game.items.get(docID))?.name ?? undefined; - } - return undefined; - } - case RollModSection.effect: { - if (this.name === "Setup") { - const docID = this.rollInstance.document.getFlag("eunos-blades", "rollCollab.docSelections.effect.Setup"); - if (!docID) { - return undefined; - } - return (game.actors.get(docID) ?? game.items.get(docID))?.name ?? undefined; - } - return undefined; - } - default: return undefined; + const rollParticipantCategoryData = this.rollInstance.rollParticipants?.[this.category]; + if (rollParticipantCategoryData && this.name in rollParticipantCategoryData) { + const rollParticipant = rollParticipantCategoryData[this.name]; + return rollParticipant.rollParticipantName; } + return undefined; } get allFlagData() { return this.rollInstance.document.getFlag("eunos-blades", "rollCollab"); @@ -592,7 +562,10 @@ class BladesRollPrimary { this.rollPrimaryType = this.rollPrimaryDoc.rollPrimaryType; this.rollPrimaryImg = rollPrimaryImg ?? this.rollPrimaryDoc.rollPrimaryImg ?? ""; this._rollModsData = rollModsData ?? []; - this.rollFactors = Object.assign(this.rollPrimaryDoc.rollFactors, rollFactors ?? {}); + this.rollFactors = { + ...this.rollPrimaryDoc.rollFactors, + ...rollFactors ?? {} + }; } else { if (!rollPrimaryName) { @@ -1106,6 +1079,7 @@ class BladesRollCollab extends DocumentSheet { await rollInst._render(true); } static RenderRollCollab(rollID) { + BladesRollCollab.Current[rollID]?.prepareRollParticipantData(); BladesRollCollab.Current[rollID]?.render(); } static async CloseRollCollab(rollID) { @@ -1358,7 +1332,6 @@ class BladesRollCollab extends DocumentSheet { } async addRollParticipant(participant) { await participant.updateRollFlags(); - this.prepareRollParticipantData(); socketlib.system.executeForEveryone("renderRollCollab", this.rollID); } get rollType() { return this.flagData.rollType; } @@ -1490,126 +1463,85 @@ class BladesRollCollab extends DocumentSheet { return this._roll; } get rollFactors() { - const sourceFactors = Object.fromEntries(Object.entries(this.rollPrimary.rollFactors) - .map(([factor, factorData]) => [ - factor, - { - ...factorData, - ...this.flagData.rollFactorToggles.source[factor] ?? [] - } - ])); - Object.entries(this.flagData.rollFactorToggles.source).forEach(([factor, factorData]) => { - if (!(factor in sourceFactors)) { - sourceFactors[factor] = { - name: factor, - value: 0, - max: 0, - baseVal: 0, - cssClasses: "factor-gold", - isActive: factorData.isActive ?? false, - isPrimary: factorData.isPrimary ?? (factor === Factor.tier), - isDominant: factorData.isDominant ?? false, - highFavorsPC: factorData.highFavorsPC ?? true - }; - } - }); - Object.keys(sourceFactors) - .filter(isFactor) - .forEach(factor => { - const factorData = sourceFactors[factor]; - if (!factorData) { - return; - } - factorData.value ??= 0; - factorData.value += - (this.flagData.GMBoosts[factor] ?? 0) - + (this.tempGMBoosts[factor] ?? 0); - }); - Object.keys(sourceFactors) - .filter(isFactor) - .forEach(factor => { - const factorData = sourceFactors[factor]; - if (!factorData) { - return; - } - factorData.value ??= 0; - factorData.value += this.flagData.GMOppBoosts[factor] ?? 0; - if (factor === Factor.tier) { - factorData.display = U.romanizeNum(factorData.value); - } - else { - factorData.display = `${factorData.value}`; - } - }); - const rollOppFactors = this.rollOpposition?.rollFactors - ?? Object.fromEntries(([ - Factor.tier, - Factor.quality, - Factor.scale, - Factor.magnitude - ]).map(factor => [ - factor, - { - name: factor, - value: 0, - max: 0, - baseVal: 0, - cssClasses: "factor-gold", - isActive: false, - isPrimary: factor === Factor.tier, - isDominant: false, - highFavorsPC: true - } - ])); - const oppFactors = {}; - Object.entries(rollOppFactors) - .forEach(([factor, factorData]) => { - if (!isFactor(factor)) { - return; - } - oppFactors[factor] = { - ...factorData, - ...this.flagData.rollFactorToggles.opposition[factor] ?? [] - }; - }); - Object.entries(this.flagData.rollFactorToggles.opposition) - .forEach(([factor, factorData]) => { - if (!isFactor(factor)) { - return; - } - if (!(factor in oppFactors)) { - oppFactors[factor] = { - name: factor, - value: 0, - max: 0, - baseVal: 0, - cssClasses: "factor-gold", - isActive: factorData.isActive ?? false, - isPrimary: factorData.isPrimary ?? (factor === Factor.tier), - isDominant: factorData.isDominant ?? false, - highFavorsPC: factorData.highFavorsPC ?? true - }; - } - }); - Object.keys(oppFactors).forEach(factor => { - if (!isFactor(factor)) { - return; - } - const factorData = oppFactors[factor]; - if (!factorData) { - return; - } - factorData.value += this.flagData.GMOppBoosts[factor] ?? 0; - if (factor === Factor.tier) { - factorData.display = U.romanizeNum(factorData.value); - } - else { - factorData.display = `${factorData.value}`; + const defaultFactors = { + [Factor.tier]: { + name: "Tier", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: true, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" + }, + [Factor.quality]: { + name: "Quality", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: false, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" + }, + [Factor.scale]: { + name: "Scale", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: false, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" + }, + [Factor.magnitude]: { + name: "Magnitude", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: false, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" } - }); + }; + const mergedSourceFactors = U.objMerge(U.objMerge(defaultFactors, this.rollPrimary.rollFactors, { isMutatingOk: false }), this.flagData.rollFactorToggles.source, { isMutatingOk: false }); + const mergedOppFactors = this.rollOpposition + ? U.objMerge(U.objMerge(defaultFactors, this.rollPrimary.rollFactors, { isMutatingOk: false }), this.flagData.rollFactorToggles.opposition, { isMutatingOk: false }) + : {}; return { - source: sourceFactors, - opposition: oppFactors + source: Object.fromEntries(Object.entries(mergedSourceFactors) + .map(([factor, factorData]) => { + factorData.value += + (this.flagData.GMBoosts[factor] ?? 0) + + (this.tempGMBoosts[factor] ?? 0); + if (factor === Factor.tier) { + factorData.display = U.romanizeNum(factorData.value); + } + else { + factorData.display = `${factorData.value}`; + } + return [factor, factorData]; + })), + opposition: Object.fromEntries(Object.entries(mergedOppFactors) + .map(([factor, factorData]) => { + factorData.value += this.flagData.GMOppBoosts[factor] ?? 0; + if (factor === Factor.tier) { + factorData.display = U.romanizeNum(factorData.value); + } + else { + factorData.display = `${factorData.value}`; + } + return [factor, factorData]; + })) }; } initRollMods(modsData) { @@ -1896,6 +1828,7 @@ class BladesRollCollab extends DocumentSheet { rollTraitOptions, diceTotal: finalDicePool, rollOpposition: this.rollOpposition, + rollParticipants: this.rollParticipants, rollEffects: Object.values(Effect), teamworkDocs: game.actors.filter(actor => BladesActor.IsType(actor, BladesActorType.pc)), rollTraitValOverride: this.rollTraitValOverride, @@ -1906,7 +1839,7 @@ class BladesRollCollab extends DocumentSheet { .map(cat => [cat, this.getRollMods(cat, "negative")])), hasInactiveConditionals: this.calculateHasInactiveConditionalsData(), rollFactors, - oddsGradient: this.calculateOddsGradient(finalDicePool, finalResult), + ...this.calculateOddsHTML(finalDicePool, finalResult), costData: this.parseCostsHTML(this.getStressCosts(rollCosts), this.getSpecArmorCost(rollCosts)) }; const rollPositionData = this.calculatePositionData(finalPosition); @@ -2001,6 +1934,43 @@ class BladesRollCollab extends DocumentSheet { `${oddsColors.success} ${gradientStops.success}%`, `${oddsColors.crit})` ].join(", "); + } + calculateOddsHTML(diceTotal, finalResult) { + const oddsColors = { + crit: "var(--blades-gold)", + success: "var(--blades-white-bright)", + partial: "var(--blades-grey)", + fail: "var(--blades-black-dark)" + }; + const odds = { ...C.DiceOdds[diceTotal] }; + if (finalResult < 0) { + for (let i = finalResult; i < 0; i++) { + oddsColors.crit = oddsColors.success; + oddsColors.success = oddsColors.partial; + oddsColors.partial = oddsColors.fail; + } + } + else if (finalResult > 0) { + for (let i = 0; i < finalResult; i++) { + oddsColors.fail = oddsColors.partial; + oddsColors.partial = oddsColors.success; + oddsColors.success = oddsColors.crit; + } + } + const resultElements = []; + Object.entries(odds).reverse().forEach(([result, chance]) => { + if (chance === 0) { + return; + } + resultElements.push(`
 
`); + }); + return { + oddsHTMLStart: [ + "
", + ...resultElements + ].join("\n"), + oddsHTMLStop: "
" + }; } calculatePositionEffectTradeData() { const canTradePosition = this.posEffectTrade === "position" || (this.posEffectTrade === false @@ -2150,7 +2120,6 @@ class BladesRollCollab extends DocumentSheet { await this.outputRollToChat(); this.close(); } - _toggleRollModClick(event) { event.preventDefault(); const elem$ = $(event.currentTarget); @@ -2180,37 +2149,7 @@ class BladesRollCollab extends DocumentSheet { default: throw new Error(`Unrecognized RollModStatus: ${rollMod.status}`); } } - _toggleRollModContext(event) { - event.preventDefault(); - if (!game.user.isGM) { - return; - } - const elem$ = $(event.currentTarget); - const id = elem$.data("id"); - const rollMod = this.getRollModByID(id); - if (!rollMod) { - throw new Error(`Unable to find roll mod with id '${id}'`); - } - switch (rollMod.status) { - case RollModStatus.Hidden: - rollMod.userStatus = RollModStatus.ToggledOff; - return; - case RollModStatus.ForcedOff: - rollMod.userStatus = RollModStatus.Hidden; - return; - case RollModStatus.ToggledOff: - rollMod.userStatus = RollModStatus.ForcedOff; - return; - case RollModStatus.ToggledOn: - rollMod.userStatus = RollModStatus.ToggledOff; - return; - case RollModStatus.ForcedOn: - rollMod.userStatus = RollModStatus.Hidden; - return; - default: throw new Error(`Unrecognized RollModStatus: ${rollMod.status}`); - } - } - _gmControlSet(event) { + _gmControlSet(event) { event.preventDefault(); if (!game.user.isGM) { return; @@ -2218,15 +2157,15 @@ class BladesRollCollab extends DocumentSheet { const elem$ = $(event.currentTarget); const id = elem$.data("id"); const status = elem$.data("status"); - if (!isModStatus(status)) { + if (!isModStatus(status) && status !== "Reset") { return; } const rollMod = this.getRollModByID(id); if (rollMod) { - rollMod.userStatus = status; + rollMod.userStatus = status === "Reset" ? undefined : status; } } - async _gmControlSetTargetToValue(event) { + async _gmControlSetTargetToValue(event) { event.preventDefault(); if (!game.user.isGM) { return; @@ -2236,7 +2175,7 @@ class BladesRollCollab extends DocumentSheet { const value = elem$.data("value"); await this.document.setFlag(C.SYSTEM_ID, target, value).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - async _gmControlResetTarget(event) { + async _gmControlResetTarget(event) { event.preventDefault(); if (!game.user.isGM) { return; @@ -2245,19 +2184,7 @@ class BladesRollCollab extends DocumentSheet { const target = elem$.data("target").replace(/flags\.eunos-blades\./, ""); await this.document.unsetFlag(C.SYSTEM_ID, target).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - _gmControlReset(event) { - event.preventDefault(); - if (!game.user.isGM) { - return; - } - const elem$ = $(event.currentTarget); - const id = elem$.data("id"); - const rollMod = this.getRollModByID(id); - if (rollMod) { - rollMod.userStatus = undefined; - } - } - _gmControlSetPosition(event) { + _gmControlSetPosition(event) { event.preventDefault(); if (!game.user.isGM) { return; @@ -2266,7 +2193,7 @@ class BladesRollCollab extends DocumentSheet { const position = elem$.data("status"); this.initialPosition = position; } - _gmControlSetEffect(event) { + _gmControlSetEffect(event) { event.preventDefault(); if (!game.user.isGM) { return; @@ -2275,7 +2202,7 @@ class BladesRollCollab extends DocumentSheet { const effect = elem$.data("status"); this.initialEffect = effect; } - async _gmControlToggleFactor(event) { + async _gmControlToggleFactor(event) { event.preventDefault(); if (!game.user.isGM) { return; @@ -2284,33 +2211,65 @@ class BladesRollCollab extends DocumentSheet { const target = elem$.data("target"); const value = !elem$.data("value"); eLog.checkLog3("toggleFactor", "_gmControlToggleFactor", { event, target, value }); - if (value && /isPrimary/.test(target)) { - const [thisSource, thisFactor] = target.split(/\./).slice(-3, -1); - eLog.checkLog3("toggleFactor", "_gmControlToggleFactor - IN", { thisSource, thisFactor }); - await Promise.all(Object.values(Factor).map(factor => { - if (factor === thisFactor) { - eLog.checkLog3("toggleFactor", `_gmControlToggleFactor - Checking ${factor} === ${thisFactor} === TRUE`, { factor, thisFactor, target, customTarget: `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary` }); - return this.document.setFlag(C.SYSTEM_ID, `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary`, true).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + const factorToggleData = this.document.getFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles"); + const [thisSource, thisFactor, thisToggle] = target.split(/\./).slice(-3); + if (!["isActive", "isPrimary", "isDominant", "highFavorsPC"].includes(thisToggle)) { + return this.document.setFlag(C.SYSTEM_ID, `rollCollab.${target}`, value) + .then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + } + factorToggleData[thisSource][thisFactor] = { + ...factorToggleData[thisSource][thisFactor] ?? { display: "" }, + [thisToggle]: value + }; + switch (thisToggle) { + case "isDominant": + case "isPrimary": { + if (value === true) { + Object.values(Factor) + .filter(factor => factor !== thisFactor) + .forEach(factor => { + if (factorToggleData[thisSource][factor]?.[thisToggle] === true) { + factorToggleData[thisSource][factor] = { + ...factorToggleData[thisSource][factor], + [thisToggle]: false + }; + } + }); } - else { - eLog.checkLog3("toggleFactor", `_gmControlToggleFactor - Checking ${factor} === ${thisFactor} === FALSE`, { factor, thisFactor, target, customTarget: `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary` }); - return this.document.setFlag(C.SYSTEM_ID, `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary`, false).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + break; + } + case "isActive": { + if (value === true) { + const otherSource = thisSource === "source" ? "opposition" : "source"; + factorToggleData[otherSource][thisFactor] = { + ...factorToggleData[otherSource][thisFactor] ?? { display: "" }, + isActive: value + }; } - })); - eLog.checkLog3("toggleFactor", "_gmControlToggleFactor - ALL DONE", { flags: this.document.getFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles") }); - } - else { - this.document.setFlag(C.SYSTEM_ID, `rollCollab.${target}`, value).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + break; + } + default: break; } + return this.document.setFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles", factorToggleData) + .then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - async _gmControlResetFactor(event) { + async _gmControlSelectDocument(event) { event.preventDefault(); - if (!game.user.isGM) { + const elem$ = $(event.currentTarget); + const section = elem$.data("rollSection"); + const subSection = elem$.data("rollSubSection"); + const selectedOption = elem$.val(); + if (typeof selectedOption !== "string") { return; } - const elem$ = $(event.currentTarget); - const target = elem$.data("target"); - await this.document.unsetFlag(C.SYSTEM_ID, `rollCollab.${target}`).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + if (selectedOption === "false") { + return this.document.unsetFlag(C.SYSTEM_ID, `rollCollab.rollParticipantData.${section}.${subSection}`); + } + return this.addRollParticipant(new BladesRollParticipant(this, { + rollParticipantSection: section, + rollParticipantSubSection: subSection, + rollParticipantID: selectedOption + })); } get resistanceStressCost() { const dieVals = this.dieVals; @@ -2360,36 +2319,34 @@ class BladesRollCollab extends DocumentSheet { html.on({ focusin: () => { BladesRollCollab.Active = this; } }); - html.find("[data-action='gm-set'").on({ - click: this._gmControlSet.bind(this) + html.find(".controls-toggle").on({ + click: event => { + event.preventDefault(); + $(event.currentTarget).parents(".controls-panel").toggleClass("active"); + } }); - html.find("[data-action='gm-reset'").on({ - click: this._gmControlReset.bind(this) + html.find("[data-action=\"gm-set\"").on({ + click: this._gmControlSet.bind(this) }); - html.find("[data-action='gm-set-position'").on({ + html.find("[data-action=\"gm-set-position\"").on({ click: this._gmControlSetPosition.bind(this) }); - html.find("[data-action='gm-set-effect'").on({ + html.find("[data-action=\"gm-set-effect\"").on({ click: this._gmControlSetEffect.bind(this) }); - html.find("[data-action='gm-set-target'").on({ + html.find("[data-action=\"gm-set-target\"").on({ click: this._gmControlSetTargetToValue.bind(this), contextmenu: this._gmControlResetTarget.bind(this) }); - html.find("[data-action='gm-toggle-factor'").on({ - click: this._gmControlToggleFactor.bind(this), - contextmenu: this._gmControlResetFactor.bind(this) + html.find("[data-action=\"gm-toggle-factor\"").on({ + click: this._gmControlToggleFactor.bind(this) }); - html.find(".controls-toggle").on({ - click: event => { - event.preventDefault(); - $(event.currentTarget).parents(".controls-panel").toggleClass("active"); - } + html.find("select.roll-sheet-doc-select").on({ + change: this._gmControlSelectDocument.bind(this) }); } - _canDragDrop(selector) { - eLog.checkLog3("canDragDrop", "Can DragDrop Selector", { selector }); + _canDragDrop() { return game.user.isGM; } _onDrop(event) { diff --git a/module/core/constants.js b/module/core/constants.js index 6b91550a..713d8298 100644 --- a/module/core/constants.js +++ b/module/core/constants.js @@ -467,13 +467,13 @@ const C = { Secretive: "Knowledge has become so precious to you, that even your closest allies are on a need-to-know basis." }, EdgeTooltips: { - "Fearsome": "

The cohort is terrifying in aspect and reputation.

", - "Independent": "

The cohort can be trusted to make good decisions and act on their own initiative in the absence of direct orders.

", - "Loyal": "

The cohort can't be bribed or turned against you.

", - "Tenacious": "

The cohort won't be deterred from a task.

", - "Nimble": "

The vehicle handles easily. Consider this an assist for tricky maneuvers.

", - "Simple": "

The vehicle is easy to repair. Remove all of its Harm during downtime

", - "Sturdy": "

The vehicle keeps operating even when Broken.

", + Fearsome: "

The cohort is terrifying in aspect and reputation.

", + Independent: "

The cohort can be trusted to make good decisions and act on their own initiative in the absence of direct orders.

", + Loyal: "

The cohort can't be bribed or turned against you.

", + Tenacious: "

The cohort won't be deterred from a task.

", + Nimble: "

The vehicle handles easily. Consider this an assist for tricky maneuvers.

", + Simple: "

The vehicle is easy to repair. Remove all of its Harm during downtime

", + Sturdy: "

The vehicle keeps operating even when Broken.

", "Arrow-Swift": "

Your pet gains Potency when tracking or fighting the supernatural.

It can move extremely quickly, outpacing any other creature or vehicle.

", "Ghost Form": "

Your pet gains Potency when tracking or fighting the supernatural.

It can transform into electroplasmic vapor as if it were a spirit.

", "Mind Link": "

Your pet gains Potency when tracking or fighting the supernatural.

You and your pet can share senses and thoughts telepathically.

" @@ -982,7 +982,17 @@ const C = { [AttributeTrait.resolve]: [ActionTrait.attune, ActionTrait.command, ActionTrait.consort, ActionTrait.sway] }, Vices: [ - Vice.Faith, Vice.Gambling, Vice.Luxury, Vice.Obligation, Vice.Pleasure, Vice.Stupor, Vice.Weird, Vice.Worship, Vice.Living_Essence, Vice.Life_Essence, Vice.Electroplasmic_Power + Vice.Faith, + Vice.Gambling, + Vice.Luxury, + Vice.Obligation, + Vice.Pleasure, + Vice.Stupor, + Vice.Weird, + Vice.Worship, + Vice.Living_Essence, + Vice.Life_Essence, + Vice.Electroplasmic_Power ] }; export const Randomizers = { @@ -4225,7 +4235,7 @@ export const SVGDATA = { } }, teeth: { - "tall": { + tall: { viewBox: "0 0 512 1540", paths: { frame: "M0,0v1540l512-244.2V0H0z M451,1263.5l-390,186V61h390V1263.5z", @@ -4233,14 +4243,14 @@ export const SVGDATA = { full: "M0,0v1540l512-244.2V0H0z" } }, - "med": { + med: { viewBox: "0 0 512 1540", paths: { frame: "M0,0v1388l512-395.6V0H0z M458,965.7L54,1278V53h404V965.7z", full: "M0,0v1540l512-244.2V0H0z" } }, - "short": { + short: { viewBox: "0 0 512 1540", paths: { frame: "M0,0v991l511.4-247L512,0H0z M470.5,715.2L41,922.6V40h430L470.5,715.2z", diff --git a/module/documents/actors/BladesPC.js b/module/documents/actors/BladesPC.js index a08d7d7c..2a2f7987 100644 --- a/module/documents/actors/BladesPC.js +++ b/module/documents/actors/BladesPC.js @@ -37,13 +37,17 @@ class BladesPC extends BladesActor { async clearLoadout() { await this.update({ "system.loadout.selected": "" }); this.updateEmbeddedDocuments("Item", [ - ...this.activeSubItems.filter(item => BladesItem.IsType(item, BladesItemType.gear) && !item.hasTag(Tag.System.Archived)) + ...this.activeSubItems + .filter(item => BladesItem.IsType(item, BladesItemType.gear) + && !item.hasTag(Tag.System.Archived)) .map(item => ({ _id: item.id, "system.tags": [...item.tags, Tag.System.Archived], "system.uses_per_score.value": 0 })), - ...this.activeSubItems.filter(item => BladesItem.IsType(item, BladesItemType.ability) && item.system.uses_per_score.max) + ...this.activeSubItems + .filter(item => BladesItem.IsType(item, BladesItemType.ability) + && item.system.uses_per_score.max) .map(item => ({ _id: item.id, "system.uses_per_score.value": 0 @@ -100,28 +104,37 @@ class BladesPC extends BladesActor { return this.activeSubItems.find(item => item.type === BladesItemType.vice); } get crew() { - return this.activeSubActors.find((subActor) => BladesActor.IsType(subActor, BladesActorType.crew)); + return this.activeSubActors + .find((subActor) => BladesActor.IsType(subActor, BladesActorType.crew)); } get abilities() { if (!this.playbook) { return []; } - return this.activeSubItems.filter(item => [BladesItemType.ability, BladesItemType.crew_ability].includes(item.type)); + return this.activeSubItems + .filter(item => [BladesItemType.ability, BladesItemType.crew_ability].includes(item.type)); } get playbookName() { return this.playbook?.name; } get playbook() { - return this.activeSubItems.find((item) => item.type === BladesItemType.playbook); + return this.activeSubItems + .find((item) => item.type === BladesItemType.playbook); } get attributes() { if (!BladesActor.IsType(this, BladesActorType.pc)) { return undefined; } return { - insight: Object.values(this.system.attributes.insight).filter(({ value }) => value > 0).length + this.system.resistance_bonus.insight, - prowess: Object.values(this.system.attributes.prowess).filter(({ value }) => value > 0).length + this.system.resistance_bonus.prowess, - resolve: Object.values(this.system.attributes.resolve).filter(({ value }) => value > 0).length + this.system.resistance_bonus.resolve + insight: Object.values(this.system.attributes.insight) + .filter(({ value }) => value > 0).length + + this.system.resistance_bonus.insight, + prowess: Object.values(this.system.attributes.prowess) + .filter(({ value }) => value > 0).length + + this.system.resistance_bonus.prowess, + resolve: Object.values(this.system.attributes.resolve) + .filter(({ value }) => value > 0).length + + this.system.resistance_bonus.resolve }; } get actions() { @@ -153,14 +166,17 @@ class BladesPC extends BladesActor { .length; } get traumaList() { - return BladesActor.IsType(this, BladesActorType.pc) ? Object.keys(this.system.trauma.active).filter(key => this.system.trauma.active[key]) : []; + return BladesActor.IsType(this, BladesActorType.pc) + ? Object.keys(this.system.trauma.active).filter(key => this.system.trauma.active[key]) + : []; } get activeTraumaConditions() { if (!BladesActor.IsType(this, BladesActorType.pc)) { return {}; } return U.objFilter(this.system.trauma.checked, - (_v, traumaName) => Boolean(traumaName in this.system.trauma.active && this.system.trauma.active[traumaName])); + (_v, traumaName) => Boolean(traumaName in this.system.trauma.active + && this.system.trauma.active[traumaName])); } get currentLoad() { if (!BladesActor.IsType(this, BladesActorType.pc)) { @@ -176,7 +192,8 @@ class BladesPC extends BladesActor { if (!this.system.loadout.selected) { return 0; } - const maxLoad = this.system.loadout.levels[game.i18n.localize(this.system.loadout.selected.toString()).toLowerCase()]; + const maxLoad = this.system.loadout.levels[game.i18n.localize(this.system.loadout.selected.toString()) + .toLowerCase()]; return Math.max(0, maxLoad - this.currentLoad); } async addStash(amount) { @@ -217,7 +234,10 @@ class BladesPC extends BladesActor { get rollPrimaryImg() { return this.img; } get rollModsData() { const rollModsData = BladesRollMod.ParseDocRollMods(this); - [[/1d/, RollModSection.roll], [/Less Effect/, RollModSection.effect]].forEach(([effectPat, effectCat]) => { + [ + [/1d/, RollModSection.roll], + [/Less Effect/, RollModSection.effect] + ].forEach(([effectPat, effectCat]) => { const { one: harmConditionOne, two: harmConditionTwo } = Object.values(this.system.harm) .find(harmData => effectPat.test(harmData.effect)) ?? {}; const harmString = U.objCompact([harmConditionOne, harmConditionTwo === "" ? null : harmConditionTwo]).join(" & "); @@ -240,7 +260,8 @@ class BladesPC extends BladesActor { }); } }); - const { one: harmCondition } = Object.values(this.system.harm).find(harmData => /Need Help/.test(harmData.effect)) ?? {}; + const { one: harmCondition } = Object.values(this.system.harm) + .find(harmData => /Need Help/.test(harmData.effect)) ?? {}; if (harmCondition && harmCondition.trim() !== "") { rollModsData.push({ id: "Push-negative-roll", diff --git a/scss/sheets/_roll-collab-sheet.scss b/scss/sheets/_roll-collab-sheet.scss index 875cbb44..237105a0 100644 --- a/scss/sheets/_roll-collab-sheet.scss +++ b/scss/sheets/_roll-collab-sheet.scss @@ -1457,6 +1457,7 @@ width: 100%; position: absolute; pointer-events: none; + margin-top: 20px; &:not(.inactive-mod-block) { flex-direction: column; @@ -1477,12 +1478,43 @@ width: 100%; border-bottom-left-radius: 30px; border-bottom-right-radius: 30px; + overflow: hidden; + position: relative; + + .roll-odds-section-container { + height: 500%; + width: 100%; + position: absolute; + z-index: 5; + + display: flex; + justify-content: stretch; + align-items: stretch; + flex-wrap: nowrap; + + filter: blur(50px); + + > * { + flex-grow: 1; + flex-shrink: 1; + } + + } + } + + .roll-odds-label-container { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; text-align: center; line-height: 28px; color: var(--blades-gold-dark); font-family: var(--font-emphasis); font-size: 18px; text-shadow: 1.5px 1.5px 0px var(--blades-black-dark); + z-index: 5; } .roll-button { @@ -1505,6 +1537,7 @@ transition: 0.25s; filter: drop-shadow(3px 3px 5px var(--blades-black)); opacity: 1; + z-index: 5; &:hover { scale: 1.2; diff --git a/templates/components/slide-out-controls.hbs b/templates/components/slide-out-controls.hbs index 9361edb9..45fa2a08 100644 --- a/templates/components/slide-out-controls.hbs +++ b/templates/components/slide-out-controls.hbs @@ -7,7 +7,7 @@
  • -
  • +
  • diff --git a/templates/roll/partials/roll-collab-action-gm.hbs b/templates/roll/partials/roll-collab-action-gm.hbs index 3db01d9b..f56dd74c 100644 --- a/templates/roll/partials/roll-collab-action-gm.hbs +++ b/templates/roll/partials/roll-collab-action-gm.hbs @@ -265,7 +265,14 @@ {{/if}}
    -
    +
    + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} +
    +
    {{#if isGMReady}}Ready!{{else}}Click to Enable Roll{{/if}}
    diff --git a/templates/roll/partials/roll-collab-action.hbs b/templates/roll/partials/roll-collab-action.hbs index f84cfb04..235f81d9 100644 --- a/templates/roll/partials/roll-collab-action.hbs +++ b/templates/roll/partials/roll-collab-action.hbs @@ -285,9 +285,15 @@ {{/if}}
    -
    - {{#unless isGMReady}}Waiting For GM ...{{/unless}} +
    + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}} + {{{oddsHTMLStart}}}{{{oddsHTMLStop}}}
    + {{#unless isGMReady}} +
    Waiting For GM ...
    {{/unless}} {{#if isGMReady}} {{/if}} diff --git a/templates/roll/partials/roll-collab-gm-factor-control.hbs b/templates/roll/partials/roll-collab-gm-factor-control.hbs index 249b0edb..40fd301d 100644 --- a/templates/roll/partials/roll-collab-gm-factor-control.hbs +++ b/templates/roll/partials/roll-collab-gm-factor-control.hbs @@ -3,6 +3,11 @@ {{#with (lookup data.GMBoosts data.factor) as |sourceBoost|}} {{#with (lookup data.GMOppBoosts data.factor) as |oppBoost|}} +{{log "CONTEXT" data}} +{{log "Source Data" sourceData}} +{{log "Source Boost" sourceBoost}} +{{log "OppBoost" oppBoost}} +
    {{label}} {{/if}}
    - {{#select (lookup (lookup docSelections category) name)}} {{#each docs}} diff --git a/ts/@types/blades-roll.d.ts b/ts/@types/blades-roll.d.ts index 0764adac..ad532600 100644 --- a/ts/@types/blades-roll.d.ts +++ b/ts/@types/blades-roll.d.ts @@ -73,6 +73,8 @@ declare global { costAmount: number } + export type FactorToggle = "isActive"|"isPrimary"|"isDominant"|"highFavorsPC"; + export type FactorFlagData = { display: string, isActive?: boolean, @@ -118,6 +120,7 @@ declare global { diceTotal: number, rollOpposition?: OppositionDocData, + rollParticipants?: RollParticipantDocs, rollPositions: Position[], rollEffects: Effect[], @@ -143,8 +146,8 @@ declare global { rollFactors: Record<"source"|"opposition", Partial>>, - oddsGradient: string, - oddsGradientTestHTML?: string, + oddsHTMLStart: string, + oddsHTMLStop: string, costData?: Record<"footerLabel"|"tooltip",string> } diff --git a/ts/@types/index.d.ts b/ts/@types/index.d.ts index 3ee12b68..ad882fdd 100644 --- a/ts/@types/index.d.ts +++ b/ts/@types/index.d.ts @@ -76,14 +76,15 @@ declare global { type ClickEvent = JQuery.ClickEvent; type ContextMenuEvent = JQuery.ContextMenuEvent; type TriggerEvent = JQuery.TriggeredEvent; - type InputChangeEvent = JQuery.TypeEventHandler; + type InputChangeEvent = JQuery.ChangeEvent; type BlurEvent = JQuery.TypeEventHandler; // type DropEvent = JQuery.TypeEventHandler; type DropEvent = JQuery.DropEvent; type OnSubmitEvent = Event & ClickEvent & { result: Promise> } - + type ChangeEvent = JQuery.ChangeEvent; + type SelectChangeEvent = JQuery.ChangeEvent; } \ No newline at end of file diff --git a/ts/BladesRollCollab.ts b/ts/BladesRollCollab.ts index a03e0d91..0bfba146 100644 --- a/ts/BladesRollCollab.ts +++ b/ts/BladesRollCollab.ts @@ -445,43 +445,22 @@ class BladesRollMod { } get tooltip() { - if (this.sideString) { - return this._tooltip - .replace(/%COLON%/g, ":") - .replace(/%DOC_NAME%/g, this.sideString); - } - return this._tooltip.replace(/%COLON%/g, ":"); + return this._tooltip.replace(/%COLON%/g, ":") + .replace(/%DOC_NAME%/g, this.sideString ?? "an Ally") + .replace(/@OPPOSITION_NAME@/g, this.rollInstance.rollOpposition?.rollOppName ?? "Your Opposition"); } get sideString(): string | undefined { if (this._sideString) { return this._sideString; } - switch (this.category) { - case RollModSection.roll: { - if (this.name === "Assist") { - const docID = this.rollInstance.document.getFlag("eunos-blades", "rollCollab.docSelections.roll.Assist") as MaybeStringOrFalse; - if (!docID) { return undefined; } - return (game.actors.get(docID) ?? game.items.get(docID))?.name ?? undefined; - } - return undefined; - } - case RollModSection.position: { - if (this.name === "Setup") { - const docID = this.rollInstance.document.getFlag("eunos-blades", "rollCollab.docSelections.position.Setup") as MaybeStringOrFalse; - if (!docID) { return undefined; } - return (game.actors.get(docID) ?? game.items.get(docID))?.name ?? undefined; - } - return undefined; - } - case RollModSection.effect: { - if (this.name === "Setup") { - const docID = this.rollInstance.document.getFlag("eunos-blades", "rollCollab.docSelections.effect.Setup") as MaybeStringOrFalse; - if (!docID) { return undefined; } - return (game.actors.get(docID) ?? game.items.get(docID))?.name ?? undefined; - } - return undefined; - } - default: return undefined; + const rollParticipantCategoryData = this.rollInstance.rollParticipants?. + [this.category as BladesRollCollab.RollParticipantSection]; + if (rollParticipantCategoryData && this.name in rollParticipantCategoryData) { + const rollParticipant = rollParticipantCategoryData[ + this.name as KeyOf + ] as BladesRollCollab.ParticipantDocData; + return rollParticipant.rollParticipantName; } + return undefined; } get allFlagData(): BladesRollCollab.FlagData { @@ -671,10 +650,10 @@ class BladesRollPrimary implements BladesRollCollab.PrimaryDocData { this.rollPrimaryType = this.rollPrimaryDoc.rollPrimaryType; this.rollPrimaryImg = rollPrimaryImg ?? this.rollPrimaryDoc.rollPrimaryImg ?? ""; this._rollModsData = rollModsData ?? []; - this.rollFactors = Object.assign( - this.rollPrimaryDoc.rollFactors, - rollFactors ?? {} - ); + this.rollFactors = { + ...this.rollPrimaryDoc.rollFactors, + ...rollFactors ?? {} + }; } else { if (!rollPrimaryName) { throw new Error("Must include a rollPrimaryName when constructing a BladesRollPrimary object."); } if (!rollPrimaryImg) { throw new Error("Must include a rollPrimaryImg when constructing a BladesRollPrimary object."); } @@ -1269,6 +1248,7 @@ class BladesRollCollab extends DocumentSheet { } static RenderRollCollab(rollID: string) { + BladesRollCollab.Current[rollID]?.prepareRollParticipantData(); BladesRollCollab.Current[rollID]?.render(); } @@ -1586,7 +1566,6 @@ class BladesRollCollab extends DocumentSheet { async addRollParticipant(participant: BladesRollParticipant) { await participant.updateRollFlags(); - this.prepareRollParticipantData(); // This.updateUsers(); socketlib.system.executeForEveryone("renderRollCollab", this.rollID); } @@ -1749,124 +1728,109 @@ class BladesRollCollab extends DocumentSheet { } get rollFactors(): Record<"source" | "opposition", Partial>> { - const sourceFactors: Partial> = Object.fromEntries( - (Object.entries(this.rollPrimary.rollFactors) as Array<[Factor, BladesRollCollab.FactorData]>) - .map(([factor, factorData]) => [ - factor, - { - ...factorData, - ...this.flagData.rollFactorToggles.source[factor] ?? [] - } - ])); - Object.entries(this.flagData.rollFactorToggles.source).forEach(([factor, factorData]) => { - if (!(factor in sourceFactors)) { - sourceFactors[factor as Factor] = { - name: factor, - value: 0, - max: 0, - baseVal: 0, - cssClasses: "factor-gold", - isActive: factorData.isActive ?? false, - isPrimary: factorData.isPrimary ?? (factor === Factor.tier), - isDominant: factorData.isDominant ?? false, - highFavorsPC: factorData.highFavorsPC ?? true - }; - } - }); - - Object.keys(sourceFactors) - .filter(isFactor) - .forEach(factor => { - const factorData = sourceFactors[factor]; - if (!factorData) { return; } - factorData.value ??= 0; - factorData.value += - (this.flagData.GMBoosts[factor] ?? 0) - + (this.tempGMBoosts[factor] ?? 0); - }); - - Object.keys(sourceFactors) - .filter(isFactor) - .forEach(factor => { - const factorData = sourceFactors[factor]; - if (!factorData) { return; } - factorData.value ??= 0; - factorData.value += this.flagData.GMOppBoosts[factor] ?? 0; - if (factor === Factor.tier) { - factorData.display = U.romanizeNum(factorData.value); - } else { - factorData.display = `${factorData.value}`; - } - }); - - const rollOppFactors = this.rollOpposition?.rollFactors - ?? Object.fromEntries(([ - Factor.tier, - Factor.quality, - Factor.scale, - Factor.magnitude - ]).map(factor => [ - factor, - { - name: factor, - value: 0, - max: 0, - baseVal: 0, - cssClasses: "factor-gold", - isActive: false, - isPrimary: factor === Factor.tier, - isDominant: false, - highFavorsPC: true - } - ])); - const oppFactors: Partial> = {}; - - Object.entries(rollOppFactors) - .forEach(([factor, factorData]) => { - if (!isFactor(factor)) { return; } - oppFactors[factor] = { - ...factorData, - ...this.flagData.rollFactorToggles.opposition[factor] ?? [] - }; - }); - - Object.entries(this.flagData.rollFactorToggles.opposition) - .forEach(([factor, factorData]) => { - if (!isFactor(factor)) { return; } - if (!(factor in oppFactors)) { - oppFactors[factor] = { - name: factor, - value: 0, - max: 0, - baseVal: 0, - cssClasses: "factor-gold", - isActive: factorData.isActive ?? false, - isPrimary: factorData.isPrimary ?? (factor === Factor.tier), - isDominant: factorData.isDominant ?? false, - highFavorsPC: factorData.highFavorsPC ?? true - }; - } - }); - Object.keys(oppFactors).forEach(factor => { - if (!isFactor(factor)) { return; } - const factorData = oppFactors[factor]; - if (!factorData) { return; } - factorData.value += this.flagData.GMOppBoosts[factor as Factor] ?? 0; - if (factor === Factor.tier) { - factorData.display = U.romanizeNum(factorData.value); - } else { - factorData.display = `${factorData.value}`; + const defaultFactors: Record = { + [Factor.tier]: { + name: "Tier", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: true, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" + }, + [Factor.quality]: { + name: "Quality", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: false, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" + }, + [Factor.scale]: { + name: "Scale", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: false, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" + }, + [Factor.magnitude]: { + name: "Magnitude", + value: 0, + max: 0, + baseVal: 0, + display: "?", + isActive: false, + isPrimary: false, + isDominant: false, + highFavorsPC: true, + cssClasses: "factor-gold" } - }); + }; + + const mergedSourceFactors = + U.objMerge( + U.objMerge( + defaultFactors, + this.rollPrimary.rollFactors, + {isMutatingOk: false} + ), + this.flagData.rollFactorToggles.source, + {isMutatingOk: false} + ) as Record; + + const mergedOppFactors = this.rollOpposition + ? U.objMerge( + U.objMerge( + defaultFactors, + this.rollPrimary.rollFactors, + {isMutatingOk: false} + ), + this.flagData.rollFactorToggles.opposition, + {isMutatingOk: false} + ) as Record + : {}; return { - source: sourceFactors, - opposition: oppFactors + source: Object.fromEntries( + (Object.entries(mergedSourceFactors) as Array<[Factor, BladesRollCollab.FactorData]>) + .map(([factor, factorData]) => { + factorData.value += + (this.flagData.GMBoosts[factor] ?? 0) + + (this.tempGMBoosts[factor] ?? 0); + if (factor === Factor.tier) { + factorData.display = U.romanizeNum(factorData.value); + } else { + factorData.display = `${factorData.value}`; + } + return [factor, factorData]; + }) + ) as Record, + opposition: Object.fromEntries( + (Object.entries(mergedOppFactors) as Array<[Factor, BladesRollCollab.FactorData]>) + .map(([factor, factorData]) => { + factorData.value += this.flagData.GMOppBoosts[factor] ?? 0; + if (factor === Factor.tier) { + factorData.display = U.romanizeNum(factorData.value); + } else { + factorData.display = `${factorData.value}`; + } + return [factor, factorData]; + }) + ) as Record }; } // #endregion @@ -2272,6 +2236,7 @@ class BladesRollCollab extends DocumentSheet { diceTotal: finalDicePool, rollOpposition: this.rollOpposition, + rollParticipants: this.rollParticipants, rollEffects: Object.values(Effect), teamworkDocs: game.actors.filter(actor => BladesActor.IsType(actor, BladesActorType.pc)), @@ -2286,7 +2251,7 @@ class BladesRollCollab extends DocumentSheet { hasInactiveConditionals: this.calculateHasInactiveConditionalsData(), rollFactors, - oddsGradient: this.calculateOddsGradient(finalDicePool, finalResult), + ...this.calculateOddsHTML(finalDicePool, finalResult), costData: this.parseCostsHTML(this.getStressCosts(rollCosts), this.getSpecArmorCost(rollCosts)) }; @@ -2401,6 +2366,52 @@ class BladesRollCollab extends DocumentSheet { ].join(", "); } + /** + * Calculate odds starting & ending HTML based on given dice total. + * @param {number} diceTotal Total number of dice. + * @param {number} finalResult + * @returns {{oddsHTMLStart: string, oddsHTMLStop: string}} Opening & Closing HTML for odds bar display + */ + private calculateOddsHTML(diceTotal: number, finalResult: number): {oddsHTMLStart: string, oddsHTMLStop: string} { + const oddsColors = { + crit: "var(--blades-gold)", + success: "var(--blades-white-bright)", + partial: "var(--blades-grey)", + fail: "var(--blades-black-dark)" + }; + const odds = {...C.DiceOdds[diceTotal]}; + + if (finalResult < 0) { + for (let i = finalResult; i < 0; i++) { + oddsColors.crit = oddsColors.success; + oddsColors.success = oddsColors.partial; + oddsColors.partial = oddsColors.fail; + } + } else if (finalResult > 0) { + for (let i = 0; i < finalResult; i++) { + oddsColors.fail = oddsColors.partial; + oddsColors.partial = oddsColors.success; + oddsColors.success = oddsColors.crit; + } + } + + const resultElements: string[] = []; + + (Object.entries(odds).reverse() as Array<[KeyOf, number]>).forEach(([result, chance]) => { + if (chance === 0) { return; } + resultElements.push(`
     
    `); + }); + + return { + oddsHTMLStart: [ + "
    ", + ...resultElements + ].join("\n"), + oddsHTMLStop: "
    " + }; + } + + /** * Calculate data for position and effect trade. * @returns {{canTradePosition: boolean, canTradeEffect: boolean}} @@ -2595,6 +2606,7 @@ class BladesRollCollab extends DocumentSheet { // #endregion // #region LISTENER FUNCTIONS ~ + _toggleRollModClick(event: ClickEvent) { event.preventDefault(); const elem$ = $(event.currentTarget); @@ -2615,39 +2627,28 @@ class BladesRollCollab extends DocumentSheet { } } - _toggleRollModContext(event: ClickEvent) { - event.preventDefault(); - if (!game.user.isGM) { return; } - const elem$ = $(event.currentTarget); - const id = elem$.data("id"); - const rollMod = this.getRollModByID(id); - if (!rollMod) { throw new Error(`Unable to find roll mod with id '${id}'`); } - - switch (rollMod.status) { - case RollModStatus.Hidden: rollMod.userStatus = RollModStatus.ToggledOff; return; - case RollModStatus.ForcedOff: rollMod.userStatus = RollModStatus.Hidden; return; - case RollModStatus.ToggledOff: rollMod.userStatus = RollModStatus.ForcedOff; return; - case RollModStatus.ToggledOn: rollMod.userStatus = RollModStatus.ToggledOff; return; - case RollModStatus.ForcedOn: rollMod.userStatus = RollModStatus.Hidden; return; - default: throw new Error(`Unrecognized RollModStatus: ${rollMod.status}`); - } - } - + /** + * Handles setting of rollMod status via GM pop-out controls + * @param {ClickEvent} event JQuery click event sent to listener. + */ _gmControlSet(event: ClickEvent) { event.preventDefault(); if (!game.user.isGM) { return; } const elem$ = $(event.currentTarget); const id = elem$.data("id"); const status = elem$.data("status"); - - if (!isModStatus(status)) { return; } - + if (!isModStatus(status) && status !== "Reset") { return; } const rollMod = this.getRollModByID(id); + if (rollMod) { - rollMod.userStatus = status; + rollMod.userStatus = status === "Reset" ? undefined : status; } } + /** + * Handles setting values via GM number line (e.g. roll factor boosts/modifications). + * @param {ClickEvent} event JQuery click event sent to listener. + */ async _gmControlSetTargetToValue(event: ClickEvent) { event.preventDefault(); if (!game.user.isGM) { return; } @@ -2658,6 +2659,10 @@ class BladesRollCollab extends DocumentSheet { await this.document.setFlag(C.SYSTEM_ID, target, value).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } + /** + * Handles resetting value associated with GM number line on a right-click. + * @param {ClickEvent} event JQuery context menu event sent to listener. + */ async _gmControlResetTarget(event: ClickEvent) { event.preventDefault(); if (!game.user.isGM) { return; } @@ -2667,18 +2672,10 @@ class BladesRollCollab extends DocumentSheet { await this.document.unsetFlag(C.SYSTEM_ID, target).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - _gmControlReset(event: ClickEvent) { - event.preventDefault(); - if (!game.user.isGM) { return; } - const elem$ = $(event.currentTarget); - const id = elem$.data("id"); - - const rollMod = this.getRollModByID(id); - if (rollMod) { - rollMod.userStatus = undefined; - } - } - + /** + * Handles setting of baseline rollPosition via GM button line + * @param {ClickEvent} event JQuery click event sent to listener. + */ _gmControlSetPosition(event: ClickEvent) { event.preventDefault(); if (!game.user.isGM) { return; } @@ -2687,6 +2684,10 @@ class BladesRollCollab extends DocumentSheet { this.initialPosition = position; } + /** + * Handles setting of baseline rollPosition via GM button line + * @param {ClickEvent} event JQuery click event sent to listener. + */ _gmControlSetEffect(event: ClickEvent) { event.preventDefault(); if (!game.user.isGM) { return; } @@ -2695,6 +2696,10 @@ class BladesRollCollab extends DocumentSheet { this.initialEffect = effect; } + /** + * Handles setting of Factor toggles: isActive, isPrimary, highFavorsPC, isDominant + * @param {ClickEvent} event JQuery click event sent to listener. + */ async _gmControlToggleFactor(event: ClickEvent) { event.preventDefault(); if (!game.user.isGM) { return; } @@ -2704,30 +2709,76 @@ class BladesRollCollab extends DocumentSheet { eLog.checkLog3("toggleFactor", "_gmControlToggleFactor", {event, target, value}); - if (value && /isPrimary/.test(target)) { - const [thisSource, thisFactor] = target.split(/\./).slice(-3, -1) as ["source" | "opposition", Factor]; - eLog.checkLog3("toggleFactor", "_gmControlToggleFactor - IN", {thisSource, thisFactor}); - await Promise.all(Object.values(Factor).map(factor => { - if (factor === thisFactor) { - eLog.checkLog3("toggleFactor", `_gmControlToggleFactor - Checking ${factor} === ${thisFactor} === TRUE`, {factor, thisFactor, target, customTarget: `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary`}); - return this.document.setFlag(C.SYSTEM_ID, `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary`, true).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); - } else { - eLog.checkLog3("toggleFactor", `_gmControlToggleFactor - Checking ${factor} === ${thisFactor} === FALSE`, {factor, thisFactor, target, customTarget: `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary`}); - return this.document.setFlag(C.SYSTEM_ID, `rollCollab.rollFactorToggles.${thisSource}.${factor}.isPrimary`, false).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + const factorToggleData = this.document.getFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles") as Record<"source"|"opposition", Record>; + + const [thisSource, thisFactor, thisToggle] = target.split(/\./).slice(-3) as ["source" | "opposition", Factor, BladesRollCollab.FactorToggle]; + + // If thisToggle is unrecognized, just toggle whatever value target points at + if (!["isActive", "isPrimary", "isDominant", "highFavorsPC"].includes(thisToggle)) { + return this.document.setFlag(C.SYSTEM_ID, `rollCollab.${target}`, value) + .then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + } + + // Otherwise, first toggle targeted factor to new value + factorToggleData[thisSource][thisFactor] = { + ...factorToggleData[thisSource][thisFactor] ?? {display: ""}, + [thisToggle]: value + }; + + // Then perform specific logic depending on toggle targeted: + switch (thisToggle) { + case "isDominant": + case "isPrimary": { + // Only one factor per sourceType can be declared Primary or Dominant: + // If one is being activated, must toggle off the others. + if (value === true) { + Object.values(Factor) + .filter(factor => factor !== thisFactor) + .forEach(factor => { + if (factorToggleData[thisSource][factor]?.[thisToggle] === true) { + factorToggleData[thisSource][factor] = { + ...factorToggleData[thisSource][factor], + [thisToggle]: false + }; + } + }); } - })); - eLog.checkLog3("toggleFactor", "_gmControlToggleFactor - ALL DONE", {flags: this.document.getFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles")}); - } else { - this.document.setFlag(C.SYSTEM_ID, `rollCollab.${target}`, value).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + break; + } + case "isActive": { + // 'isActive' should be synchronized when 1) value is true, and 2) the other value is false + if (value === true) { + const otherSource = thisSource === "source" ? "opposition" : "source"; + factorToggleData[otherSource][thisFactor] = { + ...factorToggleData[otherSource][thisFactor] ?? {display: ""}, + isActive: value + }; + } + break; + } + default: break; } + + return this.document.setFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles", factorToggleData) + .then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - async _gmControlResetFactor(event: ClickEvent) { + async _gmControlSelectDocument(event: SelectChangeEvent) { event.preventDefault(); - if (!game.user.isGM) { return; } const elem$ = $(event.currentTarget); - const target = elem$.data("target"); - await this.document.unsetFlag(C.SYSTEM_ID, `rollCollab.${target}`).then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); + const section = elem$.data("rollSection"); + const subSection = elem$.data("rollSubSection"); + const selectedOption = elem$.val(); + + if (typeof selectedOption !== "string") { return; } + if (selectedOption === "false") { + return this.document.unsetFlag(C.SYSTEM_ID, `rollCollab.rollParticipantData.${section}.${subSection}`); + } + return this.addRollParticipant(new BladesRollParticipant(this, { + rollParticipantSection: section, + rollParticipantSubSection: subSection, + rollParticipantID: selectedOption + })); } get resistanceStressCost(): number { @@ -2779,39 +2830,54 @@ class BladesRollCollab extends DocumentSheet { html.on({ focusin: () => { BladesRollCollab.Active = this; } // Set reference to top-most, focused roll. }); - html.find("[data-action='gm-set'").on({ - click: this._gmControlSet.bind(this) + /** + * Handles setting of rollMod status via GM pop-out controls + */ + html.find(".controls-toggle").on({ + click: event => { + event.preventDefault(); + $(event.currentTarget).parents(".controls-panel").toggleClass("active"); + } }); - html.find("[data-action='gm-reset'").on({ - click: this._gmControlReset.bind(this) + html.find("[data-action=\"gm-set\"").on({ + click: this._gmControlSet.bind(this) }); - html.find("[data-action='gm-set-position'").on({ + /** + * Handles setting of baseline rollPosition via GM button line + */ + html.find("[data-action=\"gm-set-position\"").on({ click: this._gmControlSetPosition.bind(this) }); - html.find("[data-action='gm-set-effect'").on({ + /** + * Handles setting of baseline rollEffect via GM button line + */ + html.find("[data-action=\"gm-set-effect\"").on({ click: this._gmControlSetEffect.bind(this) }); - html.find("[data-action='gm-set-target'").on({ + /** + * Handles setting values via GM number line (e.g. roll factor boosts/modifications). + * Handles resetting value associated with GM number line on a right-click. + */ + html.find("[data-action=\"gm-set-target\"").on({ click: this._gmControlSetTargetToValue.bind(this), contextmenu: this._gmControlResetTarget.bind(this) }); - html.find("[data-action='gm-toggle-factor'").on({ - click: this._gmControlToggleFactor.bind(this), - contextmenu: this._gmControlResetFactor.bind(this) + /** + * Handles setting of Factor toggles: isActive, isPrimary, highFavorsPC, isDominant + */ + html.find("[data-action=\"gm-toggle-factor\"").on({ + click: this._gmControlToggleFactor.bind(this) }); - html.find(".controls-toggle").on({ - click: event => { - event.preventDefault(); - $(event.currentTarget).parents(".controls-panel").toggleClass("active"); - } + + html.find("select.roll-sheet-doc-select").on({ + change: this._gmControlSelectDocument.bind(this) }); } // #endregion // #region OVERRIDES: _canDragDrop, _onDrop, _onSubmit, close, render ~ - override _canDragDrop(selector: string) { - eLog.checkLog3("canDragDrop", "Can DragDrop Selector", {selector}); + override _canDragDrop() { return game.user.isGM; } diff --git a/ts/core/constants.ts b/ts/core/constants.ts index ca8c66cd..92628f5a 100644 --- a/ts/core/constants.ts +++ b/ts/core/constants.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-shadow */ export enum BladesPermissions { NONE = CONST.DOCUMENT_PERMISSION_LEVELS.NONE, BASIC = CONST.DOCUMENT_PERMISSION_LEVELS.LIMITED, @@ -448,13 +449,13 @@ const C = { Secretive: "Knowledge has become so precious to you, that even your closest allies are on a need-to-know basis." }, EdgeTooltips: { - "Fearsome": "

    The cohort is terrifying in aspect and reputation.

    ", - "Independent": "

    The cohort can be trusted to make good decisions and act on their own initiative in the absence of direct orders.

    ", - "Loyal": "

    The cohort can't be bribed or turned against you.

    ", - "Tenacious": "

    The cohort won't be deterred from a task.

    ", - "Nimble": "

    The vehicle handles easily. Consider this an assist for tricky maneuvers.

    ", - "Simple": "

    The vehicle is easy to repair. Remove all of its Harm during downtime

    ", - "Sturdy": "

    The vehicle keeps operating even when Broken.

    ", + Fearsome: "

    The cohort is terrifying in aspect and reputation.

    ", + Independent: "

    The cohort can be trusted to make good decisions and act on their own initiative in the absence of direct orders.

    ", + Loyal: "

    The cohort can't be bribed or turned against you.

    ", + Tenacious: "

    The cohort won't be deterred from a task.

    ", + Nimble: "

    The vehicle handles easily. Consider this an assist for tricky maneuvers.

    ", + Simple: "

    The vehicle is easy to repair. Remove all of its Harm during downtime

    ", + Sturdy: "

    The vehicle keeps operating even when Broken.

    ", "Arrow-Swift": "

    Your pet gains Potency when tracking or fighting the supernatural.

    It can move extremely quickly, outpacing any other creature or vehicle.

    ", "Ghost Form": "

    Your pet gains Potency when tracking or fighting the supernatural.

    It can transform into electroplasmic vapor as if it were a spirit.

    ", "Mind Link": "

    Your pet gains Potency when tracking or fighting the supernatural.

    You and your pet can share senses and thoughts telepathically.

    " @@ -969,7 +970,17 @@ const C = { [AttributeTrait.resolve]: [ActionTrait.attune, ActionTrait.command, ActionTrait.consort, ActionTrait.sway] }, Vices: [ - Vice.Faith, Vice.Gambling, Vice.Luxury, Vice.Obligation, Vice.Pleasure, Vice.Stupor, Vice.Weird, Vice.Worship, Vice.Living_Essence, Vice.Life_Essence, Vice.Electroplasmic_Power + Vice.Faith, + Vice.Gambling, + Vice.Luxury, + Vice.Obligation, + Vice.Pleasure, + Vice.Stupor, + Vice.Weird, + Vice.Worship, + Vice.Living_Essence, + Vice.Life_Essence, + Vice.Electroplasmic_Power ] }; @@ -4221,7 +4232,7 @@ export const SVGDATA = { } }, teeth: { - "tall": { + tall: { viewBox: "0 0 512 1540", paths: { frame: "M0,0v1540l512-244.2V0H0z M451,1263.5l-390,186V61h390V1263.5z", @@ -4229,14 +4240,14 @@ export const SVGDATA = { full: "M0,0v1540l512-244.2V0H0z" } }, - "med": { + med: { viewBox: "0 0 512 1540", paths: { frame: "M0,0v1388l512-395.6V0H0z M458,965.7L54,1278V53h404V965.7z", full: "M0,0v1540l512-244.2V0H0z" } }, - "short": { + short: { viewBox: "0 0 512 1540", paths: { frame: "M0,0v991l511.4-247L512,0H0z M470.5,715.2L41,922.6V40h430L470.5,715.2z", diff --git a/ts/documents/actors/BladesPC.ts b/ts/documents/actors/BladesPC.ts index 51f67dc9..120a8672 100644 --- a/ts/documents/actors/BladesPC.ts +++ b/ts/documents/actors/BladesPC.ts @@ -15,7 +15,12 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, return super.IsType(doc, BladesActorType.pc); } - static override async create(data: ActorDataConstructorData & { system?: Partial }, options = {}) { + static override async create( + data: ActorDataConstructorData & { + system?: Partial + }, + options = {} + ) { data.token = data.token || {}; data.system = data.system ?? {}; @@ -47,13 +52,17 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, this.updateEmbeddedDocuments( "Item", [ - ...this.activeSubItems.filter(item => BladesItem.IsType(item, BladesItemType.gear) && !item.hasTag(Tag.System.Archived)) + ...this.activeSubItems + .filter(item => BladesItem.IsType(item, BladesItemType.gear) + && !item.hasTag(Tag.System.Archived)) .map(item => ({ _id: item.id, "system.tags": [...item.tags, Tag.System.Archived], "system.uses_per_score.value": 0 })), - ...this.activeSubItems.filter(item => BladesItem.IsType(item, BladesItemType.ability) && item.system.uses_per_score.max) + ...this.activeSubItems + .filter(item => BladesItem.IsType(item, BladesItemType.ability) + && item.system.uses_per_score.max) .map(item => ({ _id: item.id, "system.uses_per_score.value": 0 @@ -110,12 +119,14 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, } get crew(): BladesCrew | undefined { - return this.activeSubActors.find((subActor): subActor is BladesCrew => BladesActor.IsType(subActor, BladesActorType.crew)); + return this.activeSubActors + .find((subActor): subActor is BladesCrew => BladesActor.IsType(subActor, BladesActorType.crew)); } get abilities(): BladesItem[] { if (!this.playbook) { return []; } - return this.activeSubItems.filter(item => [BladesItemType.ability, BladesItemType.crew_ability].includes(item.type)); + return this.activeSubItems + .filter(item => [BladesItemType.ability, BladesItemType.crew_ability].includes(item.type)); } get playbookName() { @@ -123,15 +134,22 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, } get playbook(): BladesItemOfType|undefined { - return this.activeSubItems.find((item): item is BladesItemOfType => item.type === BladesItemType.playbook); + return this.activeSubItems + .find((item): item is BladesItemOfType => item.type === BladesItemType.playbook); } get attributes(): Record { if (!BladesActor.IsType(this, BladesActorType.pc)) { return undefined as never; } return { - insight: Object.values(this.system.attributes.insight).filter(({value}) => value > 0).length + this.system.resistance_bonus.insight, - prowess: Object.values(this.system.attributes.prowess).filter(({value}) => value > 0).length + this.system.resistance_bonus.prowess, - resolve: Object.values(this.system.attributes.resolve).filter(({value}) => value > 0).length + this.system.resistance_bonus.resolve + insight: Object.values(this.system.attributes.insight) + .filter(({value}) => value > 0).length + + this.system.resistance_bonus.insight, + prowess: Object.values(this.system.attributes.prowess) + .filter(({value}) => value > 0).length + + this.system.resistance_bonus.prowess, + resolve: Object.values(this.system.attributes.resolve) + .filter(({value}) => value > 0).length + + this.system.resistance_bonus.resolve }; } @@ -163,7 +181,9 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, get traumaList(): string[] { // @ts-ignore Compiler linter mismatch. - return BladesActor.IsType(this, BladesActorType.pc) ? Object.keys(this.system.trauma.active).filter(key => this.system.trauma.active[key]) : []; + return BladesActor.IsType(this, BladesActorType.pc) + ? Object.keys(this.system.trauma.active).filter(key => this.system.trauma.active[key]) + : []; } get activeTraumaConditions(): Record { @@ -171,7 +191,10 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, return U.objFilter( this.system.trauma.checked, // @ts-ignore Compiler linter mismatch. - (_v: unknown, traumaName: string): boolean => Boolean(traumaName in this.system.trauma.active && this.system.trauma.active[traumaName]) + (_v: unknown, traumaName: string): boolean => Boolean( + traumaName in this.system.trauma.active + && this.system.trauma.active[traumaName] + ) ); } @@ -184,7 +207,10 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, get remainingLoad(): number { if (!BladesActor.IsType(this, BladesActorType.pc)) { return 0; } if (!this.system.loadout.selected) { return 0; } - const maxLoad = this.system.loadout.levels[game.i18n.localize(this.system.loadout.selected.toString()).toLowerCase() as KeyOf]; + const maxLoad = this.system.loadout.levels[ + game.i18n.localize(this.system.loadout.selected.toString()) + .toLowerCase() as KeyOf + ]; return Math.max(0, maxLoad - this.currentLoad); } @@ -240,7 +266,10 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, const rollModsData = BladesRollMod.ParseDocRollMods(this); // Add roll mods from harm - [[/1d/, RollModSection.roll] as const, [/Less Effect/, RollModSection.effect] as const].forEach(([effectPat, effectCat]) => { + [ + [/1d/, RollModSection.roll] as const, + [/Less Effect/, RollModSection.effect] as const + ].forEach(([effectPat, effectCat]) => { const {one: harmConditionOne, two: harmConditionTwo} = Object.values(this.system.harm) .find(harmData => effectPat.test(harmData.effect)) ?? {}; const harmString = U.objCompact([harmConditionOne, harmConditionTwo === "" ? null : harmConditionTwo]).join(" & "); @@ -263,7 +292,8 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, }); } }); - const {one: harmCondition} = Object.values(this.system.harm).find(harmData => /Need Help/.test(harmData.effect)) ?? {}; + const {one: harmCondition} = Object.values(this.system.harm) + .find(harmData => /Need Help/.test(harmData.effect)) ?? {}; if (harmCondition && harmCondition.trim() !== "") { rollModsData.push({ id: "Push-negative-roll",