import {InitiativeTrackerConst} from "./dmscreen-initiativetracker-consts.js"; import {InitiativeTrackerNetworking} from "./dmscreen-initiativetracker-networking.js"; import {InitiativeTrackerSettings} from "./dmscreen-initiativetracker-settings.js"; import {InitiativeTrackerSettingsImport} from "./dmscreen-initiativetracker-importsettings.js"; import {InitiativeTrackerMonsterAdd} from "./dmscreen-initiativetracker-monsteradd.js"; import {InitiativeTrackerRoller} from "./dmscreen-initiativetracker-roller.js"; import {InitiativeTrackerEncounterConverter} from "./dmscreen-initiativetracker-encounterconverter.js"; import { InitiativeTrackerStatColumnFactory, IS_PLAYER_VISIBLE_ALL, } from "./dmscreen-initiativetracker-statcolumns.js"; import { InitiativeTrackerRowDataViewActive, } from "./dmscreen-initiativetracker-rowsactive.js"; import { InitiativeTrackerConditionCustomSerializer, InitiativeTrackerRowDataSerializer, InitiativeTrackerStatColumnDataSerializer, } from "./dmscreen-initiativetracker-serial.js"; import {InitiativeTrackerSort} from "./dmscreen-initiativetracker-sort.js"; import {InitiativeTrackerUtil} from "../../initiativetracker/initiativetracker-utils.js"; import {DmScreenUtil} from "../dmscreen-util.js"; import { InitiativeTrackerRowStateBuilderActive, InitiativeTrackerRowStateBuilderDefaultParty, } from "./dmscreen-initiativetracker-rowstatebuilder.js"; import {InitiativeTrackerDefaultParty} from "./dmscreen-initiativetracker-defaultparty.js"; import {ListUtilBestiary} from "../../utils-list-bestiary.js"; export class InitiativeTracker extends BaseComponent { constructor ({board, savedState}) { super(); this._board = board; this._savedState = savedState; this._networking = new InitiativeTrackerNetworking({board}); this._roller = new InitiativeTrackerRoller(); this._rowStateBuilderActive = new InitiativeTrackerRowStateBuilderActive({comp: this, roller: this._roller}); this._rowStateBuilderDefaultParty = new InitiativeTrackerRowStateBuilderDefaultParty({comp: this, roller: this._roller}); this._viewRowsActive = null; this._viewRowsActiveMeta = null; this._compDefaultParty = null; this._creatureViewers = []; } render () { if (this._viewRowsActiveMeta) this._viewRowsActiveMeta.cbDoCleanup(); this._resetHooks("state"); this._resetHooksAll("state"); this._setStateFromSerialized(); this._render_bindSortDirHooks(); const $wrpTracker = $(`
`) .on("drop", evt => this._pDoHandleImportDrop(evt.originalEvent)); const sendStateToClientsDebounced = MiscUtil.debounce( () => { this._networking.sendStateToClients({fnGetToSend: this._getPlayerFriendlyState.bind(this)}); this._sendStateToCreatureViewers(); }, 100, // long delay to avoid network spam ); const doUpdateExternalStates = () => { this._board.doSaveStateDebounced(); sendStateToClientsDebounced(); }; this._addHookAllBase(doUpdateExternalStates); this._viewRowsActive = new InitiativeTrackerRowDataViewActive({ comp: this, prop: "rows", roller: this._roller, networking: this._networking, rowStateBuilder: this._rowStateBuilderActive, }); this._viewRowsActiveMeta = this._viewRowsActive.getRenderedView(); this._viewRowsActiveMeta.$ele.appendTo($wrpTracker); this._render_$getWrpFooter({doUpdateExternalStates}).appendTo($wrpTracker); $wrpTracker.data("pDoConnectLocalV1", async () => { const {token} = await this._networking.startServerV1({doUpdateExternalStates}); return token; }); $wrpTracker.data("pDoConnectLocalV0", async (clientView) => { await this._networking.pHandleDoConnectLocalV0({clientView}); sendStateToClientsDebounced(); }); $wrpTracker.data("getState", () => this._getSerializedState()); $wrpTracker.data("getSummary", () => { const names = this._state.rows .map(({entity}) => entity.name) .filter(name => name && name.trim()); return `${this._state.rows.length} creature${this._state.rows.length === 1 ? "" : "s"} ${names.length ? `(${names.slice(0, 3).join(", ")}${names.length > 3 ? "..." : ""})` : ""}`; }); $wrpTracker.data("pDoLoadEncounter", ({entityInfos, encounterInfo}) => this._pDoLoadEncounter({entityInfos, encounterInfo})); $wrpTracker.data("getApi", () => this); return $wrpTracker; } _render_$getWrpButtonsSort () { const $btnSortAlpha = $(``) .on("click", () => { if (this._state.sort === InitiativeTrackerConst.SORT_ORDER_ALPHA) return this._doReverseSortDir(); this._proxyAssignSimple( "state", { sort: InitiativeTrackerConst.SORT_ORDER_ALPHA, dir: InitiativeTrackerConst.SORT_DIR_ASC, }, ); }); const $btnSortNumber = $(``) .on("click", () => { if (this._state.sort === InitiativeTrackerConst.SORT_ORDER_NUM) return this._doReverseSortDir(); this._proxyAssignSimple( "state", { sort: InitiativeTrackerConst.SORT_ORDER_NUM, dir: InitiativeTrackerConst.SORT_DIR_DESC, }, ); }); const hkSortDir = () => { $btnSortAlpha.toggleClass("active", this._state.sort === InitiativeTrackerConst.SORT_ORDER_ALPHA); $btnSortNumber.toggleClass("active", this._state.sort === InitiativeTrackerConst.SORT_ORDER_NUM); }; this._addHookBase("sort", hkSortDir); this._addHookBase("dir", hkSortDir); hkSortDir(); return $$`
${$btnSortAlpha} ${$btnSortNumber}
`; } _render_$getWrpFooter ({doUpdateExternalStates}) { const $btnAdd = $(``) .on("click", async () => { if (this._state.isLocked) return; this._state.rows = [ ...this._state.rows, await this._rowStateBuilderActive.pGetNewRowState({ isPlayerVisible: true, }), ] .filter(Boolean); }); const $btnAddMonster = $(``) .on("click", async () => { if (this._state.isLocked) return; const [isDataEntered, monstersToLoad] = await new InitiativeTrackerMonsterAdd({board: this._board, isRollHp: this._state.isRollHp}) .pGetShowModalResults(); if (!isDataEntered) return; this._state.isRollHp = monstersToLoad.isRollHp; const isGroupRollEval = monstersToLoad.count > 1 && this._state.isRollGroups; const mon = isGroupRollEval ? await DmScreenUtil.pGetScaledCreature({ name: monstersToLoad.name, source: monstersToLoad.source, scaledCr: monstersToLoad.scaledCr, scaledSummonSpellLevel: monstersToLoad.scaledSummonSpellLevel, scaledSummonClassLevel: monstersToLoad.scaledSummonClassLevel, }) : null; const initiative = isGroupRollEval ? await this._roller.pGetRollInitiative({mon}) : null; const rowsNxt = [...this._state.rows]; (await [...new Array(monstersToLoad.count)] .pSerialAwaitMap(async () => { const rowNxt = await this._rowStateBuilderActive.pGetNewRowState({ name: monstersToLoad.name, source: monstersToLoad.source, initiative, rows: rowsNxt, displayName: monstersToLoad.displayName, customName: monstersToLoad.customName, scaledCr: monstersToLoad.scaledCr, scaledSummonSpellLevel: monstersToLoad.scaledSummonSpellLevel, scaledSummonClassLevel: monstersToLoad.scaledSummonClassLevel, }); if (!rowNxt) return; rowsNxt.push(rowNxt); })); this._state.rows = rowsNxt; }); const $btnSetPrevActive = $(``) .click(() => this._viewRowsActive.pDoShiftActiveRow({direction: InitiativeTrackerConst.DIR_BACKWARDS})); const $btnSetNextActive = $(``) .click(() => this._viewRowsActive.pDoShiftActiveRow({direction: InitiativeTrackerConst.DIR_FORWARDS})); const $iptRound = ComponentUiUtil.$getIptInt(this, "round", 1, {min: 1}) .addClass("dm-init__rounds") .removeClass("text-right") .addClass("ve-text-center") .title("Round"); const menuPlayerWindow = ContextUtil.getMenu([ new ContextUtil.Action( "Standard", async () => { this._networking.handleClick_playerWindowV1({doUpdateExternalStates}); }, ), new ContextUtil.Action( "Manual (Legacy)", async () => { this._networking.handleClick_playerWindowV0({doUpdateExternalStates}); }, ), ]); const $btnNetworking = $(``) .click(evt => { if (evt.shiftKey) return this._networking.handleClick_playerWindowV1({doUpdateExternalStates}); return ContextUtil.pOpenMenu(evt, menuPlayerWindow); }); const $btnLock = $(``) .on("click", () => this._state.isLocked = !this._state.isLocked); this._addHookBase("isLocked", () => { $btnLock .toggleClass("btn-success", !!this._state.isLocked) .toggleClass("btn-danger", !this._state.isLocked) .title(this._state.isLocked ? "Unlock Tracker" : "Lock Tracker"); $(".dm-init-lockable").toggleClass("disabled", !!this._state.isLocked); $("input.dm-init-lockable").prop("disabled", !!this._state.isLocked); })(); this._compDefaultParty = new InitiativeTrackerDefaultParty({comp: this, roller: this._roller, rowStateBuilder: this._rowStateBuilderDefaultParty}); const pHandleClickSettings = async () => { const compSettings = new InitiativeTrackerSettings({state: MiscUtil.copyFast(this._state)}); await compSettings.pGetShowModalResults(); Object.assign(this._state, compSettings.getStateUpdate()); }; const menuConfigure = ContextUtil.getMenu([ new ContextUtil.Action( "Settings", () => pHandleClickSettings(), ), null, new ContextUtil.Action( "Edit Default Party", async () => { await this._compDefaultParty.pGetShowModalResults(); }, ), ]); const $btnConfigure = $(``) .click(async evt => { if (evt.shiftKey) return pHandleClickSettings(); return ContextUtil.pOpenMenu(evt, menuConfigure); }); const menuImport = ContextUtil.getMenu([ ...ListUtilBestiary.getContextOptionsLoadSublist({ pFnOnSelect: this._pDoLoadEncounter.bind(this), }), null, new ContextUtil.Action( "Import Settings", async () => { const compImportSettings = new InitiativeTrackerSettingsImport({state: MiscUtil.copyFast(this._state)}); await compImportSettings.pGetShowModalResults(); Object.assign(this._state, compImportSettings.getStateUpdate()); }, ), ]); const $btnLoad = $(``) .click((evt) => { if (this._state.isLocked) return; ContextUtil.pOpenMenu(evt, menuImport); }); const $btnReset = $(``) .click(async () => { if (this._state.isLocked) return; if (!await InputUiUtil.pGetUserBoolean({title: "Reset", htmlDescription: "Are you sure?", textYes: "Yes", textNo: "Cancel"})) return; const stateNxt = { rows: await this._compDefaultParty.pGetConvertedDefaultPartyActiveRows(), }; const defaultState = this._getDefaultState(); ["round", "sort", "dir"] .forEach(prop => stateNxt[prop] = defaultState[prop]); this._proxyAssignSimple("state", stateNxt); }); return $$`
${$btnAdd} ${$btnAddMonster}
${$btnSetPrevActive}${$btnSetNextActive}
${$iptRound}
${this._render_$getWrpButtonsSort()}
${$btnNetworking}
${$btnLock} ${$btnConfigure}
${$btnLoad} ${$btnReset}
`; } _render_bindSortDirHooks () { const hkSortDir = () => { this._state.rows = InitiativeTrackerSort.getSortedRows({ rows: this._state.rows, sortBy: this._state.sort, sortDir: this._state.dir, }); }; this._addHookBase("sort", hkSortDir); this._addHookBase("dir", hkSortDir); hkSortDir(); } /* -------------------------------------------- */ _doReverseSortDir () { this._state.dir = this._state.dir === InitiativeTrackerConst.SORT_DIR_ASC ? InitiativeTrackerConst.SORT_DIR_DESC : InitiativeTrackerConst.SORT_DIR_ASC; } /* -------------------------------------------- */ _getPlayerFriendlyState () { const visibleStatsCols = this._state.statsCols .filter(data => data.isPlayerVisible); const rows = this._state.rows .map(({entity}) => { if (!entity.isPlayerVisible) return null; const isMon = !!entity.source; const out = { name: entity.name, initiative: entity.initiative, isActive: entity.isActive, conditions: entity.conditions || [], rowStatColData: entity.rowStatColData .map(cell => { const mappedCol = visibleStatsCols.find(sc => sc.id === cell.id); if (!mappedCol) return null; if (mappedCol.isPlayerVisible === IS_PLAYER_VISIBLE_ALL || !isMon) { const meta = InitiativeTrackerStatColumnFactory.fromStateData({data: mappedCol}); return meta.getPlayerFriendlyState({cell}); } return {id: null, entity: {isUnknown: true}}; }) .filter(Boolean), }; if (entity.customName) out.customName = entity.customName; if (isMon ? !!this._state.playerInitShowExactMonsterHp : !!this._state.playerInitShowExactPlayerHp) { out.hpCurrent = entity.hpCurrent; out.hpMax = entity.hpMax; } else { out.hpWoundLevel = isNaN(entity.hpCurrent) || isNaN(entity.hpMax) ? -1 : InitiativeTrackerUtil.getWoundLevel(100 * entity.hpCurrent / entity.hpMax); } if (this._state.playerInitShowOrdinals && entity.isShowOrdinal) out.ordinal = entity.ordinal; return out; }) .filter(Boolean); return { type: "state", payload: { rows, statsCols: visibleStatsCols .map(({id, abbreviation}) => ({id, abbreviation})), round: this._state.round, }, }; } /* -------------------------------------------- */ async _pDoLoadEncounter ({entityInfos, encounterInfo}) { const rowsPrev = [...this._state.rows]; // Reset rows early, such that our ordinals are correct for creatures from the encounter this._state.rows = []; const isAddPlayers = this._state.importIsAddPlayers && !this._state.rowsDefaultParty.length; const nxtState = await new InitiativeTrackerEncounterConverter({ roller: this._roller, rowStateBuilderActive: this._rowStateBuilderActive, importIsAddPlayers: isAddPlayers, importIsRollGroups: this._state.importIsRollGroups, isRollInit: this._state.isRollInit, isRollHp: this._state.isRollHp, isRollGroups: this._state.isRollGroups, }).pGetConverted({entityInfos, encounterInfo}); const rowsFromDefaultParty = await this._compDefaultParty.pGetConvertedDefaultPartyActiveRows(); const idsDefaultParty = new Set(rowsFromDefaultParty.map(({id}) => id)); const rowsPrevNonDefaultParty = rowsPrev .filter(({id}) => !idsDefaultParty.has(id)); const stateNxt = { rows: this._state.importIsAppend ? [ ...rowsPrevNonDefaultParty, ...rowsFromDefaultParty, ...nxtState.rows, ] : [ ...rowsFromDefaultParty, ...nxtState.rows, ], }; if (nxtState.isOverwriteStatsCols) { const userVal = await InputUiUtil.pGetUserGenericButton({ title: "Overwrite Additional Columns", buttons: [ new InputUiUtil.GenericButtonInfo({ text: "Yes", clazzIcon: "glyphicon glyphicon-ok", value: "yes", }), new InputUiUtil.GenericButtonInfo({ text: "No", clazzIcon: "glyphicon glyphicon-remove", isPrimary: true, value: "no", }), new InputUiUtil.GenericButtonInfo({ text: "Cancel", clazzIcon: "glyphicon glyphicon-stop", isSmall: true, value: "cancel", }), ], htmlDescription: `

The encounter you are trying to load contains additional column data from the Encounter Builder's "Advanced" mode.
Do you want to overwrite your existing additional columns with columns from the encounter?

`, }); switch (userVal) { case null: case "cancel": { this._state.rows = rowsPrev; return; } case "yes": { stateNxt.isStatsAddColumns = nxtState.isStatsAddColumns; stateNxt.statsCols = nxtState.statsCols .map(it => it.getAsStateData()); break; } case "no": { // No-op break; } default: throw new Error(`Unexpected value "${userVal}"`); } } if (!this._state.importIsAppend) { const defaultState = this._getDefaultState(); ["round", "sort", "dir"] .forEach(prop => stateNxt[prop] = defaultState[prop]); } this._proxyAssignSimple("state", stateNxt); } /* -------------------------------------------- */ async _pDoHandleImportDrop (evt) { const data = EventUtil.getDropJson(evt); if (!data) return; if (data.type !== VeCt.DRAG_TYPE_IMPORT) return; evt.stopPropagation(); evt.preventDefault(); const {page, source, hash} = data; if (page !== UrlUtil.PG_BESTIARY) return; const ent = await DataLoader.pCacheAndGet(page, source, hash, {isRequired: true}); const rowsNxt = [...this._state.rows]; const rowToAdd = await this._rowStateBuilderActive.pGetNewRowState({ name: ent.name, source: ent.source, initiative: null, rows: rowsNxt, }); if (!rowToAdd) return; rowsNxt.push(rowToAdd); this._state.rows = rowsNxt; } /* -------------------------------------------- */ doConnectCreatureViewer ({creatureViewer}) { if (this._creatureViewers.includes(creatureViewer)) return this; this._creatureViewers.push(creatureViewer); creatureViewer.setCreatureState(this._getCreatureViewerFriendlyState()); return this; } static _CREATURE_VIEWER_STATE_PROPS = [ "name", "source", "scaledCr", "scaledSummonSpellLevel", "scaledSummonClassLevel", ]; _getCreatureViewerFriendlyState () { const activeRowPrime = this._state.rows .filter(({entity}) => entity.isActive) .find(Boolean); if (!activeRowPrime) { return Object.fromEntries(this.constructor._CREATURE_VIEWER_STATE_PROPS.map(prop => [prop, null])); } return Object.fromEntries(this.constructor._CREATURE_VIEWER_STATE_PROPS.map(prop => [prop, activeRowPrime.entity[prop]])); } doDisconnectCreatureViewer ({creatureViewer}) { this._creatureViewers = this._creatureViewers.filter(it => it !== creatureViewer); } _sendStateToCreatureViewers () { if (!this._creatureViewers.length) return; const creatureViewerFriendlyState = this._getCreatureViewerFriendlyState(); this._creatureViewers.forEach(it => it.setCreatureState(creatureViewerFriendlyState)); } /* -------------------------------------------- */ _setStateFromSerialized () { const stateNxt = { // region Config sort: this._savedState.s || InitiativeTrackerConst.SORT_ORDER_NUM, dir: this._savedState.d || InitiativeTrackerConst.SORT_DIR_DESC, statsCols: (this._savedState.c || []) .map(dataSerial => this._setStateFromSerialized_statsCol({dataSerial})) .filter(Boolean), // endregion // region Custom conditions conditionsCustom: (this._savedState.cndc || []) .map(dataSerial => InitiativeTrackerConditionCustomSerializer.fromSerial(dataSerial)), // endregion // region Rows rows: (this._savedState.r || []) .map(dataSerial => InitiativeTrackerRowDataSerializer.fromSerial(dataSerial)) .filter(Boolean), rowsDefaultParty: (this._savedState.rdp || []) .map(dataSerial => InitiativeTrackerRowDataSerializer.fromSerial(dataSerial)) .filter(Boolean), // endregion // region Round round: isNaN(this._savedState.n) ? 1 : Number(this._savedState.n), // endregion // region Temporary isLocked: false, // endregion }; // region Config if (this._savedState.ri != null) stateNxt.isRollInit = this._savedState.ri; if (this._savedState.m != null) stateNxt.isRollHp = this._savedState.m; if (this._savedState.rg != null) stateNxt.isRollGroups = this._savedState.rg; if (this._savedState.rri != null) stateNxt.isRerollInitiativeEachRound = this._savedState.rri; if (this._savedState.g != null) stateNxt.importIsRollGroups = this._savedState.g; if (this._savedState.p != null) stateNxt.importIsAddPlayers = this._savedState.p; if (this._savedState.a != null) stateNxt.importIsAppend = this._savedState.a; if (this._savedState.k != null) stateNxt.isStatsAddColumns = this._savedState.k; if (this._savedState.piHp != null) stateNxt.playerInitShowExactPlayerHp = this._savedState.piHp; if (this._savedState.piHm != null) stateNxt.playerInitShowExactMonsterHp = this._savedState.piHm; if (this._savedState.piV != null) stateNxt.playerInitHideNewMonster = this._savedState.piV; if (this._savedState.piO != null) stateNxt.playerInitShowOrdinals = this._savedState.piO; // endregion this._proxyAssignSimple("state", stateNxt); } _setStateFromSerialized_statsCol ({dataSerial}) { if (!dataSerial) return null; return InitiativeTrackerStatColumnFactory.fromStateData({dataSerial}) .getAsStateData(); } _getSerializedState () { return { // region Config s: this._state.sort, d: this._state.dir, ri: this._state.isRollInit, m: this._state.isRollHp, rg: this._state.isRollGroups, rri: this._state.isRerollInitiativeEachRound, g: this._state.importIsRollGroups, p: this._state.importIsAddPlayers, a: this._state.importIsAppend, k: this._state.isStatsAddColumns, piHp: this._state.playerInitShowExactPlayerHp, piHm: this._state.playerInitShowExactMonsterHp, piV: this._state.playerInitHideNewMonster, piO: this._state.playerInitShowOrdinals, c: (this._state.statsCols || []) .map(data => InitiativeTrackerStatColumnDataSerializer.toSerial(data)), // endregion // region Custom conditions cndc: (this._state.conditionsCustom || []) .map(data => InitiativeTrackerConditionCustomSerializer.toSerial(data)), // endregion // region Rows r: (this._state.rows || []) .map(data => InitiativeTrackerRowDataSerializer.toSerial(data)), rdp: (this._state.rowsDefaultParty || []) .map(data => InitiativeTrackerRowDataSerializer.toSerial(data)), // endregion // region Round n: this._state.round, // endregion }; } _getDefaultState () { return { // region Config sort: InitiativeTrackerConst.SORT_ORDER_NUM, dir: InitiativeTrackerConst.SORT_DIR_DESC, isRollInit: true, isRollHp: false, isRollGroups: false, isRerollInitiativeEachRound: false, importIsRollGroups: true, importIsAddPlayers: true, importIsAppend: false, isStatsAddColumns: false, playerInitShowExactPlayerHp: false, playerInitShowExactMonsterHp: false, playerInitHideNewMonster: true, playerInitShowOrdinals: false, statsCols: [], // endregion // region Custom conditions conditionsCustom: [], // endregion // region Rows rows: [], rowsDefaultParty: [], // endregion // region Round round: 1, // endregion // region Temporary isLocked: false, // endregion }; } /* -------------------------------------------- */ static $getPanelElement (board, savedState) { return new this({board, savedState}).render(); } }