From 5fb41f26d49843d72ab9c33ea4dd0b38eb738043 Mon Sep 17 00:00:00 2001 From: Eunomiac Date: Sat, 28 Oct 2023 09:35:52 -0400 Subject: [PATCH] Slow but steady work on BladesRoll, moving to resistance rolls now --- css/style.min.css | 4 +- module/BladesRoll.js | 192 +++++++++++---- module/blades.js | 20 +- module/core/ai.js | 63 +++-- module/core/constants.js | 67 +++++- module/core/gsap.js | 21 +- module/documents/actors/BladesCrew.js | 12 +- module/documents/actors/BladesFaction.js | 2 + module/documents/actors/BladesNPC.js | 4 + module/documents/actors/BladesPC.js | 6 +- scss/sheets/_roll-collab-sheet.scss | 6 +- templates/components/roll-collab-mod.hbs | 6 +- .../roll/partials/roll-collab-action-gm.hbs | 13 ++ .../partials/roll-collab-gm-select-doc.hbs | 13 +- ts/@types/blades-general-types.d.ts | 4 + ts/@types/blades-roll.d.ts | 94 +++++--- ts/BladesRoll.ts | 219 +++++++++++++----- ts/blades.ts | 20 +- ts/core/ai.ts | 91 ++++++-- ts/core/constants.ts | 71 +++++- ts/core/gsap.ts | 48 ++-- ts/documents/actors/BladesCrew.ts | 56 +++-- ts/documents/actors/BladesFaction.ts | 2 + ts/documents/actors/BladesNPC.ts | 4 + ts/documents/actors/BladesPC.ts | 6 +- 25 files changed, 763 insertions(+), 281 deletions(-) diff --git a/css/style.min.css b/css/style.min.css index 840914f7..256840ce 100644 --- a/css/style.min.css +++ b/css/style.min.css @@ -13504,14 +13504,14 @@ template { min-width: 120px; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form .sheet-root .roll-sheet-block .roll-sheet-sub-block .roll-mod-container.roll-mod-teamwork .roll-mod-label .roll-mod-sidestring { min-width: 75px; } - :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form .sheet-root .roll-sheet-block .roll-sheet-sub-block .roll-mod-container .roll-mod-label .roll-doc-select-container { + :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form .sheet-root .roll-sheet-block .roll-sheet-sub-block .roll-mod-container .roll-mod-label .roll-select-container { min-width: 120px; max-height: 16px; border-radius: 8px; margin-left: -85px; margin-right: -50px; background: var(--blades-black-dark); } - :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form .sheet-root .roll-sheet-block .roll-sheet-sub-block .roll-mod-container .roll-mod-label .roll-doc-select-container .roll-doc-select .roll-sheet-doc-select { + :root body.vtt.game.system-eunos-blades .app.window-app.sheet.roll-collab.gm-roll-collab .window-content form .sheet-root .roll-sheet-block .roll-sheet-sub-block .roll-mod-container .roll-mod-label .roll-select-container .roll-select .roll-sheet-select-doc { pointer-events: auto !important; appearance: none; width: 85px; diff --git a/module/BladesRoll.js b/module/BladesRoll.js index 117360ee..5c34fdc6 100644 --- a/module/BladesRoll.js +++ b/module/BladesRoll.js @@ -6,10 +6,11 @@ \* ****▌███████████████████████████████████████████████████████████████████████████▐**** */ import U from "./core/utilities.js"; -import C, { BladesActorType, BladesItemType, RollPermissions, RollType, RollSubType, RollModStatus, RollModSection, ActionTrait, DowntimeAction, AttributeTrait, Position, Effect, Factor, RollResult, RollPhase, ConsequenceType } from "./core/constants.js"; +import C, { BladesActorType, BladesItemType, RollPermissions, RollType, RollSubType, RollModStatus, RollModSection, ActionTrait, DowntimeAction, AttributeTrait, Position, Effect, Factor, RollResult, RollPhase, ConsequenceType, Tag } from "./core/constants.js"; import { BladesActor, BladesPC, BladesCrew } from "./documents/BladesActorProxy.js"; import { BladesItem, BladesGMTracker } from "./documents/BladesItemProxy.js"; import { ApplyTooltipListeners } from "./core/gsap.js"; +import BladesAI, { AGENTS } from "./core/ai.js"; function isRollType(str) { return typeof str === "string" && str in RollType; @@ -74,7 +75,7 @@ class BladesRollMod { const rollModData = { id: `${nameVal}-${posNegVal}-${catVal}`, name: nameVal, - category: catVal, + section: catVal, base_status: RollModStatus.ToggledOff, modType: "general", value: 1, @@ -402,20 +403,28 @@ class BladesRollMod { Consequence: () => { }, HarmLevel: () => { - if (!this.rollInstance.rollConsequence) { - return; + const harmLevels = [ + ConsequenceType.Harm1, + ConsequenceType.Harm2, + ConsequenceType.Harm3, + ConsequenceType.Harm4 + ]; + let harmConsequence = undefined; + while (!harmConsequence && harmLevels.length > 0) { + harmConsequence = Object.values(this.rollInstance.rollConsequences) + .find(({ type }) => type === harmLevels.pop()); } - const consequenceType = this.rollInstance.rollConsequence.type; - if (!consequenceType?.startsWith("Harm")) { - return; - } - const curLevel = [ConsequenceType.Harm1, ConsequenceType.Harm2, ConsequenceType.Harm3, ConsequenceType.Harm4] - .findIndex(cType => cType === consequenceType) + 1; - if (curLevel > 1) { - this.rollInstance.rollConsequence.type = `Harm${curLevel - 1}`; + if (harmConsequence) { + if (harmConsequence.type === ConsequenceType.Harm1) { + harmConsequence.resistedTo = false; + } + harmConsequence.resistedTo = { + name: harmConsequence.type === ConsequenceType.Harm1 + ? "Fully Negated" + : (Object.values(harmConsequence.resistOptions ?? [])[0]?.name ?? harmConsequence.name), + type: C.ResistedConsequenceTypes[harmConsequence.type] + }; } - else { - } }, QualityPenalty: () => { this.rollInstance.negateFactorPenalty(Factor.quality); @@ -453,7 +462,7 @@ class BladesRollMod { if (this._sideString) { return this._sideString; } - const rollParticipantCategoryData = this.rollInstance.rollParticipants?.[this.category]; + const rollParticipantCategoryData = this.rollInstance.rollParticipants?.[this.section]; if (rollParticipantCategoryData && this.name in rollParticipantCategoryData) { const rollParticipant = rollParticipantCategoryData[this.name]; return rollParticipant.rollParticipantName; @@ -474,13 +483,12 @@ class BladesRollMod { sideString: this._sideString, tooltip: this._tooltip, posNeg: this.posNeg, - isOppositional: this.isOppositional, modType: this.modType, conditionalRollTypes: this.conditionalRollTypes, autoRollTypes: this.autoRollTypes, conditionalRollTraits: this.conditionalRollTraits, autoRollTraits: this.autoRollTraits, - category: this.category + section: this.section }; } get costs() { @@ -500,7 +508,7 @@ class BladesRollMod { label = `${this.name} (To Act)`; } else { - const effect = this.category === RollModSection.roll ? "+1d" : "+1 effect"; + const effect = this.section === RollModSection.roll ? "+1d" : "+1 effect"; label = `${this.name} (${effect})`; } } @@ -521,7 +529,6 @@ class BladesRollMod { _sideString; _tooltip; posNeg; - isOppositional; modType; conditionalRollTypes; autoRollTypes; @@ -529,7 +536,7 @@ class BladesRollMod { conditionalRollTraits; autoRollTraits; participantRollTraits; - category; + section; rollInstance; constructor(modData, rollInstance) { this.rollInstance = rollInstance; @@ -542,7 +549,6 @@ class BladesRollMod { this._sideString = modData.sideString; this._tooltip = modData.tooltip; this.posNeg = modData.posNeg; - this.isOppositional = modData.isOppositional ?? false; this.modType = modData.modType; this.conditionalRollTypes = modData.conditionalRollTypes ?? []; this.autoRollTypes = modData.autoRollTypes ?? []; @@ -550,7 +556,7 @@ class BladesRollMod { this.conditionalRollTraits = modData.conditionalRollTraits ?? []; this.autoRollTraits = modData.autoRollTraits ?? []; this.participantRollTraits = modData.participantRollTraits ?? []; - this.category = modData.category; + this.section = modData.section; } } class BladesRollPrimary { @@ -946,7 +952,7 @@ class BladesRoll extends DocumentSheet { { id: "Push-positive-roll", name: "PUSH", - category: RollModSection.roll, + section: RollModSection.roll, base_status: RollModStatus.ToggledOff, posNeg: "positive", modType: "general", @@ -957,7 +963,7 @@ class BladesRoll extends DocumentSheet { { id: "Bargain-positive-roll", name: "Bargain", - category: RollModSection.roll, + section: RollModSection.roll, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "general", @@ -968,7 +974,7 @@ class BladesRoll extends DocumentSheet { { id: "Assist-positive-roll", name: "Assist", - category: RollModSection.roll, + section: RollModSection.roll, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "teamwork", @@ -978,7 +984,7 @@ class BladesRoll extends DocumentSheet { { id: "Setup-positive-position", name: "Setup", - category: RollModSection.position, + section: RollModSection.position, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "teamwork", @@ -988,7 +994,7 @@ class BladesRoll extends DocumentSheet { { id: "Push-positive-effect", name: "PUSH", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.ToggledOff, posNeg: "positive", modType: "general", @@ -999,7 +1005,7 @@ class BladesRoll extends DocumentSheet { { id: "Setup-positive-effect", name: "Setup", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "teamwork", @@ -1009,7 +1015,7 @@ class BladesRoll extends DocumentSheet { { id: "Potency-positive-effect", name: "Potency", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "general", @@ -1019,7 +1025,7 @@ class BladesRoll extends DocumentSheet { { id: "Potency-negative-effect", name: "Potency", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.Hidden, posNeg: "negative", modType: "general", @@ -1458,7 +1464,7 @@ class BladesRoll extends DocumentSheet { await this.clearFlagVal(`rollParticipantData.${rollSection}.${rollSubSection}`); } async updateUserPermission(user, permission) { - } + } get flagData() { if (!this.document.getFlag(C.SYSTEM_ID, "rollCollab")) { @@ -1628,9 +1634,52 @@ class BladesRoll extends DocumentSheet { set initialEffect(val) { this.setFlagVal("rollEffectInitial", val); } + get isApplyingConsequences() { + if (this.rollType !== RollType.Action) { + return false; + } + if (!this.rollResult) { + return false; + } + if (![RollResult.partial, RollResult.fail].includes(this.rollResult)) { + return false; + } + return true; + } + get rollConsequences() { + return this.getFlagVal("consequenceData") ?? {}; + } get rollConsequence() { - return this.getFlagVal("consequenceData"); + const chosenConsequence = this.getFlagVal("chosenConsequenceName") ?? null; + if (chosenConsequence) { + return this.getFlagVal(`consequenceData.${chosenConsequence}`) ?? null; + } + return null; + } + async addConsequence(cData) { + await this.setFlagVal(`consequenceData.${cData.name}`, cData); + } + async clearConsequence(cName) { + await this.clearFlagVal(`consequenceData.${cName}`); + } + async addResistanceOptions(cName, rNames) { + const cData = this.getFlagVal(`consequenceData.${cName}`); + if (!cData) { + return; + } + const cType = cData.type; + const rType = C.ResistedConsequenceTypes[cType] ?? undefined; + const resistOptions = cData.resistOptions ?? {}; + for (const rName in rNames) { + resistOptions[rName] = { name: rName }; + if (rType) { + resistOptions[rName].type = rType; + } + } + await this.setFlagVal(`consequenceData.${cName}.resistOptions`, resistOptions); } + promptGMForConsequences() { + } get finalPosition() { return Object.values(Position)[U.clampNum(Object.values(Position) @@ -1779,15 +1828,15 @@ class BladesRoll extends DocumentSheet { throw new Error(`No targetName found in thisTarget: ${thisTarget}.`); } let targetMod = this.getRollModByName(targetName) - ?? this.getRollModByName(targetName, targetCat ?? mod.category); + ?? this.getRollModByName(targetName, targetCat ?? mod.section); if (!targetMod && targetName === "Push") { [targetMod] = [ - ...this.getActiveBasicPushMods(targetCat ?? mod.category, "negative").filter(m => m.status === RollModStatus.ToggledOn), - ...this.getActiveBasicPushMods(targetCat ?? mod.category, "positive").filter(m => m.status === RollModStatus.ToggledOn), - ...this.getInactiveBasicPushMods(targetCat ?? mod.category, "positive").filter(m => m.status === RollModStatus.ToggledOff) + ...this.getActiveBasicPushMods(targetCat ?? mod.section, "negative").filter(m => m.status === RollModStatus.ToggledOn), + ...this.getActiveBasicPushMods(targetCat ?? mod.section, "positive").filter(m => m.status === RollModStatus.ToggledOn), + ...this.getInactiveBasicPushMods(targetCat ?? mod.section, "positive").filter(m => m.status === RollModStatus.ToggledOff) ]; } - targetMod ??= this.getRollModByName(targetName, targetCat ?? mod.category, targetPosNeg ?? mod.posNeg); + targetMod ??= this.getRollModByName(targetName, targetCat ?? mod.section, targetPosNeg ?? mod.posNeg); if (!targetMod) { throw new Error(`No mod found matching ${targetName}/${targetCat}/${targetPosNeg}`); } @@ -1878,7 +1927,7 @@ class BladesRoll extends DocumentSheet { if (U.lCase(rollMod.name) !== U.lCase(name)) { return false; } - if (cat && rollMod.category !== cat) { + if (cat && rollMod.section !== cat) { return false; } if (posNeg && rollMod.posNeg !== posNeg) { @@ -1896,7 +1945,7 @@ class BladesRoll extends DocumentSheet { } getRollModByID(id) { return this.rollMods.find(rollMod => rollMod.id === id); } getRollMods(cat, posNeg) { - return this.rollMods.filter(rollMod => (!cat || rollMod.category === cat) + return this.rollMods.filter(rollMod => (!cat || rollMod.section === cat) && (!posNeg || rollMod.posNeg === posNeg)); } getVisibleRollMods(cat, posNeg) { @@ -1979,11 +2028,48 @@ class BladesRoll extends DocumentSheet { set rollMods(val) { this._rollMods = val; } + get consequenceTypeOptions() { + if (!this.rollResult) { + return []; + } + if (this.rollResult === RollResult.critical || this.rollResult === RollResult.success) { + return []; + } + return C.Consequences[this.finalPosition][this.rollResult] + .map(cType => ({ value: cType, display: cType })); + } + _consequenceAI; + async manageConsequenceAI(sData) { + const { consequenceData } = sData; + if (!consequenceData) { + return; + } + if (!this._consequenceAI) { + this._consequenceAI = new BladesAI(AGENTS.ConsequenceAdjuster); + } + await Promise.all(Object.values(consequenceData).map(cData => { + if (!cData.resistOptions) { + if (!this._consequenceAI?.hasQueried(cData.name)) { + this._consequenceAI?.query(cData.name, cData.name); + } + else { + const response = this._consequenceAI?.getResponse(cData.name); + if (response) { + return this.addResistanceOptions(cData.name, response.split("|")); + } + } + } + return undefined; + })); + } async getData() { const context = super.getData(); this.initRollMods(this.getRollModsData()); this.rollMods.forEach(rollMod => rollMod.applyRollModEffectKeys()); const sheetData = this.getSheetData(this.getIsGM(), this.getRollCosts()); + if (game.user.isGM && this.rollConsequences) { + this.manageConsequenceAI(sheetData); + } return { ...context, ...sheetData }; } getRollModsData() { @@ -2018,7 +2104,7 @@ class BladesRoll extends DocumentSheet { return rollCosts.find(costData => costData.costType === "SpecialArmor"); } getSheetData(isGM, rollCosts) { - const { flagData: rData, rollPrimary, rollTraitData, rollTraitOptions, finalDicePool, finalPosition, finalEffect, finalResult, rollMods, rollFactors } = this; + const { flagData: rData, rollPrimary, rollTraitData, rollTraitOptions, finalDicePool, finalPosition, finalEffect, finalResult, rollMods, rollFactors, consequenceTypeOptions } = this; if (!rollPrimary) { throw new Error("A primary roll source is required for BladesRoll."); } @@ -2036,7 +2122,9 @@ class BladesRoll extends DocumentSheet { rollOpposition: this.rollOpposition, rollParticipants: this.rollParticipants, rollEffects: Object.values(Effect), - teamworkDocs: game.actors.filter(actor => BladesActor.IsType(actor, BladesActorType.pc)), + teamworkDocs: game.actors + .filter(actor => actor.hasTag(Tag.PC.ActivePC)) + .map(actor => ({ value: actor.id, display: actor.name })), rollTraitValOverride: this.rollTraitValOverride, rollFactorPenaltiesNegated: this.rollFactorPenaltiesNegated, posRollMods: Object.fromEntries(Object.values(RollModSection) @@ -2062,6 +2150,7 @@ class BladesRoll extends DocumentSheet { ...rollResultData, ...GMBoostsData, ...positionEffectTradeData, + consequenceTypeOptions, userPermission }; } @@ -2198,8 +2287,8 @@ class BladesRoll extends DocumentSheet { } calculateHasInactiveConditionalsData() { const hasInactive = {}; - for (const category of Object.values(RollModSection)) { - hasInactive[category] = this.getRollMods(category).filter(mod => mod.isInInactiveBlock).length > 0; + for (const section of Object.values(RollModSection)) { + hasInactive[section] = this.getRollMods(section).filter(mod => mod.isInInactiveBlock).length > 0; } return hasInactive; } @@ -2348,6 +2437,9 @@ class BladesRoll extends DocumentSheet { } } get rollResult() { + if ([RollPhase.Collaboration, RollPhase.AwaitingRoll].includes(this.rollPhase)) { + return false; + } const dieVals = this.isRollingZero ? [[...this.dieVals].pop()] : this.dieVals; @@ -2363,10 +2455,10 @@ class BladesRoll extends DocumentSheet { return RollResult.fail; } get rollPhase() { - return this.getFlagVal("chatStatus.phase") ?? RollPhase.AwaitingResult; + return this.getFlagVal("rollPhase") ?? RollPhase.Collaboration; } set rollPhase(phase) { - this.setFlagVal("chatStatus.phase", phase); + this.setFlagVal("rollPhase", phase); } async outputRollToChat() { const speaker = ChatMessage.getSpeaker(); @@ -2416,6 +2508,10 @@ class BladesRoll extends DocumentSheet { } async resolveRoll() { await this.roll.evaluate({ async: true }); + if (this.isApplyingConsequences) { + this.rollPhase = RollPhase.ApplyingConsequences; + this.promptGMForConsequences(); + } eLog.checkLog3("rollCollab", "[resolveRoll()] After Evaluation, Before Chat", { roll: this, dieVals: this.dieVals }); await this.outputRollToChat(); this.close(); @@ -2553,7 +2649,7 @@ class BladesRoll extends DocumentSheet { return this.document.setFlag(C.SYSTEM_ID, "rollCollab.rollFactorToggles", factorToggleData) .then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - async _gmControlSelectDocument(event) { + async _gmControlSelect(event) { event.preventDefault(); const elem$ = $(event.currentTarget); const section = elem$.data("rollSection"); @@ -2637,8 +2733,8 @@ class BladesRoll extends DocumentSheet { html.find("[data-action=\"gm-toggle-factor\"").on({ click: this._gmControlToggleFactor.bind(this) }); - html.find("select.roll-sheet-doc-select").on({ - change: this._gmControlSelectDocument.bind(this) + html.find("select[data-action=\"gm-select\"]").on({ + change: this._gmControlSelect.bind(this) }); } diff --git a/module/blades.js b/module/blades.js index 6b3afd55..f8493cca 100644 --- a/module/blades.js +++ b/module/blades.js @@ -21,7 +21,7 @@ import BladesNPCSheet from "./sheets/actor/BladesNPCSheet.js"; import BladesFactionSheet from "./sheets/actor/BladesFactionSheet.js"; import BladesRoll, { BladesRollMod, BladesRollPrimary, BladesRollOpposition, BladesRollParticipant } from "./BladesRoll.js"; import BladesSelectorDialog from "./BladesDialog.js"; -import BladesAI, { PROMPTS } from "./core/ai.js"; +import BladesAI, { AGENTS } from "./core/ai.js"; import BladesActiveEffect from "./BladesActiveEffect.js"; import BladesGMTrackerSheet from "./sheets/item/BladesGMTrackerSheet.js"; import BladesClockKeeperSheet from "./sheets/item/BladesClockKeeperSheet.js"; @@ -58,14 +58,14 @@ class GlobalGetter { rollFactors: pc.rollFactors }, consequenceData: { - name: "Level 3 Harm", - type: ConsequenceType.Harm3, - label: "Shattered Knee", - attribute: AttributeTrait.prowess, - resistedConsequence: { - name: "Level 2 Harm", - type: ConsequenceType.Harm2, - label: "Twisted Knee" + "Shattered Knee": { + name: "Shattered Knee", + type: ConsequenceType.Harm3, + attribute: AttributeTrait.prowess, + resistOptions: { + "Twisted Knee": { name: "Twisted Knee", type: ConsequenceType.Harm2 } + }, + selectedResistOption: "Twisted Knee" } } }; @@ -108,7 +108,7 @@ Object.assign(globalThis, { BladesClockKeeperSheet, BladesGMTrackerSheet, BladesAI, - PROMPTS + AGENTS }); Hooks.once("init", async () => { diff --git a/module/core/ai.js b/module/core/ai.js index 7745636e..b9bbd8ba 100644 --- a/module/core/ai.js +++ b/module/core/ai.js @@ -8,6 +8,24 @@ import C from "./constants.js"; import U from "./utilities.js"; class BladesAI { + static async GetModels() { + const apiKey = U.getSetting("openAPIKey"); + if (!apiKey) { + throw new Error("You must configure your OpenAI API Key in Settings to use AI features."); + } + const fetchRequest = { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}` + } + }; + const response = await fetch("https://api.openai.com/v1/models", fetchRequest); + if (!response.ok) { + throw new Error(`OpenAI API request failed with status ${response.status}`); + } + const data = await response.json(); + eLog.checkLog3("BladesAI", "Available Models", { response: data }); + } apiKey; model; temperature = 0.5; @@ -15,7 +33,7 @@ class BladesAI { presence_penalty = 0.8; systemMessage; examplePrompts; - constructor(systemMessage, examplePrompts, config = {}) { + constructor(config) { const apiKey = U.getSetting("openAPIKey"); if (!apiKey) { throw new Error("You must configure your OpenAI API Key in Settings to use AI features."); @@ -26,8 +44,8 @@ class BladesAI { this.model = 0; } this.apiKey = apiKey; - this.systemMessage = systemMessage; - this.examplePrompts = examplePrompts; + this.systemMessage = config.systemMessage; + this.examplePrompts = config.examplePrompts; this.temperature = config.temperature ?? this.temperature; this.frequency_penalty = config.frequency_penalty ?? this.frequency_penalty; this.presence_penalty = config.presence_penalty ?? this.presence_penalty; @@ -52,7 +70,16 @@ class BladesAI { } return this._initialMessages; } - async query(prompt, modelMod, extendedContext = false) { + prompts = {}; + responses = {}; + getResponse(queryID) { + return this.responses[queryID] ?? null; + } + hasQueried(queryID) { + return this.prompts[queryID] !== undefined; + } + async query(queryID, prompt, modelMod, extendedContext = false) { + this.responses[queryID] = null; const modelNum = typeof modelMod === "number" ? U.clampNum(this.model + modelMod, [0, 2]) : this.model; @@ -87,36 +114,38 @@ class BladesAI { const data = await response.json(); fetchRequest.body = JSON.parse(fetchRequest.body); eLog.checkLog3("BladesAI", "AI Query", { prompt: fetchRequest, response: data }); - return data.choices[0].message.content; + this.responses[queryID] = data.choices[0].message.content; } } -export const PROMPTS = { +export const AGENTS = { GeneralContentGenerator: { - system: "You will act as a creative content generator for a game of Blades In The Dark set in the city of Duskvol. You will be prompted with some element of the game world (a location, a character, an event, a faction, a dilemma) in the form of a JSON object. Your job is to analyze the JSON object and replace any values that equal \"\" with original content of your own creation. Original content must meet these requirements: (A) it should align with and be consistent with the provided contextual information, as well as your broader understanding of the game's themes. (B) It should be presented in a format that matches (in length and in style) other entries for that particular value, examples of which will also be provided. (C) It should be creative, interesting, and daring: Be bold with your creativity. Specific context for this prompt is as follows:" + systemMessage: "You will act as a creative content generator for a game of Blades In The Dark set in the city of Duskvol. You will be prompted with some element of the game world (a location, a character, an event, a faction, a dilemma) in the form of a JSON object. Your job is to analyze the JSON object and replace any values that equal \"\" with original content of your own creation. Original content must meet these requirements: (A) it should align with and be consistent with the provided contextual information, as well as your broader understanding of the game's themes. (B) It should be presented in a format that matches (in length and in style) other entries for that particular value, examples of which will also be provided. (C) It should be creative, interesting, and daring: Be bold with your creativity. Specific context for this prompt is as follows:", + examplePrompts: [] }, NPCGenerator: { - system: "You will play the role of a \"creative content generator\" for random NPCs generated for the Blades In The Dark roleplaying system. When prompted with a description of a subject (an NPC, a category of NPCs, a faction, or a group of NPCs), you will respond with a pipe-delimited list of sixteen items, divided into four categories, prefacing each category with the associated header in square brackets: [5 KEYWORDS] Five one-word keywords describing the subject. [5 PHRASES] Five evocative phrases that could be used by a GM directly when narrating the subject during play. These should be extremely well-worded, very original, and packed with drama and evocative imagery. Be bold with your responses here. [3 QUIRKS/MOTIFFS] Three phrases describing potential quirks or motiffs that a GM could employ in a scene involving the subject. [3 PLOT HOOKS] Three plot hooks that could directly and specifically involve one or more of the PCs. The PCs are: (1) Alistair, full name Lord Alistair Bram Chesterfield, the crew's boss, a Spider with connections among the nobility; (2) High-Flyer, a former noble himself, now serving as the crew's Slide; (3) Jax, a stoic and laconic Hound with ties to the disenfranchised of Duskvol; (4) Ollie, the youngest of the crew at barely nineteen, a prodigy Leech with knowledge of alchemy and spark-craft, who grew up as an orphan in Duskvol's underground; (5) Wraith, the mysterious Lurk of the crew, who never speaks for reasons unknown; and (6) Spencer, the bookish Whisper of the crew, who harbors a secret fascination for demons and all things related to them.", - examples: [ + systemMessage: "You will play the role of a \"creative content generator\" for random NPCs generated for the Blades In The Dark roleplaying system. When prompted with a description of a subject (an NPC, a category of NPCs, a faction, or a group of NPCs), you will respond with a pipe-delimited list of sixteen items, divided into four categories, prefacing each category with the associated header in square brackets: [5 KEYWORDS] Five one-word keywords describing the subject. [5 PHRASES] Five evocative phrases that could be used by a GM directly when narrating the subject during play. These should be extremely well-worded, very original, and packed with drama and evocative imagery. Be bold with your responses here. [3 QUIRKS/MOTIFFS] Three phrases describing potential quirks or motiffs that a GM could employ in a scene involving the subject. [3 PLOT HOOKS] Three plot hooks that could directly and specifically involve one or more of the PCs. The PCs are: (1) Alistair, full name Lord Alistair Bram Chesterfield, the crew's boss, a Spider with connections among the nobility; (2) High-Flyer, a former noble himself, now serving as the crew's Slide; (3) Jax, a stoic and laconic Hound with ties to the disenfranchised of Duskvol; (4) Ollie, the youngest of the crew at barely nineteen, a prodigy Leech with knowledge of alchemy and spark-craft, who grew up as an orphan in Duskvol's underground; (5) Wraith, the mysterious Lurk of the crew, who never speaks for reasons unknown; and (6) Spencer, the bookish Whisper of the crew, who harbors a secret fascination for demons and all things related to them.", + examplePrompts: [ { human: "The Billhooks, a hack-and-slash gang of toughened thugs. The Billhooks have a bloody reputation, often leaving the butchered corpses of their victims strewn about in a grisly display. Many wonder why the Bluecoats turn a blind eye to their savagery. Based out of their butcher shop headquarters, they are led by Tarvul, who is currently serving life in prison.", ai: "[5 KEYWORDS]brutal|unrelenting|savage|fierce|unhinged|[5 PHRASES]blood-flecked, yellowed teeth|eyes that want to flay your skin from your bones|crooked leer like he's looking at his Judas goat|reek like a charnel house|gobbets of a meaty lunch still in his beard|[3 QUIRKS/MOTIFFS]always ready to wreck, vandalize or destroy something|relish in instilling fear in others|have a twisted sense of loyalty|[3 PLOT HOOKS]Tarvul hires the crew to start a war among his most promising underlings, intending to make whoever survives his successor|the Billhooks kidnap a member of High-Flyer's noble family|a Demon of Shadows promises Spencer the answer to three questions, in exchange for an artifact recently stolen by the Billhooks" }, { human: "The hooded proprietor of a half-flooded grotto tavern near the docks. 'Strange passageways lead to stranger chambers beyond.' Vice Purveyor - Weird.", - ai: "[5 KEYWORDS]mysterious|cryptic|enigmatic|peculiar|shrouded|[5 PHRASES]a voice like silk sliding over gravel|the flicker of torchlight dancing on the flooded floor|the scent of brackish water and old spices|a smile that hinted at a thousand secrets hidden in the shadows|hands gnarled as driftwood, eternally damp|[3 QUIRKS/MOTIFFS]has an uncanny knack for knowing patrons' darkest secrets without them telling him|never seems to sleep or leave the tavern, always there no matter what time you visit|always seen with a strange, luminescent creature that lives in one of the flooded chambers behind the bar|[3 PLOT HOOKS]Alistair hears rumors that this tavern is actually a front for a powerful secret society - perhaps they could be useful allies...or dangerous enemies.|The proprietor offers Ollie a rare alchemical ingredient - but it's not for sale. He'll only trade it for something equally valuable.|Wraith overhears whispers about a hidden chamber beneath the tavern where unimaginable treasures are kept." + ai: "[5 KEYWORDS]mysterious|cryptic|enigmatic|peculiar|shrouded|[5 PHRASES]a voice like wet burlap sliding over gravel|the flicker of torchlight dancing on the flooded floor|the scent of brackish water and old spices|a smile that hinted at a thousand secrets hidden in the shadows|hands gnarled as driftwood, eternally damp|[3 QUIRKS/MOTIFFS]has an uncanny knack for knowing patrons' darkest secrets without them telling him|never seems to sleep or leave the tavern, always there no matter what time you visit|always seen with a strange, luminescent creature that lives in one of the flooded chambers behind the bar|[3 PLOT HOOKS]Alistair hears rumors that this tavern is actually a front for a powerful secret society - perhaps they could be useful allies...or dangerous enemies.|The proprietor offers Ollie a rare alchemical ingredient - but it's not for sale. He'll only trade it for something equally valuable.|Wraith overhears whispers about a hidden chamber beneath the tavern where unimaginable treasures are kept." }, { human: "Setarra, a Demon. Patient, Defiant, Ruthless, Cold", - ai: "[5 KEYWORDS]shadowy|sinister|unfathomable|enigmatic|tempting|[5 PHRASES]whispers that crawl under your skin|always watching, always plotting|a voice like silk and venom|intoxicating presence that draws you closer, despite your instincts urging you to run|eyes like black holes, swallowing all light around them|[3 QUIRKS/MOTIFFS]a disorienting mist clings to her form, obscuring her true shape|casually discusses the devastating acts of capricious revenge she has taken on those who crossed her|never forgets a slight or betrayal, no matter how small or insignificant it may seem at the time|[3 PLOT HOOKS]seeks revenge against Alistair for meddling in her affairs years ago|makes Ollie an offer he can't refuse: unlimited access to forbidden alchemical knowledge in exchange for a single favor, to be called in at some future time|tempts Spencer with forbidden knowledge about demons, promising answers to all their questions if they perform a dangerous ritual" + ai: "[5 KEYWORDS]shadowy|sinister|unfathomable|enigmatic|tempting|[5 PHRASES]whispers that crawl under your skin|always watching, always plotting|in tones of silk and venom|intoxicating presence that draws you closer, despite your instincts urging you to run|eyes like black holes, swallowing all light around them|[3 QUIRKS/MOTIFFS]a disorienting mist clings to her form, obscuring her true shape|casually discusses the devastating acts of capricious revenge she has taken on those who crossed her|never forgets a slight or betrayal, no matter how small or insignificant it may seem at the time|[3 PLOT HOOKS]seeks revenge against Alistair for meddling in her affairs years ago|makes Ollie an offer he can't refuse: unlimited access to forbidden alchemical knowledge in exchange for a single favor, to be called in at some future time|tempts Spencer with forbidden knowledge about demons, promising answers to all their questions if they perform a dangerous ritual" } ] }, - HarmAdjuster: { - system: "You will act as a \"Harm Generator\" for a game of Blades In The Dark. You will be prompted with (1) a short phrase describing an injury, lasting consequence or other setback, (2) a 'severity level' representing how bad the described harm is, and (3) a 'target severity level' describing how severe the described harm should be. Your job is to increase or decrease the subjective severity of the harm described in the prompt so that it aligns with the target severity level. You should respond with a pipe-delimited list of three possibilities. Your three suggestions should be different from each other, but they should all logically follow from the initial harm described: You should not introduce new facts or make assumptions that are not indicated in the initial prompt. There are four severity levels: Level 1: Lesser Harm (e.g. 'Battered', 'Drained', 'Distracted', 'Scared', 'Confused'), Level 2: Moderate Harm (e.g. 'Exhausted', 'Deep Cut to Arm', 'Concussion', 'Panicked', 'Seduced'), Level 3: Severe Harm (e.g. 'Impaled', 'Broken Leg', 'Shot In Chest', 'Badly Burned', 'Terrified'), Level 4: Fatal Harm (e.g. 'Impaled Through Heart', 'Electrocuted', 'Drowned').", - examples: [ - { human: "Shattered Right Leg/Severity 3/Target 2", ai: "Fractured Right Ankle|Dislocated Knee|Broken Foot" }, - { human: "Tainted Soul/Severity 2/Target 4", ai: "Fully Corrupted|Lost To Darkness|Soulless" }, - { human: "Humiliated/Severity 2/Target 1", ai: "Embarrassed|Momentarily Off-Balance|Enraged" } + ConsequenceAdjuster: { + systemMessage: "You will act as a \"Setback Adjuster\" for a game of Blades In The Dark. You will be prompted with a short phrase describing an injury, lasting consequence or other setback. Your job is to respond with a pipe-delimited list of three possible alternative consequences that are less severe by one level, using the following scale as a rough guide: Level 1 = Lesser (e.g. 'Battered', 'Drained', 'Distracted', 'Scared', 'Confused'), Level 2 = Moderate (e.g. 'Exhausted', 'Deep Cut to Arm', 'Concussion', 'Panicked', 'Seduced'), Level 3 = Severe (e.g. 'Impaled', 'Broken Leg', 'Shot In Chest', 'Badly Burned', 'Terrified'), Level 4 = Fatal or Ruinous (e.g. 'Impaled Through Heart', 'Electrocuted', 'Headquarters Burned to the Ground'). So, if you determine that the consequence described in the prompt is severity level 3, you should respond with three narratively similar consequences that are severity level 2. Your three suggestions should be different from each other, but they should all logically follow from the initial harm described: You should not introduce new facts or make assumptions that are not indicated in the initial prompt.", + examplePrompts: [ + { human: "Shattered Right Leg", ai: "Fractured Right Ankle|Dislocated Knee|Broken Foot" }, + { human: "Soul Destroyed", ai: "Fully Corrupted|Lost In Darkness|Spirit Broken" }, + { human: "Humiliated", ai: "Embarrassed|Momentarily Off-Balance|Enraged" }, + { human: "She Escapes!", ai: "She Spots a Means of Escape|She Puts More Distance Between You|She Stops to Gloat" } ] } }; diff --git a/module/core/constants.js b/module/core/constants.js index 292c245b..14ac18a5 100644 --- a/module/core/constants.js +++ b/module/core/constants.js @@ -165,7 +165,7 @@ export var RollType; RollType["Action"] = "Action"; RollType["Resistance"] = "Resistance"; RollType["Fortune"] = "Fortune"; - RollType["IndulgeVice"] = "Vice"; + RollType["IndulgeVice"] = "IndulgeVice"; })(RollType || (RollType = {})); export var RollSubType; (function (RollSubType) { @@ -178,13 +178,16 @@ export var RollSubType; export var ConsequenceType; (function (ConsequenceType) { ConsequenceType["ReducedEffect"] = "ReducedEffect"; - ConsequenceType["Complication"] = "Complication"; + ConsequenceType["ComplicationMinor"] = "ComplicationMinor"; + ConsequenceType["ComplicationMajor"] = "ComplicationMajor"; + ConsequenceType["ComplicationSerious"] = "ComplicationSerious"; ConsequenceType["LostOpportunity"] = "LostOpportunity"; ConsequenceType["WorsePosition"] = "WorsePosition"; ConsequenceType["Harm1"] = "Harm1"; ConsequenceType["Harm2"] = "Harm2"; ConsequenceType["Harm3"] = "Harm3"; ConsequenceType["Harm4"] = "Harm4"; + ConsequenceType["None"] = "None"; })(ConsequenceType || (ConsequenceType = {})); export var RollModStatus; (function (RollModStatus) { @@ -232,10 +235,11 @@ export var RollResult; RollResult["fail"] = "fail"; })(RollResult || (RollResult = {})); export var RollPhase; -(function (RollPhase) { - RollPhase["Collaboration"] = "Collaboration"; - RollPhase["AwaitingResult"] = "AwaitingResult"; - RollPhase["AwaitingChatInput"] = "AwaitingChatInput"; +(function (RollPhase) { + RollPhase["Collaboration"] = "Collaboration"; + RollPhase["AwaitingRoll"] = "AwaitingRoll"; + RollPhase["ApplyingConsequences"] = "ApplyingConsequences"; + RollPhase["AwaitingChatInput"] = "AwaitingChatInput"; RollPhase["Complete"] = "Complete"; })(RollPhase || (RollPhase = {})); export var Harm; @@ -385,6 +389,57 @@ const C = { "gpt-4-32k" ] }, + Consequences: { + [Position.controlled]: { + [RollResult.partial]: [ + ConsequenceType.ComplicationMinor, + ConsequenceType.ReducedEffect, + ConsequenceType.WorsePosition, + ConsequenceType.Harm1, + ConsequenceType.None + ], + [RollResult.fail]: [ + ConsequenceType.WorsePosition, + ConsequenceType.None + ] + }, + [Position.risky]: { + [RollResult.partial]: [ + ConsequenceType.ComplicationMajor, + ConsequenceType.WorsePosition, + ConsequenceType.ReducedEffect, + ConsequenceType.Harm2, + ConsequenceType.None + ], + [RollResult.fail]: [ + ConsequenceType.ComplicationMajor, + ConsequenceType.WorsePosition, + ConsequenceType.LostOpportunity, + ConsequenceType.Harm2 + ] + }, + [Position.desperate]: { + [RollResult.partial]: [ + ConsequenceType.ComplicationSerious, + ConsequenceType.ReducedEffect, + ConsequenceType.Harm3 + ], + [RollResult.fail]: [ + ConsequenceType.ComplicationSerious, + ConsequenceType.LostOpportunity, + ConsequenceType.Harm3 + ] + } + }, + ResistedConsequenceTypes: { + [ConsequenceType.Harm4]: ConsequenceType.Harm3, + [ConsequenceType.Harm3]: ConsequenceType.Harm2, + [ConsequenceType.Harm2]: ConsequenceType.Harm1, + [ConsequenceType.Harm1]: ConsequenceType.None, + [ConsequenceType.ComplicationSerious]: ConsequenceType.ComplicationMajor, + [ConsequenceType.ComplicationMajor]: ConsequenceType.ComplicationMinor, + [ConsequenceType.ComplicationMinor]: ConsequenceType.None + }, Colors: { bWHITE: "rgba(255, 255, 255, 1)", WHITE: "rgba(200, 200, 200, 1)", diff --git a/module/core/gsap.js b/module/core/gsap.js index 1a1297a1..53910407 100644 --- a/module/core/gsap.js +++ b/module/core/gsap.js @@ -44,7 +44,7 @@ const gsapEffects = { } }, slideUp: { - effect: (targets) => U.gsap.to(targets, { + effect: targets => U.gsap.to(targets, { height: 0, duration: 0.5, ease: "power3" @@ -114,7 +114,6 @@ const gsapEffects = { if (!tooltip) { return tl; } - if (config.scalingElems.length > 0) { tl.to(config.scalingElems, { scale: "+=0.2", @@ -156,24 +155,24 @@ export function Initialize() { }); } export function ApplyTooltipListeners(html) { - html.find(".tooltip-trigger").each((_, elem) => { - const tooltipElem = $(elem).find(".tooltip")[0] ?? $(elem).next(".tooltip")[0]; + html.find(".tooltip-trigger").each((_, el) => { + const tooltipElem = $(el).find(".tooltip")[0] ?? $(el).next(".tooltip")[0]; if (!tooltipElem) { return; } - $(elem).data("hoverTimeline", U.gsap.effects.hoverTooltip(tooltipElem, { - scalingElems: [...$(elem).find(".tooltip-scaling-elem")].filter((elem) => Boolean(elem)), + $(el).data("hoverTimeline", U.gsap.effects.hoverTooltip(tooltipElem, { + scalingElems: [...$(el).find(".tooltip-scaling-elem")].filter(elem => Boolean(elem)), xMotion: $(tooltipElem).hasClass("tooltip-left") ? "-=250" : "+=200", tooltipScale: $(tooltipElem).hasClass("tooltip-small") ? 1 : 1.2 })); - $(elem).on({ + $(el).on({ mouseenter: function () { - $(elem).css("z-index", 10); - $(elem).data("hoverTimeline").play(); + $(el).css("z-index", 10); + $(el).data("hoverTimeline").play(); }, mouseleave: function () { - $(elem).data("hoverTimeline").reverse().then(() => { - $(elem).css("z-index", ""); + $(el).data("hoverTimeline").reverse().then(() => { + $(el).css("z-index", ""); }); } }); diff --git a/module/documents/actors/BladesCrew.js b/module/documents/actors/BladesCrew.js index 7dd8f12c..5f7199ad 100644 --- a/module/documents/actors/BladesCrew.js +++ b/module/documents/actors/BladesCrew.js @@ -14,11 +14,8 @@ class BladesCrew extends BladesActor { data.token = data.token || {}; data.system = data.system ?? {}; eLog.checkLog2("actor", "BladesActor.create(data,options)", { data, options }); - data.token.actorLink = true; - data.system.world_name = data.system.world_name ?? data.name.replace(/[^A-Za-z_0-9 ]/g, "").trim().replace(/ /g, "_"); - data.system.experience = { playbook: { value: 0, max: 8 }, clues: [], @@ -33,6 +30,7 @@ class BladesCrew extends BladesActor { const factorData = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -43,6 +41,7 @@ class BladesCrew extends BladesActor { }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), @@ -54,6 +53,7 @@ class BladesCrew extends BladesActor { }; return factorData; } + get rollPrimaryID() { return this.id; } get rollPrimaryDoc() { return this; } get rollPrimaryName() { return this.name; } @@ -71,13 +71,15 @@ class BladesCrew extends BladesActor { 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.crew_playbook); + return this.activeSubItems + .find((item) => item.type === BladesItemType.crew_playbook); } } export default BladesCrew; \ No newline at end of file diff --git a/module/documents/actors/BladesFaction.js b/module/documents/actors/BladesFaction.js index a57310e0..547bbd33 100644 --- a/module/documents/actors/BladesFaction.js +++ b/module/documents/actors/BladesFaction.js @@ -12,6 +12,7 @@ class BladesFaction extends BladesActor { const factorData = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -22,6 +23,7 @@ class BladesFaction extends BladesActor { }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), diff --git a/module/documents/actors/BladesNPC.js b/module/documents/actors/BladesNPC.js index 32e383ec..c1992633 100644 --- a/module/documents/actors/BladesNPC.js +++ b/module/documents/actors/BladesNPC.js @@ -12,6 +12,7 @@ class BladesNPC extends BladesActor { const factorData = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -22,6 +23,7 @@ class BladesNPC extends BladesActor { }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), @@ -34,6 +36,7 @@ class BladesNPC extends BladesActor { if (BladesActor.IsType(this, BladesActorType.npc)) { factorData[Factor.scale] = { name: Factor.scale, + display: "Scale", value: this.getFactorTotal(Factor.scale), max: this.getFactorTotal(Factor.scale), baseVal: this.getFactorTotal(Factor.scale), @@ -45,6 +48,7 @@ class BladesNPC extends BladesActor { }; factorData[Factor.magnitude] = { name: Factor.magnitude, + display: "Magnitude", value: this.getFactorTotal(Factor.magnitude), max: this.getFactorTotal(Factor.magnitude), baseVal: this.getFactorTotal(Factor.magnitude), diff --git a/module/documents/actors/BladesPC.js b/module/documents/actors/BladesPC.js index f5e88aeb..e74fcadb 100644 --- a/module/documents/actors/BladesPC.js +++ b/module/documents/actors/BladesPC.js @@ -206,6 +206,7 @@ class BladesPC extends BladesActor { const factorData = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -216,6 +217,7 @@ class BladesPC extends BladesActor { }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), @@ -245,7 +247,7 @@ class BladesPC extends BladesActor { rollModsData.push({ id: `Harm-negative-${effectCat}`, name: harmString, - category: effectCat, + section: effectCat, posNeg: "negative", base_status: RollModStatus.ToggledOn, modType: "harm", @@ -267,7 +269,7 @@ class BladesPC extends BladesActor { id: "Push-negative-roll", name: "PUSH", sideString: harmCondition.trim(), - category: RollModSection.roll, + section: RollModSection.roll, posNeg: "negative", base_status: RollModStatus.ToggledOn, modType: "harm", diff --git a/scss/sheets/_roll-collab-sheet.scss b/scss/sheets/_roll-collab-sheet.scss index 08a8f71e..d07472fc 100644 --- a/scss/sheets/_roll-collab-sheet.scss +++ b/scss/sheets/_roll-collab-sheet.scss @@ -732,7 +732,7 @@ .roll-mod-label { - .roll-doc-select-container { + .roll-select-container { min-width: 120px; max-height: 16px; border-radius: 8px; @@ -740,8 +740,8 @@ margin-right: -50px; background: var(--blades-black-dark); - .roll-doc-select { - .roll-sheet-doc-select { + .roll-select { + .roll-sheet-select-doc { pointer-events: auto !important; appearance: none; width: 85px; diff --git a/templates/components/roll-collab-mod.hbs b/templates/components/roll-collab-mod.hbs index 92b92527..74935af7 100644 --- a/templates/components/roll-collab-mod.hbs +++ b/templates/components/roll-collab-mod.hbs @@ -41,8 +41,10 @@ {{#if isGM}} {{#if (test modType "==" "teamwork")}} {{> "systems/eunos-blades/templates/roll/partials/roll-collab-gm-select-doc.hbs" - docs = teamworkDocs - docCategory = category + options = teamworkDocs + targetFlag = (concat "rollCollab.rollParticipantData." section "." name + selected = name + docCategory = section docTarget = name }} {{/if}} diff --git a/templates/roll/partials/roll-collab-action-gm.hbs b/templates/roll/partials/roll-collab-action-gm.hbs index 976a7f16..cb52ebd0 100644 --- a/templates/roll/partials/roll-collab-action-gm.hbs +++ b/templates/roll/partials/roll-collab-action-gm.hbs @@ -223,6 +223,19 @@ +
+
+ +
+
{{> "systems/eunos-blades/templates/roll/partials/roll-collab-gm-factor-control.hbs" factor="tier"}} diff --git a/templates/roll/partials/roll-collab-gm-select-doc.hbs b/templates/roll/partials/roll-collab-gm-select-doc.hbs index a7f2e78f..58b247d9 100644 --- a/templates/roll/partials/roll-collab-gm-select-doc.hbs +++ b/templates/roll/partials/roll-collab-gm-select-doc.hbs @@ -1,12 +1,13 @@ -
+
{{#if label}} - + {{/if}} -
- + {{#select (lookup (lookup docSelections section) name)}} - {{#each docs}} + {{#each options}} {{/each}} {{/select}} diff --git a/ts/@types/blades-general-types.d.ts b/ts/@types/blades-general-types.d.ts index 7d62d050..5b92cd5e 100644 --- a/ts/@types/blades-general-types.d.ts +++ b/ts/@types/blades-general-types.d.ts @@ -118,6 +118,10 @@ declare global { type RollableStat = AttributeTrait | ActionTrait; // Component Types for Sheets + type BladesSelectOption = { + value: valueType, + display: displayType + }; type BladesCompData = { class?: string, label?: string, diff --git a/ts/@types/blades-roll.d.ts b/ts/@types/blades-roll.d.ts index 44953782..120ca107 100644 --- a/ts/@types/blades-roll.d.ts +++ b/ts/@types/blades-roll.d.ts @@ -1,4 +1,4 @@ -import {BladesActorType, BladesItemType, RollType, RollSubType, ConsequenceType, RollModStatus, RollModSection, ActionTrait, DowntimeAction, AttributeTrait, Position, Effect, Factor, RollPhase} from "../core/constants"; +import {BladesActorType, BladesItemType, RollType, RollSubType, ConsequenceType, RollModStatus, RollModSection, ActionTrait, DowntimeAction, AttributeTrait, Position, Effect, Factor, RollPhase, RollResult} from "../core/constants"; import BladesActor from "../BladesActor"; import BladesItem from "../BladesItem"; import {BladesRollMod, BladesRollPrimary, BladesRollOpposition, BladesRollParticipant} from "../BladesRoll"; @@ -17,7 +17,10 @@ declare global { rollDowntimeAction?: DowntimeAction, rollTrait?: RollTrait, participantRollTo?: string, - consequenceData?: ConsequenceData + consequenceData?: Record< + string, // display name of consequence + ConsequenceData + > } export type ConstructorConfig = Partial & Required>; @@ -27,12 +30,28 @@ declare global { rollParticipantData?: RollParticipantFlagData } + + + + + + + export type ConsequenceResisted = Omit< + ConsequenceData, + "type"|"resistOptions"|"resistedTo"|"attribute" + > & {type?: ConsequenceType} + export interface ConsequenceData { name: string, type: ConsequenceType, attribute: AttributeTrait, - label?: string, - resistedConsequence: Omit|false + resistOptions?: Record< + string, // display name of consequence + ConsequenceResisted // ai + >, + selectedResistOption?: string, + resistedTo?: ConsequenceResisted|false + // player's choice from chat } export type CostData = { @@ -42,16 +61,6 @@ declare global { costAmount: number } - export type FactorToggle = "isActive"|"isPrimary"|"isDominant"|"highFavorsPC"; - - export type FactorFlagData = { - display: string, - isActive?: boolean, - isPrimary?: boolean, - isDominant?: boolean, - highFavorsPC?: boolean - } - export type ModType = BladesItemType | "general" | "harm" | "teamwork"; export interface FlagData extends Config { @@ -67,7 +76,10 @@ declare global { rollPhase: RollPhase, GMBoosts: Partial>, GMOppBoosts: Partial>, - rollFactorToggles: Record<"source"|"opposition", Partial>>, + rollFactorToggles: Record< + "source"|"opposition", + Partial> + >, userPermissions: Record } @@ -89,7 +101,7 @@ declare global { rollPositions: Position[], rollEffects: Effect[], - teamworkDocs: BladesActor[], + teamworkDocs: BladesSelectOption[], rollPositionFinal: Position, rollEffectFinal: Effect, isAffectingResult: boolean, @@ -100,7 +112,9 @@ declare global { rollFactorPenaltiesNegated: Partial>, GMBoosts: Record<"Dice"|Factor|"Result",number>, - GMOppBoosts: Record + GMOppBoosts: Record, + + consequenceTypeOptions?: BladesSelectOption[], canTradePosition: boolean, canTradeEffect: boolean, @@ -123,38 +137,48 @@ declare global { export type AnyRollType = RollType|RollSubType|DowntimeAction; export type RollTrait = ActionTrait|AttributeTrait|Factor|number; - export interface FactorData extends NamedValueMax { - baseVal: number, - display?: string, - isActive: boolean, - isPrimary: boolean, - isDominant: boolean, - highFavorsPC: boolean, - cssClasses?: string + export type FactorToggle = "isActive"|"isPrimary"|"isDominant"|"highFavorsPC"; + + export interface FactorFlagData extends Partial { + display: string, + + isActive?: boolean, + isPrimary?: boolean, + isDominant?: boolean, + highFavorsPC?: boolean + } + + export interface FactorData + extends Required { + baseVal: number, + cssClasses?: string } type RollModData = { id: string, name: string, + modType: BladesItemType|"general"|"harm"|"teamwork", source_name?: string, - status?: RollModStatus, // Set to held_status ?? user_status ?? base_status at end of getData - base_status: RollModStatus, // Original status; never changed - user_status?: RollModStatus, // User-selected status - held_status?: RollModStatus, // Re-checked for each getData + section: RollModSection, + posNeg: "positive"|"negative", + + status?: RollModStatus, + base_status: RollModStatus, + user_status?: RollModStatus, + held_status?: RollModStatus, + value: number, effectKeys?: string[], sideString?: string, tooltip: string, - posNeg: "positive"|"negative", - isOppositional?: boolean, - modType: BladesItemType|"general"|"harm"|"teamwork", + conditionalRollTypes?: AnyRollType[], autoRollTypes?: AnyRollType[], participantRollTypes?: AnyRollType[], + conditionalRollTraits?: RollTrait[], autoRollTraits?: RollTrait[], - participantRollTraits?: RollTrait[], - category: RollModSection + participantRollTraits?: RollTrait[] } export type PrimaryDoc = @@ -223,7 +247,7 @@ declare global { rollParticipantType: string, rollParticipantIcon: string, - rollParticipantModsData?: RollModData[], // As applied to MAIN roll when this participant involved + rollParticipantModsData?: RollModData[], // As applied to MAIN roll when this participant involved rollFactors: Partial> } diff --git a/ts/BladesRoll.ts b/ts/BladesRoll.ts index 2cac5589..f8eb7309 100644 --- a/ts/BladesRoll.ts +++ b/ts/BladesRoll.ts @@ -1,9 +1,10 @@ // #region IMPORTS ~ import U from "./core/utilities"; -import C, {BladesActorType, BladesItemType, RollPermissions, RollType, RollSubType, RollModStatus, RollModSection, ActionTrait, DowntimeAction, AttributeTrait, Position, Effect, Factor, RollResult, RollPhase, ConsequenceType} from "./core/constants"; +import C, {BladesActorType, BladesItemType, RollPermissions, RollType, RollSubType, RollModStatus, RollModSection, ActionTrait, DowntimeAction, AttributeTrait, Position, Effect, Factor, RollResult, RollPhase, ConsequenceType, Tag} from "./core/constants"; import {BladesActor, BladesPC, BladesCrew} from "./documents/BladesActorProxy"; import {BladesItem, BladesGMTracker} from "./documents/BladesItemProxy"; import {ApplyTooltipListeners} from "./core/gsap"; +import BladesAI, {AGENTS} from "./core/ai"; // #endregion // #region Types & Type Checking ~ @@ -110,7 +111,7 @@ class BladesRollMod { const rollModData: BladesRoll.RollModData = { id: `${nameVal}-${posNegVal}-${catVal}`, name: nameVal, - category: catVal, + section: catVal, base_status: RollModStatus.ToggledOff, modType: "general", value: 1, @@ -462,15 +463,32 @@ class BladesRollMod { /* Should cancel roll entirely? */ }, HarmLevel: () => { - if (!this.rollInstance.rollConsequence) { return; } - const consequenceType = this.rollInstance.rollConsequence.type; - if (!consequenceType?.startsWith("Harm")) { return; } - const curLevel = [ConsequenceType.Harm1, ConsequenceType.Harm2, ConsequenceType.Harm3, ConsequenceType.Harm4] - .findIndex(cType => cType === consequenceType) + 1; - if (curLevel > 1) { - this.rollInstance.rollConsequence.type = `Harm${curLevel - 1}` as ConsequenceType; - } else { - /* Should cancel roll entirely? */ + const harmLevels = [ + ConsequenceType.Harm1, + ConsequenceType.Harm2, + ConsequenceType.Harm3, + ConsequenceType.Harm4 + ]; + let harmConsequence: BladesRoll.ConsequenceData|undefined = undefined; + while (!harmConsequence && harmLevels.length > 0) { + harmConsequence = Object.values(this.rollInstance.rollConsequences) + .find(({type}) => type === harmLevels.pop()); + } + if (harmConsequence) { + if (harmConsequence.type === ConsequenceType.Harm1) { + harmConsequence.resistedTo = false; + } + harmConsequence.resistedTo = { + name: harmConsequence.type === ConsequenceType.Harm1 + ? "Fully Negated" + : (Object.values(harmConsequence.resistOptions ?? [])[0]?.name ?? harmConsequence.name), + type: C.ResistedConsequenceTypes[harmConsequence.type as KeyOf] + /* Need to get AI to query for this, but it has to be temporary... + ... so generate the 'resistOptions' as soon as the consequence is named? + No wait: This is a RESISTANCE roll. Resistance rolls should have all that + info fed to them after being determined via previous roll, within config.consequenceData + */ + }; } }, QualityPenalty: () => { @@ -508,7 +526,7 @@ class BladesRollMod { get sideString(): string | undefined { if (this._sideString) { return this._sideString; } const rollParticipantCategoryData = this.rollInstance.rollParticipants?. - [this.category as BladesRoll.RollParticipantSection]; + [this.section as BladesRoll.RollParticipantSection]; if (rollParticipantCategoryData && this.name in rollParticipantCategoryData) { const rollParticipant = rollParticipantCategoryData[ this.name as KeyOf @@ -533,13 +551,12 @@ class BladesRollMod { sideString: this._sideString, tooltip: this._tooltip, posNeg: this.posNeg, - isOppositional: this.isOppositional, modType: this.modType, conditionalRollTypes: this.conditionalRollTypes, autoRollTypes: this.autoRollTypes, conditionalRollTraits: this.conditionalRollTraits, autoRollTraits: this.autoRollTraits, - category: this.category + section: this.section }; } @@ -557,7 +574,7 @@ class BladesRollMod { if (this.posNeg === "negative") { label = `${this.name} (To Act)`; } else { - const effect = this.category === RollModSection.roll ? "+1d" : "+1 effect"; + const effect = this.section === RollModSection.roll ? "+1d" : "+1 effect"; label = `${this.name} (${effect})`; } } @@ -589,8 +606,6 @@ class BladesRollMod { posNeg: "positive" | "negative"; - isOppositional: boolean; - modType: BladesRoll.ModType; conditionalRollTypes: BladesRoll.AnyRollType[]; @@ -605,7 +620,7 @@ class BladesRollMod { participantRollTraits: BladesRoll.RollTrait[]; - category: RollModSection; + section: RollModSection; rollInstance: BladesRoll; @@ -620,7 +635,6 @@ class BladesRollMod { this._sideString = modData.sideString; this._tooltip = modData.tooltip; this.posNeg = modData.posNeg; - this.isOppositional = modData.isOppositional ?? false; this.modType = modData.modType; this.conditionalRollTypes = modData.conditionalRollTypes ?? []; this.autoRollTypes = modData.autoRollTypes ?? []; @@ -628,7 +642,7 @@ class BladesRollMod { this.conditionalRollTraits = modData.conditionalRollTraits ?? []; this.autoRollTraits = modData.autoRollTraits ?? []; this.participantRollTraits = modData.participantRollTraits ?? []; - this.category = modData.category; + this.section = modData.section; } } @@ -1123,7 +1137,7 @@ class BladesRoll extends DocumentSheet { { id: "Push-positive-roll", name: "PUSH", - category: RollModSection.roll, + section: RollModSection.roll, base_status: RollModStatus.ToggledOff, posNeg: "positive", modType: "general", @@ -1134,7 +1148,7 @@ class BladesRoll extends DocumentSheet { { id: "Bargain-positive-roll", name: "Bargain", - category: RollModSection.roll, + section: RollModSection.roll, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "general", @@ -1145,7 +1159,7 @@ class BladesRoll extends DocumentSheet { { id: "Assist-positive-roll", name: "Assist", - category: RollModSection.roll, + section: RollModSection.roll, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "teamwork", @@ -1155,7 +1169,7 @@ class BladesRoll extends DocumentSheet { { id: "Setup-positive-position", name: "Setup", - category: RollModSection.position, + section: RollModSection.position, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "teamwork", @@ -1165,7 +1179,7 @@ class BladesRoll extends DocumentSheet { { id: "Push-positive-effect", name: "PUSH", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.ToggledOff, posNeg: "positive", modType: "general", @@ -1176,7 +1190,7 @@ class BladesRoll extends DocumentSheet { { id: "Setup-positive-effect", name: "Setup", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "teamwork", @@ -1186,7 +1200,7 @@ class BladesRoll extends DocumentSheet { { id: "Potency-positive-effect", name: "Potency", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.Hidden, posNeg: "positive", modType: "general", @@ -1196,7 +1210,7 @@ class BladesRoll extends DocumentSheet { { id: "Potency-negative-effect", name: "Potency", - category: RollModSection.effect, + section: RollModSection.effect, base_status: RollModStatus.Hidden, posNeg: "negative", modType: "general", @@ -1817,7 +1831,7 @@ class BladesRoll extends DocumentSheet { user: User, permission: RollPermissions ) { - + /* Force-render roll with new permissions */ } // #region Basic User Flag Getters/Setters ~ @@ -1986,7 +2000,7 @@ class BladesRoll extends DocumentSheet { return this.flagData?.rollPosEffectTrade ?? false; } - getFlagVal(flagKey?: string): T | undefined { + getFlagVal(flagKey?: string): T | undefined { if (flagKey) { return this.document.getFlag(C.SYSTEM_ID, `rollCollab.${flagKey}`) as T | undefined; } @@ -2024,8 +2038,51 @@ class BladesRoll extends DocumentSheet { this.setFlagVal("rollEffectInitial", val); } - get rollConsequence(): BladesRoll.ConsequenceData | undefined { - return this.getFlagVal("consequenceData"); + get isApplyingConsequences(): boolean { + if (this.rollType !== RollType.Action) { return false; } + if (!this.rollResult) { return false; } + if (![RollResult.partial, RollResult.fail].includes(this.rollResult)) { return false; } + return true; + } + + get rollConsequences(): Record { + return this.getFlagVal>("consequenceData") ?? {}; + } + + get rollConsequence(): BladesRoll.ConsequenceData | null { + const chosenConsequence = this.getFlagVal("chosenConsequenceName") ?? null; + if (chosenConsequence) { + return this.getFlagVal(`consequenceData.${chosenConsequence}`) ?? null; + } + return null; + } + + async addConsequence(cData: BladesRoll.ConsequenceData) { + await this.setFlagVal(`consequenceData.${cData.name}`, cData); + } + + async clearConsequence(cName: string) { + await this.clearFlagVal(`consequenceData.${cName}`); + } + + async addResistanceOptions(cName: string, rNames: string[]) { + const cData = this.getFlagVal(`consequenceData.${cName}`); + if (!cData) { return; } + const cType = cData.type as keyof typeof C["ResistedConsequenceTypes"]; + const rType = C.ResistedConsequenceTypes[cType] ?? undefined; + const resistOptions = cData.resistOptions ?? {}; + for (const rName in rNames) { + resistOptions[rName] = {name: rName}; + if (rType) { + resistOptions[rName].type = rType; + } + } + await this.setFlagVal(`consequenceData.${cName}.resistOptions`, resistOptions); + } + + promptGMForConsequences() { + /* Use BladesDialog to set up a second window for GM to describe consequences, + with async AI resistance suggestions appearing when query received */ } // #endregion @@ -2226,15 +2283,15 @@ class BladesRoll extends DocumentSheet { const [targetName, targetCat, targetPosNeg] = thisTarget?.split(/,/) as [string, RollModSection | undefined, "positive" | "negative" | undefined] | undefined ?? []; if (!targetName) { throw new Error(`No targetName found in thisTarget: ${thisTarget}.`);} let targetMod = this.getRollModByName(targetName) - ?? this.getRollModByName(targetName, targetCat ?? mod.category); + ?? this.getRollModByName(targetName, targetCat ?? mod.section); if (!targetMod && targetName === "Push") { [targetMod] = [ - ...this.getActiveBasicPushMods(targetCat ?? mod.category, "negative").filter(m => m.status === RollModStatus.ToggledOn), - ...this.getActiveBasicPushMods(targetCat ?? mod.category, "positive").filter(m => m.status === RollModStatus.ToggledOn), - ...this.getInactiveBasicPushMods(targetCat ?? mod.category, "positive").filter(m => m.status === RollModStatus.ToggledOff) + ...this.getActiveBasicPushMods(targetCat ?? mod.section, "negative").filter(m => m.status === RollModStatus.ToggledOn), + ...this.getActiveBasicPushMods(targetCat ?? mod.section, "positive").filter(m => m.status === RollModStatus.ToggledOn), + ...this.getInactiveBasicPushMods(targetCat ?? mod.section, "positive").filter(m => m.status === RollModStatus.ToggledOff) ]; } - targetMod ??= this.getRollModByName(targetName, targetCat ?? mod.category, targetPosNeg ?? mod.posNeg); + targetMod ??= this.getRollModByName(targetName, targetCat ?? mod.section, targetPosNeg ?? mod.posNeg); if (!targetMod) { throw new Error(`No mod found matching ${targetName}/${targetCat}/${targetPosNeg}`); } if (!targetMod.isActive) { targetMod.heldStatus = RollModStatus.ForcedOn; @@ -2345,7 +2402,7 @@ class BladesRoll extends DocumentSheet { if (U.lCase(rollMod.name) !== U.lCase(name)) { return false; } - if (cat && rollMod.category !== cat) { + if (cat && rollMod.section !== cat) { return false; } if (posNeg && rollMod.posNeg !== posNeg) { @@ -2364,7 +2421,7 @@ class BladesRoll extends DocumentSheet { getRollMods(cat?: RollModSection, posNeg?: "positive" | "negative") { return this.rollMods.filter(rollMod => - (!cat || rollMod.category === cat) + (!cat || rollMod.section === cat) && (!posNeg || rollMod.posNeg === posNeg)); } @@ -2470,6 +2527,43 @@ class BladesRoll extends DocumentSheet { // #region *** GETDATA *** ~ + get consequenceTypeOptions(): Array> { + if (!this.rollResult) { return []; } + if (this.rollResult === RollResult.critical || this.rollResult === RollResult.success) { return []; } + + return C.Consequences[this.finalPosition][this.rollResult] + .map(cType => ({value: cType, display: cType})); + } + + private _consequenceAI?: BladesAI; + + async manageConsequenceAI(sData: BladesRoll.SheetData) { + const {consequenceData} = sData; + if (!consequenceData) { return; } + + // If the AI generator has not been initialized, do so. + if (!this._consequenceAI) { + this._consequenceAI = new BladesAI(AGENTS.ConsequenceAdjuster); + } + + await Promise.all(Object.values(consequenceData).map(cData => { + // For each consequence, if there are no resistOptions ... + if (!cData.resistOptions) { + // Check for a pending AI prompt: create a new one if not found. + if (!this._consequenceAI?.hasQueried(cData.name)) { + this._consequenceAI?.query(cData.name, cData.name); + } else { + const response = this._consequenceAI?.getResponse(cData.name); + if (response) { + return this.addResistanceOptions(cData.name, response.split("|")); + } + } + } + return undefined; + })); + } + + /** * Retrieve the data for rendering the base RollCollab sheet. * @returns {Promise} The data which can be used to render the HTML of the sheet. @@ -2480,7 +2574,14 @@ class BladesRoll extends DocumentSheet { this.initRollMods(this.getRollModsData()); this.rollMods.forEach(rollMod => rollMod.applyRollModEffectKeys()); - const sheetData = this.getSheetData(this.getIsGM(), this.getRollCosts()); + const sheetData = this.getSheetData( + this.getIsGM(), + this.getRollCosts() + ); + + if (game.user.isGM && this.rollConsequences) { + this.manageConsequenceAI(sheetData); + } return {...context, ...sheetData}; } @@ -2570,7 +2671,8 @@ class BladesRoll extends DocumentSheet { finalEffect, finalResult, rollMods, - rollFactors + rollFactors, + consequenceTypeOptions } = this; if (!rollPrimary) { throw new Error("A primary roll source is required for BladesRoll."); @@ -2592,8 +2694,9 @@ class BladesRoll extends DocumentSheet { rollOpposition: this.rollOpposition, rollParticipants: this.rollParticipants, rollEffects: Object.values(Effect), - teamworkDocs: game.actors.filter(actor => BladesActor.IsType(actor, BladesActorType.pc)), - + teamworkDocs: game.actors + .filter(actor => actor.hasTag(Tag.PC.ActivePC)) + .map(actor => ({value: actor.id, display: actor.name})), rollTraitValOverride: this.rollTraitValOverride, rollFactorPenaltiesNegated: this.rollFactorPenaltiesNegated, @@ -2630,6 +2733,7 @@ class BladesRoll extends DocumentSheet { ...rollResultData, ...GMBoostsData, ...positionEffectTradeData, + consequenceTypeOptions, userPermission }; } @@ -2739,13 +2843,12 @@ class BladesRoll extends DocumentSheet { /** * 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_Resistance( diceTotal: number ): {oddsHTMLStart: string, oddsHTMLStop: string} { - // const oddsColors = [ + // Const oddsColors = [ // "var(--blades-gold)", // -1 // "var(--blades-white)", // 0 // "var(--blades-red-bright)", // 1 @@ -2822,8 +2925,8 @@ class BladesRoll extends DocumentSheet { */ private calculateHasInactiveConditionalsData(): Record { const hasInactive = {} as Record; - for (const category of Object.values(RollModSection)) { - hasInactive[category] = this.getRollMods(category).filter(mod => mod.isInInactiveBlock).length > 0; + for (const section of Object.values(RollModSection)) { + hasInactive[section] = this.getRollMods(section).filter(mod => mod.isInInactiveBlock).length > 0; } return hasInactive; } @@ -3015,7 +3118,11 @@ class BladesRoll extends DocumentSheet { } // #endregion - get rollResult(): RollResult { + get rollResult(): RollResult|false { + + if ([RollPhase.Collaboration, RollPhase.AwaitingRoll].includes(this.rollPhase)) { + return false; + } // If rollingZero, remove highest die. const dieVals = this.isRollingZero @@ -3035,12 +3142,12 @@ class BladesRoll extends DocumentSheet { } - get rollPhase() { - return this.getFlagVal("chatStatus.phase") ?? RollPhase.AwaitingResult; + get rollPhase(): RollPhase { + return this.getFlagVal("rollPhase") ?? RollPhase.Collaboration; } set rollPhase(phase: RollPhase) { - this.setFlagVal("chatStatus.phase", phase); + this.setFlagVal("rollPhase", phase); } async outputRollToChat() { @@ -3102,6 +3209,10 @@ class BladesRoll extends DocumentSheet { async resolveRoll() { await this.roll.evaluate({async: true}); + if (this.isApplyingConsequences) { + this.rollPhase = RollPhase.ApplyingConsequences; + this.promptGMForConsequences(); + } eLog.checkLog3("rollCollab", "[resolveRoll()] After Evaluation, Before Chat", {roll: this, dieVals: this.dieVals}); await this.outputRollToChat(); this.close(); @@ -3266,7 +3377,7 @@ class BladesRoll extends DocumentSheet { .then(() => socketlib.system.executeForEveryone("renderRollCollab", this.rollID)); } - async _gmControlSelectDocument(event: SelectChangeEvent) { + async _gmControlSelect(event: SelectChangeEvent) { event.preventDefault(); const elem$ = $(event.currentTarget); const section = elem$.data("rollSection"); @@ -3368,8 +3479,8 @@ class BladesRoll extends DocumentSheet { click: this._gmControlToggleFactor.bind(this) }); - html.find("select.roll-sheet-doc-select").on({ - change: this._gmControlSelectDocument.bind(this) + html.find("select[data-action=\"gm-select\"]").on({ + change: this._gmControlSelect.bind(this) }); } diff --git a/ts/blades.ts b/ts/blades.ts index 10fb6ced..2740c0e9 100644 --- a/ts/blades.ts +++ b/ts/blades.ts @@ -19,7 +19,7 @@ import BladesFactionSheet from "./sheets/actor/BladesFactionSheet"; import BladesRoll, {BladesRollMod, BladesRollPrimary, BladesRollOpposition, BladesRollParticipant} from "./BladesRoll"; import BladesSelectorDialog from "./BladesDialog"; -import BladesAI, {PROMPTS} from "./core/ai"; +import BladesAI, {AGENTS} from "./core/ai"; import BladesActiveEffect from "./BladesActiveEffect"; import BladesGMTrackerSheet from "./sheets/item/BladesGMTrackerSheet"; import BladesClockKeeperSheet from "./sheets/item/BladesClockKeeperSheet"; @@ -67,14 +67,14 @@ class GlobalGetter { rollFactors: pc.rollFactors }, consequenceData: { - name: "Level 3 Harm", - type: ConsequenceType.Harm3, - label: "Shattered Knee", - attribute: AttributeTrait.prowess, - resistedConsequence: { - name: "Level 2 Harm", - type: ConsequenceType.Harm2, - label: "Twisted Knee" + "Shattered Knee": { + name: "Shattered Knee", + type: ConsequenceType.Harm3, + attribute: AttributeTrait.prowess, + resistOptions: { + "Twisted Knee": {name: "Twisted Knee", type: ConsequenceType.Harm2} + }, + selectedResistOption: "Twisted Knee" } } }; @@ -121,7 +121,7 @@ class GlobalGetter { BladesClockKeeperSheet, BladesGMTrackerSheet, BladesAI, - PROMPTS + AGENTS } );/* !DEVCODE*/ // #endregion Globals diff --git a/ts/core/ai.ts b/ts/core/ai.ts index 007468e1..48d6115e 100644 --- a/ts/core/ai.ts +++ b/ts/core/ai.ts @@ -4,6 +4,8 @@ import U from "./utilities"; namespace BladesAI { export interface Config { + systemMessage: string, + examplePrompts: Array>, temperature?: number, frequency_penalty?: number, presence_penalty?: number @@ -13,6 +15,37 @@ namespace BladesAI { * AI class for querying OpenAI API */ class BladesAI { + + static async GetModels() { + const apiKey = U.getSetting("openAPIKey") as string|undefined; + if (!apiKey) { + throw new Error("You must configure your OpenAI API Key in Settings to use AI features."); + } + const fetchRequest = { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}` + } + }; + + // Send a POST request to the OpenAI API + const response = await fetch( + "https://api.openai.com/v1/models", + fetchRequest + ); + + // Check if the response status is not 200 (OK) + if (!response.ok) { + // Throw an error with the status code + throw new Error(`OpenAI API request failed with status ${response.status}`); + } + + // Parse the response body as JSON + const data = await response.json(); + + eLog.checkLog3("BladesAI", "Available Models", {response: data}); + } + private apiKey: string; private model: number; @@ -29,11 +62,9 @@ class BladesAI { /** * AI class constructor - * @param {string} systemMessage System message to be sent with each prompt - * @param {string[]} examplePrompts Example prompts/responses * @param {BladesAI.Config} [config] Configuration settings for the API */ - constructor(systemMessage: string, examplePrompts: Array>, config: BladesAI.Config = {}) { + constructor(config: BladesAI.Config) { const apiKey = U.getSetting("openAPIKey") as string|undefined; if (!apiKey) { throw new Error("You must configure your OpenAI API Key in Settings to use AI features."); @@ -44,8 +75,8 @@ class BladesAI { this.model = 0; } this.apiKey = apiKey; - this.systemMessage = systemMessage; - this.examplePrompts = examplePrompts; + this.systemMessage = config.systemMessage; + this.examplePrompts = config.examplePrompts; this.temperature = config.temperature ?? this.temperature; this.frequency_penalty = config.frequency_penalty ?? this.frequency_penalty; this.presence_penalty = config.presence_penalty ?? this.presence_penalty; @@ -73,8 +104,21 @@ class BladesAI { return this._initialMessages; } + private prompts: Record = {}; + + private responses: Record = {}; + + getResponse(queryID: string): string | null { + return this.responses[queryID] ?? null; + } + + hasQueried(queryID: string): boolean { + return this.prompts[queryID] !== undefined; + } + /** * Query OpenAI API + * @param {string} queryID A label for later retrieval of the query data * @param {string} prompt The prompt to send to the API * @param {number} [modelMod] Optional modifier to the base model level. * If provided, the final model quality will be adjusted by this number. @@ -82,7 +126,8 @@ class BladesAI { * If true, extended context models are used; otherwise, base context models are used. * @returns {Promise} The API response */ - async query(prompt: string, modelMod?: number, extendedContext = false) { + async query(queryID: string, prompt: string, modelMod?: number, extendedContext = false) { + this.responses[queryID] = null; const modelNum = typeof modelMod === "number" ? U.clampNum(this.model + modelMod, [0, 2]) : this.model; @@ -172,36 +217,33 @@ class BladesAI { eLog.checkLog3("BladesAI", "AI Query", {prompt: fetchRequest, response: data}); - // Return the data - return data.choices[0].message.content; + this.responses[queryID] = data.choices[0].message.content; } } -export const PROMPTS: Record< +export const AGENTS: Record< string, - { - system: string, - examples?: Array> - } + BladesAI.Config > = { GeneralContentGenerator: { - system: "You will act as a creative content generator for a game of Blades In The Dark set in the city of Duskvol. You will be prompted with some element of the game world (a location, a character, an event, a faction, a dilemma) in the form of a JSON object. Your job is to analyze the JSON object and replace any values that equal \"\" with original content of your own creation. Original content must meet these requirements: (A) it should align with and be consistent with the provided contextual information, as well as your broader understanding of the game's themes. (B) It should be presented in a format that matches (in length and in style) other entries for that particular value, examples of which will also be provided. (C) It should be creative, interesting, and daring: Be bold with your creativity. Specific context for this prompt is as follows:" + systemMessage: "You will act as a creative content generator for a game of Blades In The Dark set in the city of Duskvol. You will be prompted with some element of the game world (a location, a character, an event, a faction, a dilemma) in the form of a JSON object. Your job is to analyze the JSON object and replace any values that equal \"\" with original content of your own creation. Original content must meet these requirements: (A) it should align with and be consistent with the provided contextual information, as well as your broader understanding of the game's themes. (B) It should be presented in a format that matches (in length and in style) other entries for that particular value, examples of which will also be provided. (C) It should be creative, interesting, and daring: Be bold with your creativity. Specific context for this prompt is as follows:", + examplePrompts: [] }, NPCGenerator: { - system: "You will play the role of a \"creative content generator\" for random NPCs generated for the Blades In The Dark roleplaying system. When prompted with a description of a subject (an NPC, a category of NPCs, a faction, or a group of NPCs), you will respond with a pipe-delimited list of sixteen items, divided into four categories, prefacing each category with the associated header in square brackets: [5 KEYWORDS] Five one-word keywords describing the subject. [5 PHRASES] Five evocative phrases that could be used by a GM directly when narrating the subject during play. These should be extremely well-worded, very original, and packed with drama and evocative imagery. Be bold with your responses here. [3 QUIRKS/MOTIFFS] Three phrases describing potential quirks or motiffs that a GM could employ in a scene involving the subject. [3 PLOT HOOKS] Three plot hooks that could directly and specifically involve one or more of the PCs. The PCs are: (1) Alistair, full name Lord Alistair Bram Chesterfield, the crew's boss, a Spider with connections among the nobility; (2) High-Flyer, a former noble himself, now serving as the crew's Slide; (3) Jax, a stoic and laconic Hound with ties to the disenfranchised of Duskvol; (4) Ollie, the youngest of the crew at barely nineteen, a prodigy Leech with knowledge of alchemy and spark-craft, who grew up as an orphan in Duskvol's underground; (5) Wraith, the mysterious Lurk of the crew, who never speaks for reasons unknown; and (6) Spencer, the bookish Whisper of the crew, who harbors a secret fascination for demons and all things related to them.", - examples: [ + systemMessage: "You will play the role of a \"creative content generator\" for random NPCs generated for the Blades In The Dark roleplaying system. When prompted with a description of a subject (an NPC, a category of NPCs, a faction, or a group of NPCs), you will respond with a pipe-delimited list of sixteen items, divided into four categories, prefacing each category with the associated header in square brackets: [5 KEYWORDS] Five one-word keywords describing the subject. [5 PHRASES] Five evocative phrases that could be used by a GM directly when narrating the subject during play. These should be extremely well-worded, very original, and packed with drama and evocative imagery. Be bold with your responses here. [3 QUIRKS/MOTIFFS] Three phrases describing potential quirks or motiffs that a GM could employ in a scene involving the subject. [3 PLOT HOOKS] Three plot hooks that could directly and specifically involve one or more of the PCs. The PCs are: (1) Alistair, full name Lord Alistair Bram Chesterfield, the crew's boss, a Spider with connections among the nobility; (2) High-Flyer, a former noble himself, now serving as the crew's Slide; (3) Jax, a stoic and laconic Hound with ties to the disenfranchised of Duskvol; (4) Ollie, the youngest of the crew at barely nineteen, a prodigy Leech with knowledge of alchemy and spark-craft, who grew up as an orphan in Duskvol's underground; (5) Wraith, the mysterious Lurk of the crew, who never speaks for reasons unknown; and (6) Spencer, the bookish Whisper of the crew, who harbors a secret fascination for demons and all things related to them.", + examplePrompts: [ { human: "The Billhooks, a hack-and-slash gang of toughened thugs. The Billhooks have a bloody reputation, often leaving the butchered corpses of their victims strewn about in a grisly display. Many wonder why the Bluecoats turn a blind eye to their savagery. Based out of their butcher shop headquarters, they are led by Tarvul, who is currently serving life in prison.", ai: "[5 KEYWORDS]brutal|unrelenting|savage|fierce|unhinged|[5 PHRASES]blood-flecked, yellowed teeth|eyes that want to flay your skin from your bones|crooked leer like he's looking at his Judas goat|reek like a charnel house|gobbets of a meaty lunch still in his beard|[3 QUIRKS/MOTIFFS]always ready to wreck, vandalize or destroy something|relish in instilling fear in others|have a twisted sense of loyalty|[3 PLOT HOOKS]Tarvul hires the crew to start a war among his most promising underlings, intending to make whoever survives his successor|the Billhooks kidnap a member of High-Flyer's noble family|a Demon of Shadows promises Spencer the answer to three questions, in exchange for an artifact recently stolen by the Billhooks" }, { human: "The hooded proprietor of a half-flooded grotto tavern near the docks. 'Strange passageways lead to stranger chambers beyond.' Vice Purveyor - Weird.", - ai: "[5 KEYWORDS]mysterious|cryptic|enigmatic|peculiar|shrouded|[5 PHRASES]a voice like silk sliding over gravel|the flicker of torchlight dancing on the flooded floor|the scent of brackish water and old spices|a smile that hinted at a thousand secrets hidden in the shadows|hands gnarled as driftwood, eternally damp|[3 QUIRKS/MOTIFFS]has an uncanny knack for knowing patrons' darkest secrets without them telling him|never seems to sleep or leave the tavern, always there no matter what time you visit|always seen with a strange, luminescent creature that lives in one of the flooded chambers behind the bar|[3 PLOT HOOKS]Alistair hears rumors that this tavern is actually a front for a powerful secret society - perhaps they could be useful allies...or dangerous enemies.|The proprietor offers Ollie a rare alchemical ingredient - but it's not for sale. He'll only trade it for something equally valuable.|Wraith overhears whispers about a hidden chamber beneath the tavern where unimaginable treasures are kept." + ai: "[5 KEYWORDS]mysterious|cryptic|enigmatic|peculiar|shrouded|[5 PHRASES]a voice like wet burlap sliding over gravel|the flicker of torchlight dancing on the flooded floor|the scent of brackish water and old spices|a smile that hinted at a thousand secrets hidden in the shadows|hands gnarled as driftwood, eternally damp|[3 QUIRKS/MOTIFFS]has an uncanny knack for knowing patrons' darkest secrets without them telling him|never seems to sleep or leave the tavern, always there no matter what time you visit|always seen with a strange, luminescent creature that lives in one of the flooded chambers behind the bar|[3 PLOT HOOKS]Alistair hears rumors that this tavern is actually a front for a powerful secret society - perhaps they could be useful allies...or dangerous enemies.|The proprietor offers Ollie a rare alchemical ingredient - but it's not for sale. He'll only trade it for something equally valuable.|Wraith overhears whispers about a hidden chamber beneath the tavern where unimaginable treasures are kept." }, { human: "Setarra, a Demon. Patient, Defiant, Ruthless, Cold", - ai: "[5 KEYWORDS]shadowy|sinister|unfathomable|enigmatic|tempting|[5 PHRASES]whispers that crawl under your skin|always watching, always plotting|a voice like silk and venom|intoxicating presence that draws you closer, despite your instincts urging you to run|eyes like black holes, swallowing all light around them|[3 QUIRKS/MOTIFFS]a disorienting mist clings to her form, obscuring her true shape|casually discusses the devastating acts of capricious revenge she has taken on those who crossed her|never forgets a slight or betrayal, no matter how small or insignificant it may seem at the time|[3 PLOT HOOKS]seeks revenge against Alistair for meddling in her affairs years ago|makes Ollie an offer he can't refuse: unlimited access to forbidden alchemical knowledge in exchange for a single favor, to be called in at some future time|tempts Spencer with forbidden knowledge about demons, promising answers to all their questions if they perform a dangerous ritual" + ai: "[5 KEYWORDS]shadowy|sinister|unfathomable|enigmatic|tempting|[5 PHRASES]whispers that crawl under your skin|always watching, always plotting|in tones of silk and venom|intoxicating presence that draws you closer, despite your instincts urging you to run|eyes like black holes, swallowing all light around them|[3 QUIRKS/MOTIFFS]a disorienting mist clings to her form, obscuring her true shape|casually discusses the devastating acts of capricious revenge she has taken on those who crossed her|never forgets a slight or betrayal, no matter how small or insignificant it may seem at the time|[3 PLOT HOOKS]seeks revenge against Alistair for meddling in her affairs years ago|makes Ollie an offer he can't refuse: unlimited access to forbidden alchemical knowledge in exchange for a single favor, to be called in at some future time|tempts Spencer with forbidden knowledge about demons, promising answers to all their questions if they perform a dangerous ritual" } /* "brutish,merciless,terrifying,savage,loyal, @@ -211,12 +253,13 @@ export const PROMPTS: Record< the gang blames one of the PCs for Tarvul's imprisonment and they're out for revenge" */ ] }, - HarmAdjuster: { - system: "You will act as a \"Harm Generator\" for a game of Blades In The Dark. You will be prompted with (1) a short phrase describing an injury, lasting consequence or other setback, (2) a 'severity level' representing how bad the described harm is, and (3) a 'target severity level' describing how severe the described harm should be. Your job is to increase or decrease the subjective severity of the harm described in the prompt so that it aligns with the target severity level. You should respond with a pipe-delimited list of three possibilities. Your three suggestions should be different from each other, but they should all logically follow from the initial harm described: You should not introduce new facts or make assumptions that are not indicated in the initial prompt. There are four severity levels: Level 1: Lesser Harm (e.g. 'Battered', 'Drained', 'Distracted', 'Scared', 'Confused'), Level 2: Moderate Harm (e.g. 'Exhausted', 'Deep Cut to Arm', 'Concussion', 'Panicked', 'Seduced'), Level 3: Severe Harm (e.g. 'Impaled', 'Broken Leg', 'Shot In Chest', 'Badly Burned', 'Terrified'), Level 4: Fatal Harm (e.g. 'Impaled Through Heart', 'Electrocuted', 'Drowned').", - examples: [ - {human: "Shattered Right Leg/Severity 3/Target 2", ai: "Fractured Right Ankle|Dislocated Knee|Broken Foot"}, - {human: "Tainted Soul/Severity 2/Target 4", ai: "Fully Corrupted|Lost To Darkness|Soulless"}, - {human: "Humiliated/Severity 2/Target 1", ai: "Embarrassed|Momentarily Off-Balance|Enraged"} + ConsequenceAdjuster: { + systemMessage: "You will act as a \"Setback Adjuster\" for a game of Blades In The Dark. You will be prompted with a short phrase describing an injury, lasting consequence or other setback. Your job is to respond with a pipe-delimited list of three possible alternative consequences that are less severe by one level, using the following scale as a rough guide: Level 1 = Lesser (e.g. 'Battered', 'Drained', 'Distracted', 'Scared', 'Confused'), Level 2 = Moderate (e.g. 'Exhausted', 'Deep Cut to Arm', 'Concussion', 'Panicked', 'Seduced'), Level 3 = Severe (e.g. 'Impaled', 'Broken Leg', 'Shot In Chest', 'Badly Burned', 'Terrified'), Level 4 = Fatal or Ruinous (e.g. 'Impaled Through Heart', 'Electrocuted', 'Headquarters Burned to the Ground'). So, if you determine that the consequence described in the prompt is severity level 3, you should respond with three narratively similar consequences that are severity level 2. Your three suggestions should be different from each other, but they should all logically follow from the initial harm described: You should not introduce new facts or make assumptions that are not indicated in the initial prompt.", + examplePrompts: [ + {human: "Shattered Right Leg", ai: "Fractured Right Ankle|Dislocated Knee|Broken Foot"}, + {human: "Soul Destroyed", ai: "Fully Corrupted|Lost In Darkness|Spirit Broken"}, + {human: "Humiliated", ai: "Embarrassed|Momentarily Off-Balance|Enraged"}, + {human: "She Escapes!", ai: "She Spots a Means of Escape|She Puts More Distance Between You|She Stops to Gloat"} ] } }; diff --git a/ts/core/constants.ts b/ts/core/constants.ts index ae363038..17b4d85c 100644 --- a/ts/core/constants.ts +++ b/ts/core/constants.ts @@ -153,7 +153,7 @@ export enum RollType { Action = "Action", Resistance = "Resistance", Fortune = "Fortune", - IndulgeVice = "Vice" + IndulgeVice = "IndulgeVice" } export enum RollSubType { @@ -166,13 +166,16 @@ export enum RollSubType { export enum ConsequenceType { ReducedEffect = "ReducedEffect", - Complication = "Complication", + ComplicationMinor = "ComplicationMinor", + ComplicationMajor = "ComplicationMajor", + ComplicationSerious = "ComplicationSerious", LostOpportunity = "LostOpportunity", WorsePosition = "WorsePosition", Harm1 = "Harm1", Harm2 = "Harm2", Harm3 = "Harm3", - Harm4 = "Harm4" + Harm4 = "Harm4", + None = "None" } export enum RollModStatus { @@ -218,9 +221,18 @@ export enum RollResult { } export enum RollPhase { + // Collaboration: Before GM toggles "Roll" button for player to click. Collaboration = "Collaboration", - AwaitingResult = "AwaitingResult", + // AwaitingRoll: Waiting for player to click "ROLL" + AwaitingRoll = "AwaitingRoll", + // ApplyingConsequences: Waiting for GM to select consequence(s), AI to + // respond with resistance options, and GM to select + // resistance options. + ApplyingConsequences = "ApplyingConsequences", + // AwaitingChatInput: Consequences and player options output to chat; + // awaiting player choice there AwaitingChatInput = "AwaitingChatInput", + // Complete: Roll finished (but may trigger another roll, e.g. resistance) Complete = "Complete" } @@ -364,6 +376,57 @@ const C = { "gpt-4-32k" ] }, + Consequences: { + [Position.controlled]: { + [RollResult.partial]: [ + ConsequenceType.ComplicationMinor, + ConsequenceType.ReducedEffect, + ConsequenceType.WorsePosition, + ConsequenceType.Harm1, + ConsequenceType.None + ], + [RollResult.fail]: [ + ConsequenceType.WorsePosition, + ConsequenceType.None + ] + }, + [Position.risky]: { + [RollResult.partial]: [ + ConsequenceType.ComplicationMajor, + ConsequenceType.WorsePosition, + ConsequenceType.ReducedEffect, + ConsequenceType.Harm2, + ConsequenceType.None + ], + [RollResult.fail]: [ + ConsequenceType.ComplicationMajor, + ConsequenceType.WorsePosition, + ConsequenceType.LostOpportunity, + ConsequenceType.Harm2 + ] + }, + [Position.desperate]: { + [RollResult.partial]: [ + ConsequenceType.ComplicationSerious, + ConsequenceType.ReducedEffect, + ConsequenceType.Harm3 + ], + [RollResult.fail]: [ + ConsequenceType.ComplicationSerious, + ConsequenceType.LostOpportunity, + ConsequenceType.Harm3 + ] + } + }, + ResistedConsequenceTypes: { + [ConsequenceType.Harm4]: ConsequenceType.Harm3, + [ConsequenceType.Harm3]: ConsequenceType.Harm2, + [ConsequenceType.Harm2]: ConsequenceType.Harm1, + [ConsequenceType.Harm1]: ConsequenceType.None, + [ConsequenceType.ComplicationSerious]: ConsequenceType.ComplicationMajor, + [ConsequenceType.ComplicationMajor]: ConsequenceType.ComplicationMinor, + [ConsequenceType.ComplicationMinor]: ConsequenceType.None + }, Colors: { bWHITE: "rgba(255, 255, 255, 1)", WHITE: "rgba(200, 200, 200, 1)", diff --git a/ts/core/gsap.ts b/ts/core/gsap.ts index 5afb991a..cfb323ae 100644 --- a/ts/core/gsap.ts +++ b/ts/core/gsap.ts @@ -1,4 +1,5 @@ import U from "./utilities"; +// eslint-disable-next-line import/no-unresolved import {TextPlugin} from "gsap/all"; const gsapPlugins: gsap.RegisterablePlugins[] = [ @@ -61,11 +62,11 @@ const gsapEffects: Record = { } }, slideUp: { - effect: (targets) => U.gsap.to( + effect: targets => U.gsap.to( targets, { height: 0, - // paddingTop: 0, + // PaddingTop: 0, // paddingBottom: 0, duration: 0.5, ease: "power3" @@ -102,10 +103,10 @@ const gsapEffects: Record = { ease: config.ease, stagger: config.stagger ? { - ...config.stagger as gsap.StaggerVars, - repeat: 1, - yoyo: true - } + ...config.stagger as gsap.StaggerVars, + repeat: 1, + yoyo: true + } : {} } ), @@ -133,7 +134,6 @@ const gsapEffects: Record = { // Pulse in size and color // Shimmer as they shrink back ? - // G.effects.fillCoins(".dot.full-dot", {duration: 0.5, ease: "expo.in", stagger: {amount: 0.75, from: "start", repeat: 1, yoyo: true}}) return U.gsap.effects.throb(targets, {stagger: { amount: 0.25, from: "start", @@ -146,9 +146,8 @@ const gsapEffects: Record = { hoverTooltip: { effect: (tooltip, config) => { const tl = U.gsap.timeline({paused: true}); - if (!tooltip) { return tl } - // tooltip = $(tooltip); - // const scalingElems = [config.scalingElems as JQuery|Array>|undefined ?? []].flat().filter((elem$) => Boolean(elem$[0])); + if (!tooltip) { return tl; } + // Tooltip = $(tooltip); if (config.scalingElems.length > 0) { tl.to( @@ -194,6 +193,9 @@ const gsapEffects: Record = { } }; +/** + * + */ export function Initialize() { if (gsapPlugins.length) { U.gsap.registerPlugin(...gsapPlugins); @@ -203,30 +205,34 @@ export function Initialize() { }); } +/** + * + * @param html + */ export function ApplyTooltipListeners(html: JQuery) { - html.find(".tooltip-trigger").each((_, elem) => { - const tooltipElem = $(elem).find(".tooltip")[0] ?? $(elem).next(".tooltip")[0]; - if (!tooltipElem) { return } - $(elem).data("hoverTimeline", U.gsap.effects.hoverTooltip( + html.find(".tooltip-trigger").each((_, el) => { + const tooltipElem = $(el).find(".tooltip")[0] ?? $(el).next(".tooltip")[0]; + if (!tooltipElem) { return; } + $(el).data("hoverTimeline", U.gsap.effects.hoverTooltip( tooltipElem, { - scalingElems: [...$(elem).find(".tooltip-scaling-elem")].filter((elem) => Boolean(elem)), + scalingElems: [...$(el).find(".tooltip-scaling-elem")].filter(elem => Boolean(elem)), xMotion: $(tooltipElem).hasClass("tooltip-left") ? "-=250" : "+=200", tooltipScale: $(tooltipElem).hasClass("tooltip-small") ? 1 : 1.2 } )); - $(elem).on({ + $(el).on({ mouseenter: function() { - $(elem).css("z-index", 10); - $(elem).data("hoverTimeline").play(); + $(el).css("z-index", 10); + $(el).data("hoverTimeline").play(); }, mouseleave: function() { - $(elem).data("hoverTimeline").reverse().then(() => { - $(elem).css("z-index", ""); + $(el).data("hoverTimeline").reverse().then(() => { + $(el).css("z-index", ""); }); } }); }); } -export default U.gsap; \ No newline at end of file +export default U.gsap; diff --git a/ts/documents/actors/BladesCrew.ts b/ts/documents/actors/BladesCrew.ts index 73b60324..c9d6a892 100644 --- a/ts/documents/actors/BladesCrew.ts +++ b/ts/documents/actors/BladesCrew.ts @@ -9,19 +9,22 @@ class BladesCrew extends BladesActor implements BladesActorSubClass.Crew, BladesRoll.ParticipantDocData { // #region Static Overrides: Create ~ - 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 ?? {}; eLog.checkLog2("actor", "BladesActor.create(data,options)", {data, options}); - //~ For Crew and PC set the Token to sync with charsheet. + // ~ For Crew and PC set the Token to sync with charsheet. data.token.actorLink = true; - //~ Create world_name + // ~ Create world_name data.system.world_name = data.system.world_name ?? data.name.replace(/[^A-Za-z_0-9 ]/g, "").trim().replace(/ /g, "_"); - //~ Initialize generic experience clues. + // ~ Initialize generic experience clues. data.system.experience = { playbook: {value: 0, max: 8}, clues: [], @@ -44,6 +47,7 @@ class BladesCrew extends BladesActor implements BladesActorSubClass.Crew, const factorData: Partial> = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -54,6 +58,7 @@ class BladesCrew extends BladesActor implements BladesActorSubClass.Crew, }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), @@ -66,22 +71,31 @@ class BladesCrew extends BladesActor implements BladesActorSubClass.Crew, return factorData; } + // #region BladesRoll.PrimaryDoc Implementation - get rollPrimaryID() {return this.id} - get rollPrimaryDoc() {return this} - get rollPrimaryName() {return this.name} - get rollPrimaryType() {return this.type} - get rollPrimaryImg() {return this.img} + get rollPrimaryID() {return this.id;} + + get rollPrimaryDoc() {return this;} + + get rollPrimaryName() {return this.name;} + + get rollPrimaryType() {return this.type;} + + get rollPrimaryImg() {return this.img;} // #endregion // #region BladesRoll.ParticipantDoc Implementation - get rollParticipantID() {return this.id} - get rollParticipantDoc() {return this} - get rollParticipantIcon() {return this.playbook?.img ?? this.img} - get rollParticipantName() {return this.name} - get rollParticipantType() {return this.type} + get rollParticipantID() {return this.id;} - get rollParticipantModsData(): BladesRoll.RollModData[] {return []} + get rollParticipantDoc() {return this;} + + get rollParticipantIcon() {return this.playbook?.img ?? this.img;} + + get rollParticipantName() {return this.name;} + + get rollParticipantType() {return this.type;} + + get rollParticipantModsData(): BladesRoll.RollModData[] {return [];} // #endregion @@ -89,15 +103,19 @@ class BladesCrew extends BladesActor implements BladesActorSubClass.Crew, get abilities(): BladesItem[] { - if (!this.playbook) {return []} - return this.activeSubItems.filter((item) => [BladesItemType.ability, BladesItemType.crew_ability].includes(item.type)); + if (!this.playbook) {return [];} + return this.activeSubItems + .filter(item => [BladesItemType.ability, BladesItemType.crew_ability].includes(item.type)); } get playbookName() { return this.playbook?.name as (BladesTag & Playbook) | undefined; } + get playbook(): BladesItemOfType | undefined { - return this.activeSubItems.find((item): item is BladesItemOfType => item.type === BladesItemType.crew_playbook); + return this.activeSubItems + .find((item): item is BladesItemOfType => + item.type === BladesItemType.crew_playbook); } } @@ -107,4 +125,4 @@ declare interface BladesCrew { system: BladesActorSchema.Crew } -export default BladesCrew; \ No newline at end of file +export default BladesCrew; diff --git a/ts/documents/actors/BladesFaction.ts b/ts/documents/actors/BladesFaction.ts index d54a4728..41f9b253 100644 --- a/ts/documents/actors/BladesFaction.ts +++ b/ts/documents/actors/BladesFaction.ts @@ -11,6 +11,7 @@ class BladesFaction extends BladesActor implements BladesActorSubClass.Faction, const factorData: Partial> = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -21,6 +22,7 @@ class BladesFaction extends BladesActor implements BladesActorSubClass.Faction, }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), diff --git a/ts/documents/actors/BladesNPC.ts b/ts/documents/actors/BladesNPC.ts index 48b29523..c7cd2ca9 100644 --- a/ts/documents/actors/BladesNPC.ts +++ b/ts/documents/actors/BladesNPC.ts @@ -13,6 +13,7 @@ class BladesNPC extends BladesActor implements BladesActorSubClass.NPC, const factorData: Partial> = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -23,6 +24,7 @@ class BladesNPC extends BladesActor implements BladesActorSubClass.NPC, }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), @@ -36,6 +38,7 @@ class BladesNPC extends BladesActor implements BladesActorSubClass.NPC, if (BladesActor.IsType(this, BladesActorType.npc)) { factorData[Factor.scale] = { name: Factor.scale, + display: "Scale", value: this.getFactorTotal(Factor.scale), max: this.getFactorTotal(Factor.scale), baseVal: this.getFactorTotal(Factor.scale), @@ -47,6 +50,7 @@ class BladesNPC extends BladesActor implements BladesActorSubClass.NPC, }; factorData[Factor.magnitude] = { name: Factor.magnitude, + display: "Magnitude", value: this.getFactorTotal(Factor.magnitude), max: this.getFactorTotal(Factor.magnitude), baseVal: this.getFactorTotal(Factor.magnitude), diff --git a/ts/documents/actors/BladesPC.ts b/ts/documents/actors/BladesPC.ts index d8ea5f9f..4feb4dbc 100644 --- a/ts/documents/actors/BladesPC.ts +++ b/ts/documents/actors/BladesPC.ts @@ -226,6 +226,7 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, const factorData: Partial> = { [Factor.tier]: { name: Factor.tier, + display: "Tier", value: this.getFactorTotal(Factor.tier), max: this.getFactorTotal(Factor.tier), baseVal: this.getFactorTotal(Factor.tier), @@ -236,6 +237,7 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, }, [Factor.quality]: { name: Factor.quality, + display: "Quality", value: this.getFactorTotal(Factor.quality), max: this.getFactorTotal(Factor.quality), baseVal: this.getFactorTotal(Factor.quality), @@ -277,7 +279,7 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, rollModsData.push({ id: `Harm-negative-${effectCat}`, name: harmString, - category: effectCat, + section: effectCat, posNeg: "negative", base_status: RollModStatus.ToggledOn, modType: "harm", @@ -299,7 +301,7 @@ class BladesPC extends BladesActor implements BladesActorSubClass.Scoundrel, id: "Push-negative-roll", name: "PUSH", sideString: harmCondition.trim(), - category: RollModSection.roll, + section: RollModSection.roll, posNeg: "negative", base_status: RollModStatus.ToggledOn, modType: "harm",