import {EncounterBuilderRandomizer} from "./encounterbuilder-randomizer.js"; import {EncounterBuilderCreatureMeta, EncounterBuilderXpInfo, EncounterPartyMeta, EncounterPartyPlayerMeta} from "./encounterbuilder-models.js"; import {EncounterBuilderUiTtk} from "./encounterbuilder-ui-ttk.js"; import {EncounterBuilderUiHelp} from "./encounterbuilder-ui-help.js"; import {EncounterBuilderRenderableCollectionPlayersSimple} from "./encounterbuilder-playerssimple.js"; import {EncounterBuilderRenderableCollectionColsExtraAdvanced} from "./encounterbuilder-colsextraadvanced.js"; import {EncounterBuilderRenderableCollectionPlayersAdvanced} from "./encounterbuilder-playersadvanced.js"; import {EncounterBuilderAdjuster} from "./encounterbuilder-adjuster.js"; /** * TODO rework this to use doubled multipliers for XP, so we avoid the 0.5x issue for 6+ party sizes. Then scale * everything back down at the end. */ export class EncounterBuilderUi extends BaseComponent { static _RenderState = class { constructor () { this.$wrpRowsSimple = null; this.$wrpRowsAdvanced = null; this.$wrpHeadersAdvanced = null; this.$wrpFootersAdvanced = null; this.infoHoverId = null; this._collectionPlayersSimple = null; this._collectionColsExtraAdvanced = null; this._collectionPlayersAdvanced = null; } }; /* -------------------------------------------- */ _cache = null; _comp = null; constructor ({cache, comp}) { super(); this._cache = cache; this._comp = comp; } /** * @param {?jQuery} $parentRandomAndAdjust * @param {?jQuery} $parentViewer * @param {?jQuery} $parentGroupAndDifficulty */ render ( { $parentRandomAndAdjust = null, $parentViewer = null, $parentGroupAndDifficulty = null, }, ) { const rdState = new this.constructor._RenderState(); this._render_randomAndAdjust({rdState, $parentRandomAndAdjust}); this._render_viewer({rdState, $parentViewer}); this._render_groupAndDifficulty({rdState, $parentGroupAndDifficulty}); this._render_addHooks({rdState}); } /* -------------------------------------------- */ _render_randomAndAdjust ({$parentRandomAndAdjust}) { const { $btnRandom, $btnRandomMode, $liRandomEasy, $liRandomMedium, $liRandomHard, $liRandomDeadly, } = this._render_randomAndAdjust_getRandomMeta(); const { $btnAdjust, $btnAdjustMode, $liAdjustEasy, $liAdjustMedium, $liAdjustHard, $liAdjustDeadly, } = this._render_randomAndAdjust_getAdjustMeta(); $$($parentRandomAndAdjust)`
${$btnRandom} ${$btnRandomMode}
    ${$liRandomEasy} ${$liRandomMedium} ${$liRandomHard} ${$liRandomDeadly}
${$btnAdjust} ${$btnAdjustMode}
    ${$liAdjustEasy} ${$liAdjustMedium} ${$liAdjustHard} ${$liAdjustDeadly}
`; } _render_randomAndAdjust_getRandomMeta () { let modeRandom = "medium"; const pSetRandomMode = async (mode) => { const randomizer = new EncounterBuilderRandomizer({ partyMeta: this._getPartyMeta(), cache: this._cache, }); const randomCreatureMetas = await randomizer.pGetRandomEncounter({ difficulty: mode, lockedEncounterCreatures: this._comp.creatureMetas.filter(creatureMeta => creatureMeta.isLocked), }); if (randomCreatureMetas != null) this._comp.creatureMetas = randomCreatureMetas; modeRandom = mode; $btnRandom .text(`Random ${mode.toTitleCase()}`) .title(`Randomly generate ${Parser.getArticle(mode)} ${mode.toTitleCase()} encounter`); }; const $getLiRandom = (mode) => { return $(`
  • Random ${mode.toTitleCase()}
  • `) .click(async (evt) => { evt.preventDefault(); await pSetRandomMode(mode); }); }; const $btnRandom = $(``) .click(async evt => { evt.preventDefault(); await pSetRandomMode(modeRandom); }); const $btnRandomMode = $(``); JqueryUtil.bindDropdownButton($btnRandomMode); return { $btnRandom, $btnRandomMode, $liRandomEasy: $getLiRandom("easy"), $liRandomMedium: $getLiRandom("medium"), $liRandomHard: $getLiRandom("hard"), $liRandomDeadly: $getLiRandom("deadly"), }; } _render_randomAndAdjust_getAdjustMeta () { let modeAdjust = "medium"; const pSetAdjustMode = async (mode) => { const adjuster = new EncounterBuilderAdjuster({ partyMeta: this._getPartyMeta(), }); const adjustedCreatureMetas = await adjuster.pGetAdjustedEncounter({ difficulty: mode, creatureMetas: this._comp.creatureMetas, }); if (adjustedCreatureMetas != null) this._comp.creatureMetas = adjustedCreatureMetas; modeAdjust = mode; $btnAdjust .text(`Adjust to ${mode.toTitleCase()}`) .title(`Adjust the current encounter difficulty to ${mode.toTitleCase()}`); }; const $getLiAdjust = (mode) => { return $(`
  • Adjust to ${mode.toTitleCase()}
  • `) .click(async (evt) => { evt.preventDefault(); await pSetAdjustMode(mode); }); }; const $btnAdjust = $(``) .click(async evt => { evt.preventDefault(); await pSetAdjustMode(modeAdjust); }); const $btnAdjustMode = $(``); JqueryUtil.bindDropdownButton($btnAdjustMode); return { $btnAdjust, $btnAdjustMode, $liAdjustEasy: $getLiAdjust("easy"), $liAdjustMedium: $getLiAdjust("medium"), $liAdjustHard: $getLiAdjust("hard"), $liAdjustDeadly: $getLiAdjust("deadly"), }; } /* -------------------------------------------- */ _render_viewer ({$parentViewer}) { if (!$parentViewer) return; const $wrpOutput = $(`
    `); $$($parentViewer)`${$wrpOutput}`; this._comp.addHookCreatureMetas(() => { const $lis = this._comp.creatureMetas .map(creatureMeta => { const $btnShuffle = $(``) .click(() => { this._doShuffle({creatureMeta}); }); return $$`
  • ${$btnShuffle} ${Renderer.get().render(`${creatureMeta.count}× {@creature ${creatureMeta.creature.name}|${creatureMeta.creature.source}}`)}
  • `; }); $$($wrpOutput.empty())``; })(); } /* -------------------------------------------- */ _render_groupAndDifficulty ({rdState, $parentGroupAndDifficulty}) { const { $stg: $stgSimple, $wrpRows: $wrpRowsSimple, } = this._renderGroupAndDifficulty_getGroupEles_simple(); rdState.$wrpRowsSimple = $wrpRowsSimple; const { $stg: $stgAdvanced, $wrpRows: $wrpRowsAdvanced, $wrpHeaders: $wrpHeadersAdvanced, $wrpFooters: $wrpFootersAdvanced, } = this._renderGroupAndDifficulty_getGroupEles_advanced(); rdState.$wrpRowsAdvanced = $wrpRowsAdvanced; rdState.$wrpHeadersAdvanced = $wrpHeadersAdvanced; rdState.$wrpFootersAdvanced = $wrpFootersAdvanced; const $hrHasCreatures = $(`
    `); const $wrpDifficulty = $$`
    ${this._renderGroupAndDifficulty_$getDifficultyLhs()} ${this._renderGroupAndDifficulty_$getDifficultyRhs({rdState})}
    `; this._addHookBase("derivedGroupAndDifficulty", () => { const { encounterXpInfo = EncounterBuilderXpInfo.getDefault(), } = this._state.derivedGroupAndDifficulty; $hrHasCreatures.toggleVe(encounterXpInfo.relevantCount); $wrpDifficulty.toggleVe(encounterXpInfo.relevantCount); })(); $$($parentGroupAndDifficulty)`

    Group Info

    ${$stgSimple} ${$stgAdvanced} ${this._renderGroupAndDifficulty_$getGroupInfoRhs()}
    ${$hrHasCreatures} ${$wrpDifficulty}`; rdState.collectionPlayersSimple = new EncounterBuilderRenderableCollectionPlayersSimple({ comp: this._comp, rdState, }); rdState.collectionColsExtraAdvanced = new EncounterBuilderRenderableCollectionColsExtraAdvanced({ comp: this._comp, rdState, }); rdState.collectionPlayersAdvanced = new EncounterBuilderRenderableCollectionPlayersAdvanced({ comp: this._comp, rdState, }); } _renderGroupAndDifficulty_getGroupEles_simple () { const $btnAddPlayers = $(``) .click(() => this._comp.doAddPlayer()); const $wrpRows = $(`
    `); const $stg = $$`
    Players:
    Level:
    ${$wrpRows}
    ${$btnAddPlayers}
    ${this._renderGroupAndDifficulty_$getPtAdvancedMode()}
    `; this._comp.addHookIsAdvanced(() => { $stg.toggleVe(!this._comp.isAdvanced); })(); return { $wrpRows, $stg, }; } _renderGroupAndDifficulty_getGroupEles_advanced () { const $btnAddPlayers = $(``) .click(() => this._comp.doAddPlayer()); const $btnAddAdvancedCol = $(``) .click(() => this._comp.doAddColExtraAdvanced()); const $wrpHeaders = $(`
    `); const $wrpFooters = $(`
    `); const $wrpRows = $(`
    `); const $stg = $$`
    Name
    Level
    ${$wrpHeaders} ${$btnAddAdvancedCol}
    ${$wrpRows}
    ${$btnAddPlayers}
    ${$wrpFooters}
    ${this._renderGroupAndDifficulty_$getPtAdvancedMode()}
    ${Renderer.get().render(`{@note Additional columns will be imported into the DM Screen.}`)}
    `; this._comp.addHookIsAdvanced(() => { $stg.toggleVe(this._comp.isAdvanced); })(); return { $stg, $wrpRows, $wrpHeaders, $wrpFooters, }; } _renderGroupAndDifficulty_$getPtAdvancedMode () { const $cbAdvanced = ComponentUiUtil.$getCbBool(this._comp, "isAdvanced"); return $$`
    `; } static _TITLE_DIFFICULTIES = { easy: "An easy encounter doesn't tax the characters' resources or put them in serious peril. They might lose a few hit points, but victory is pretty much guaranteed.", medium: "A medium encounter usually has one or two scary moments for the players, but the characters should emerge victorious with no casualties. One or more of them might need to use healing resources.", hard: "A hard encounter could go badly for the adventurers. Weaker characters might get taken out of the fight, and there's a slim chance that one or more characters might die.", deadly: "A deadly encounter could be lethal for one or more player characters. Survival often requires good tactics and quick thinking, and the party risks defeat", absurd: "An "absurd" encounter is a deadly encounter as per the rules, but is differentiated here to provide an additional tool for judging just how deadly a "deadly" encounter will be. It is calculated as: "deadly + (deadly - hard)".", }; static _TITLE_BUDGET_DAILY = "This provides a rough estimate of the adjusted XP value for encounters the party can handle before the characters will need to take a long rest."; static _TITLE_XP_TO_NEXT_LEVEL = "The total XP required to allow each member of the party to level up to their next level."; static _TITLE_TTK = "Time to Kill: The estimated number of turns the party will require to defeat the encounter. This assumes single-target damage only."; static _getDifficultyKey ({partyMeta, encounterXpInfo}) { if (encounterXpInfo.adjustedXp >= partyMeta.easy && encounterXpInfo.adjustedXp < partyMeta.medium) return "easy"; if (encounterXpInfo.adjustedXp >= partyMeta.medium && encounterXpInfo.adjustedXp < partyMeta.hard) return "medium"; if (encounterXpInfo.adjustedXp >= partyMeta.hard && encounterXpInfo.adjustedXp < partyMeta.deadly) return "hard"; if (encounterXpInfo.adjustedXp >= partyMeta.deadly && encounterXpInfo.adjustedXp < partyMeta.absurd) return "deadly"; if (encounterXpInfo.adjustedXp >= partyMeta.absurd) return "absurd"; return "trivial"; } static _getDifficultyHtml ({partyMeta, difficulty}) { return `${difficulty.toTitleCase()}: ${partyMeta[difficulty].toLocaleString()} XP`; } _renderGroupAndDifficulty_$getGroupInfoRhs () { const $dispXpEasy = $(`
    `); const $dispXpMedium = $(`
    `); const $dispXpHard = $(`
    `); const $dispXpDeadly = $(`
    `); const $dispXpAbsurd = $(`
    `); const $dispsXpDifficulty = { "easy": $dispXpEasy, "medium": $dispXpMedium, "hard": $dispXpHard, "deadly": $dispXpDeadly, "absurd": $dispXpAbsurd, }; const $dispTtk = $(`
    `); const $dispBudgetDaily = $(`
    `); const $dispExpToLevel = $(`
    `); this._addHookBase("derivedGroupAndDifficulty", () => { const { partyMeta = EncounterPartyMeta.getDefault(), encounterXpInfo = EncounterBuilderXpInfo.getDefault(), } = this._state.derivedGroupAndDifficulty; const difficulty = this.constructor._getDifficultyKey({partyMeta, encounterXpInfo}); Object.entries($dispsXpDifficulty) .forEach(([difficulty_, $disp]) => { $disp .toggleClass("bold", difficulty === difficulty_) .html(this.constructor._getDifficultyHtml({partyMeta, difficulty: difficulty_})); }); $dispTtk .html(`TTK: ${EncounterBuilderUiTtk.getApproxTurnsToKill({partyMeta, creatureMetas: this._comp.creatureMetas}).toFixed(2)}`); $dispBudgetDaily .html(`Daily Budget: ${partyMeta.dailyBudget.toLocaleString()} XP`); $dispExpToLevel .html(`XP to Next Level: ${partyMeta.xpToNextLevel.toLocaleString()} XP`); })(); return $$`
    ${$dispXpEasy} ${$dispXpMedium} ${$dispXpHard} ${$dispXpDeadly} ${$dispXpAbsurd}
    ${$dispTtk}
    ${$dispBudgetDaily} ${$dispExpToLevel}
    `; } _renderGroupAndDifficulty_$getDifficultyLhs () { const $dispDifficulty = $(`

    `); this._addHookBase("derivedGroupAndDifficulty", () => { const { partyMeta = EncounterPartyMeta.getDefault(), encounterXpInfo = EncounterBuilderXpInfo.getDefault(), } = this._state.derivedGroupAndDifficulty; const difficulty = this.constructor._getDifficultyKey({partyMeta, encounterXpInfo}); $dispDifficulty.text(`Difficulty: ${difficulty.toTitleCase()}`); })(); return $$`
    ${$dispDifficulty}
    `; } _renderGroupAndDifficulty_$getDifficultyRhs ({rdState}) { const $dispXpRawTotal = $(`

    `); const $dispXpRawPerPlayer = $(``); const $hovXpAdjustedInfo = $(``); const $dispXpAdjustedTotal = $(`

    `); const $dispXpAdjustedPerPlayer = $(``); this._addHookBase("derivedGroupAndDifficulty", () => { const { partyMeta = EncounterPartyMeta.getDefault(), encounterXpInfo = EncounterBuilderXpInfo.getDefault(), } = this._state.derivedGroupAndDifficulty; $dispXpRawTotal.text(`Total XP: ${encounterXpInfo.baseXp.toLocaleString()}`); $dispXpRawPerPlayer.text(`(${Math.floor(encounterXpInfo.baseXp / partyMeta.cntPlayers).toLocaleString()} per player)`); const infoEntry = EncounterBuilderUiHelp.getHelpEntry({partyMeta, encounterXpInfo}); if (rdState.infoHoverId == null) { const hoverMeta = Renderer.hover.getMakePredefinedHover(infoEntry, {isBookContent: true}); rdState.infoHoverId = hoverMeta.id; $hovXpAdjustedInfo .off("mouseover") .off("mousemove") .off("mouseleave") .on("mouseover", function (event) { hoverMeta.mouseOver(event, this); }) .on("mousemove", function (event) { hoverMeta.mouseMove(event, this); }) .on("mouseleave", function (event) { hoverMeta.mouseLeave(event, this); }); } else { Renderer.hover.updatePredefinedHover(rdState.infoHoverId, infoEntry); } $dispXpAdjustedTotal.html(`Adjusted XP (×${encounterXpInfo.playerAdjustedXpMult}): ${encounterXpInfo.adjustedXp.toLocaleString()}`); $dispXpAdjustedPerPlayer.text(`(${Math.floor(encounterXpInfo.adjustedXp / partyMeta.cntPlayers).toLocaleString()} per player)`); })(); return $$`
    ${$dispXpRawTotal}
    ${$dispXpRawPerPlayer}
    ${$hovXpAdjustedInfo}${$dispXpAdjustedTotal}
    ${$dispXpAdjustedPerPlayer}
    `; } /* -------------------------------------------- */ _render_addHooks ({rdState}) { this._comp.addHookPlayersSimple((valNotFirstRun) => { rdState.collectionPlayersSimple.render(); if (valNotFirstRun == null) return; this._render_hk_setDerivedGroupAndDifficulty(); this._render_hk_doUpdateExternalStates(); })(); this._comp.addHookPlayersAdvanced((valNotFirstRun) => { rdState.collectionPlayersAdvanced.render(); if (valNotFirstRun == null) return; this._render_hk_setDerivedGroupAndDifficulty(); this._render_hk_doUpdateExternalStates(); })(); this._comp.addHookIsAdvanced((valNotFirstRun) => { if (valNotFirstRun == null) return; this._render_hk_setDerivedGroupAndDifficulty(); this._render_hk_doUpdateExternalStates(); })(); this._comp.addHookCreatureMetas(() => { this._render_hk_setDerivedGroupAndDifficulty(); this._render_hk_doUpdateExternalStates(); })(); this._comp.addHookColsExtraAdvanced(() => { rdState.collectionColsExtraAdvanced.render(); this._render_hk_doUpdateExternalStates(); })(); } _render_hk_setDerivedGroupAndDifficulty () { const partyMeta = this._getPartyMeta(); const encounterXpInfo = EncounterBuilderCreatureMeta.getEncounterXpInfo(this._comp.creatureMetas, this._getPartyMeta()); this._state.derivedGroupAndDifficulty = { partyMeta, encounterXpInfo, }; } _render_hk_doUpdateExternalStates () { /* Implement as required */ } /* -------------------------------------------- */ _doShuffle ({creatureMeta}) { if (creatureMeta.isLocked) return; const ix = this._comp.creatureMetas.findIndex(creatureMeta_ => creatureMeta_.isSameCreature(creatureMeta)); if (!~ix) throw new Error(`Could not find creature ${creatureMeta.getHash()} (${creatureMeta.customHashId})`); const creatureMeta_ = this._comp.creatureMetas[ix]; if (creatureMeta_.isLocked) return; const lockedHashes = new Set( this._comp.creatureMetas .filter(creatureMeta => creatureMeta.isLocked) .map(creatureMeta => creatureMeta.getHash()), ); const monRolled = this._doShuffle_getShuffled({creatureMeta: creatureMeta_, lockedHashes}); if (!monRolled) return JqueryUtil.doToast({content: "Could not find another creature worth the same amount of XP!", type: "warning"}); const creatureMetaNxt = new EncounterBuilderCreatureMeta({ creature: monRolled, count: creatureMeta_.count, }); const creatureMetasNxt = [...this._comp.creatureMetas]; const withMonRolled = creatureMetasNxt.find(creatureMeta_ => creatureMeta_.hasCreature(monRolled)); if (withMonRolled) { withMonRolled.count += creatureMetaNxt.count; creatureMetasNxt.splice(ix, 1); } else { creatureMetasNxt[ix] = creatureMetaNxt; } this._comp.creatureMetas = creatureMetasNxt; } _doShuffle_getShuffled ({creatureMeta, lockedHashes}) { const xp = creatureMeta.getXp(); const hash = creatureMeta.getHash(); const availMons = this._cache.getCreaturesByXp(xp) .filter(mon => { const hash_ = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY](mon); return !lockedHashes.has(hash) && hash_ !== hash; }); if (!availMons.length) return null; return RollerUtil.rollOnArray(availMons); } /* -------------------------------------------- */ _getPartyMeta () { return this._comp.getPartyMeta(); } _getDefaultState () { return { derivedGroupAndDifficulty: {}, }; } }