mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2025-10-28 20:45:35 -05:00
v1.198.1
This commit is contained in:
26
js/dmscreen/dmscreen-consts.js
Normal file
26
js/dmscreen/dmscreen-consts.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export const PANEL_TYP_EMPTY = 0;
|
||||
export const PANEL_TYP_STATS = 1;
|
||||
export const PANEL_TYP_ROLLBOX = 2;
|
||||
export const PANEL_TYP_TEXTBOX = 3;
|
||||
export const PANEL_TYP_RULES = 4;
|
||||
export const PANEL_TYP_INITIATIVE_TRACKER = 5;
|
||||
export const PANEL_TYP_INITIATIVE_TRACKER_CREATURE_VIEWER = 51;
|
||||
export const PANEL_TYP_UNIT_CONVERTER = 6;
|
||||
export const PANEL_TYP_CREATURE_SCALED_CR = 7;
|
||||
export const PANEL_TYP_CREATURE_SCALED_SPELL_SUMMON = 71;
|
||||
export const PANEL_TYP_CREATURE_SCALED_CLASS_SUMMON = 72;
|
||||
export const PANEL_TYP_TIME_TRACKER = 8;
|
||||
export const PANEL_TYP_MONEY_CONVERTER = 9;
|
||||
export const PANEL_TYP_TUBE = 10;
|
||||
export const PANEL_TYP_TWITCH = 11;
|
||||
export const PANEL_TYP_TWITCH_CHAT = 12;
|
||||
export const PANEL_TYP_ADVENTURES = 13;
|
||||
export const PANEL_TYP_BOOKS = 14;
|
||||
export const PANEL_TYP_INITIATIVE_TRACKER_PLAYER_V1 = 15;
|
||||
export const PANEL_TYP_INITIATIVE_TRACKER_PLAYER_V0 = 151;
|
||||
export const PANEL_TYP_COUNTER = 16;
|
||||
export const PANEL_TYP_IMAGE = 20;
|
||||
export const PANEL_TYP_ADVENTURE_DYNAMIC_MAP = 21;
|
||||
export const PANEL_TYP_GENERIC_EMBED = 90;
|
||||
export const PANEL_TYP_ERROR = 98;
|
||||
export const PANEL_TYP_BLANK = 99;
|
||||
161
js/dmscreen/dmscreen-counter.js
Normal file
161
js/dmscreen/dmscreen-counter.js
Normal file
@@ -0,0 +1,161 @@
|
||||
export class Counter {
|
||||
static $getCounter (board, state) {
|
||||
const $wrpPanel = $(`<div class="w-100 h-100 dm-cnt__root dm__panel-bg dm__data-anchor"/>`) // root class used to identify for saving
|
||||
.data("getState", () => counters.getSaveableState());
|
||||
const counters = new CounterRoot(board, $wrpPanel);
|
||||
counters.setStateFrom(state);
|
||||
counters.render($wrpPanel);
|
||||
return $wrpPanel;
|
||||
}
|
||||
}
|
||||
|
||||
class CounterComponent extends BaseComponent {
|
||||
constructor (board, $wrpPanel) {
|
||||
super();
|
||||
this._board = board;
|
||||
this._$wrpPanel = $wrpPanel;
|
||||
this._addHookAll("state", () => this._board.doSaveStateDebounced());
|
||||
}
|
||||
}
|
||||
|
||||
class CounterRoot extends CounterComponent {
|
||||
constructor (board, $wrpPanel) {
|
||||
super(board, $wrpPanel);
|
||||
|
||||
this._childComps = [];
|
||||
this._$wrpRows = null;
|
||||
}
|
||||
|
||||
render ($parent) {
|
||||
$parent.empty();
|
||||
|
||||
const pod = this.getPod();
|
||||
|
||||
this._$wrpRows = $$`<div class="ve-flex-col w-100 h-100 overflow-y-auto relative"/>`;
|
||||
this._childComps.forEach(it => it.render(this._$wrpRows, pod));
|
||||
|
||||
const $btnAdd = $(`<button class="btn btn-primary btn-xs"><span class="glyphicon glyphicon-plus"/> Add Counter</button>`)
|
||||
.click(() => {
|
||||
const comp = new CounterRow(this._board, this._$wrpPanel);
|
||||
this._childComps.push(comp);
|
||||
comp.render(this._$wrpRows, pod);
|
||||
this._board.doSaveStateDebounced();
|
||||
});
|
||||
|
||||
$$`<div class="w-100 h-100 ve-flex-col px-2 pb-3">
|
||||
<div class="no-shrink pt-4"/>
|
||||
${this._$wrpRows}
|
||||
<div class="no-shrink ve-flex-h-right">${$btnAdd}</div>
|
||||
</div>`.appendTo($parent);
|
||||
}
|
||||
|
||||
_swapRowPositions (ixA, ixB) {
|
||||
const a = this._childComps[ixA];
|
||||
this._childComps[ixA] = this._childComps[ixB];
|
||||
this._childComps[ixB] = a;
|
||||
|
||||
this._childComps.forEach(it => it.$row.detach().appendTo(this._$wrpRows));
|
||||
|
||||
this._board.doSaveStateDebounced();
|
||||
}
|
||||
|
||||
_removeRow (comp) {
|
||||
const ix = this._childComps.indexOf(comp);
|
||||
if (~ix) {
|
||||
this._childComps.splice(ix, 1);
|
||||
comp.$row.remove();
|
||||
this._board.doSaveStateDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
getPod () {
|
||||
const pod = super.getPod();
|
||||
pod.swapRowPositions = this._swapRowPositions.bind(this);
|
||||
pod.removeRow = this._removeRow.bind(this);
|
||||
pod.$getChildren = () => this._childComps.map(comp => comp.$row);
|
||||
return pod;
|
||||
}
|
||||
|
||||
setStateFrom (toLoad) {
|
||||
this.setBaseSaveableStateFrom(toLoad);
|
||||
this._childComps = [];
|
||||
|
||||
if (toLoad.rowState) {
|
||||
toLoad.rowState.forEach(r => {
|
||||
const comp = new CounterRow(this._board, this._$wrpPanel);
|
||||
comp.setStateFrom(r);
|
||||
this._childComps.push(comp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSaveableState () {
|
||||
return {
|
||||
...this.getBaseSaveableState(),
|
||||
rowState: this._childComps.map(r => r.getSaveableState()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CounterRow extends CounterComponent {
|
||||
constructor (board, $wrpPanel) {
|
||||
super(board, $wrpPanel);
|
||||
|
||||
this._$row = null;
|
||||
}
|
||||
|
||||
get $row () { return this._$row; }
|
||||
|
||||
render ($parent, parent) {
|
||||
this._parent = parent;
|
||||
|
||||
const $iptName = ComponentUiUtil.$getIptStr(this, "name").addClass("mr-2 small-caps");
|
||||
|
||||
const $iptCur = ComponentUiUtil.$getIptInt(this, "current", 0, {$ele: $(`<input class="form-control input-xs form-control--minimal ve-text-center dm-cnt__ipt dm-cnt__ipt--cur bold">`)});
|
||||
const $iptMax = ComponentUiUtil.$getIptInt(this, "max", 0, {$ele: $(`<input class="form-control input-xs form-control--minimal ve-text-center dm-cnt__ipt dm-cnt__ipt--max mr-2 text-muted bold">`)});
|
||||
|
||||
const hookDisplayMinMax = () => {
|
||||
$iptCur.removeClass("text-success text-danger");
|
||||
if (this._state.current >= this._state.max) $iptCur.addClass("text-success");
|
||||
else if (this._state.current <= 0) $iptCur.addClass("text-danger");
|
||||
};
|
||||
this._addHookBase("current", hookDisplayMinMax);
|
||||
this._addHookBase("max", hookDisplayMinMax);
|
||||
hookDisplayMinMax();
|
||||
|
||||
const $btnDown = $(`<button class="btn btn-danger btn-xs"><span class="glyphicon glyphicon-minus"/></button>`)
|
||||
.click(() => this._state.current--);
|
||||
|
||||
const $btnUp = $(`<button class="btn btn-success btn-xs"><span class="glyphicon glyphicon-plus"/></button>`)
|
||||
.click(() => this._state.current++);
|
||||
|
||||
const $btnRemove = $(`<button class="btn btn-danger btn-xxs"><span class="glyphicon glyphicon-trash"/></button>`)
|
||||
.click(() => {
|
||||
const {removeRow} = this._parent;
|
||||
removeRow(this);
|
||||
});
|
||||
|
||||
this._$row = $$`<div class="ve-flex-v-center w-100 my-1">
|
||||
${$iptName}
|
||||
<div class="relative ve-flex-vh-center">
|
||||
${$iptCur}
|
||||
<div class="dm-cnt__slash text-muted ve-text-center">/</div>
|
||||
${$iptMax}
|
||||
</div>
|
||||
<div class="ve-flex btn-group mr-2">
|
||||
${$btnDown}
|
||||
${$btnUp}
|
||||
</div>
|
||||
|
||||
${DragReorderUiUtil.$getDragPad2(() => this._$row, $parent, this._parent)}
|
||||
${$btnRemove}
|
||||
</div>`.appendTo($parent);
|
||||
}
|
||||
|
||||
_getDefaultState () { return MiscUtil.copy(CounterRow._DEFAULT_STATE); }
|
||||
}
|
||||
CounterRow._DEFAULT_STATE = {
|
||||
name: "",
|
||||
current: 0,
|
||||
max: 1,
|
||||
};
|
||||
209
js/dmscreen/dmscreen-initiativetrackercreatureviewer.js
Normal file
209
js/dmscreen/dmscreen-initiativetrackercreatureviewer.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import {DmScreenUtil} from "./dmscreen-util.js";
|
||||
import {PANEL_TYP_INITIATIVE_TRACKER} from "./dmscreen-consts.js";
|
||||
|
||||
export class InitiativeTrackerCreatureViewer extends BaseComponent {
|
||||
static $getPanelElement (board, savedState) {
|
||||
return new this({board, savedState}).render();
|
||||
}
|
||||
|
||||
constructor ({board, savedState}) {
|
||||
super();
|
||||
this._board = board;
|
||||
this._savedState = savedState;
|
||||
|
||||
this._trackerLinked = null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
render () {
|
||||
const $out = $$`<div class="ve-flex-col w-100 h-100 min-h-0 dm__data-anchor">
|
||||
${this._render_$getStgNoTrackerAvailable()}
|
||||
${this._render_$getStgConnect()}
|
||||
${this._render_$getStgCreature()}
|
||||
</div>`;
|
||||
|
||||
this._addHookBase("cntPanelsAvailable", (prop, val, prev) => {
|
||||
if (prop == null) return;
|
||||
if (prev || val !== 1) return;
|
||||
|
||||
this._setLinkedTrackerFromEle({
|
||||
$eleData: DmScreenUtil.$getPanelDataElements({board: this._board, type: PANEL_TYP_INITIATIVE_TRACKER})[0],
|
||||
});
|
||||
})();
|
||||
|
||||
const hkBoardPanels = () => {
|
||||
const $elesData = DmScreenUtil.$getPanelDataElements({board: this._board, type: PANEL_TYP_INITIATIVE_TRACKER});
|
||||
this._state.cntPanelsAvailable = $elesData.length;
|
||||
};
|
||||
hkBoardPanels();
|
||||
|
||||
$out
|
||||
.data("onDestroy", () => {
|
||||
if (!this._trackerLinked) return;
|
||||
this._trackerLinked.doDisconnectCreatureViewer({creatureViewer: this});
|
||||
this._state.isActive = false;
|
||||
})
|
||||
.data("onBoardEvent", ({type, payload = {}}) => {
|
||||
if (
|
||||
!(
|
||||
type === "panelDestroy"
|
||||
|| (type === "panelPopulate" && payload.type === PANEL_TYP_INITIATIVE_TRACKER)
|
||||
|| (type === "panelIdSetActive" && payload.type === PANEL_TYP_INITIATIVE_TRACKER)
|
||||
)
|
||||
) return;
|
||||
|
||||
hkBoardPanels();
|
||||
});
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
_render_$getStgNoTrackerAvailable () {
|
||||
const $stg = $$`<div class="ve-flex-vh-center w-100 h-100 min-h-0">
|
||||
<div class="dnd-font italic small-caps ve-muted">No Initiative Tracker available.</div>
|
||||
</div>`;
|
||||
|
||||
const hkIsVisible = () => $stg.toggleVe(!this._state.isActive && !this._state.cntPanelsAvailable);
|
||||
this._addHookBase("isActive", hkIsVisible);
|
||||
this._addHookBase("cntPanelsAvailable", hkIsVisible);
|
||||
hkIsVisible();
|
||||
|
||||
return $stg;
|
||||
}
|
||||
|
||||
_render_$getStgConnect () {
|
||||
const $btnConnect = $(`<button class="btn btn-primary min-w-200p">Connect to Tracker</button>`)
|
||||
.on("click", async () => {
|
||||
const $elesData = DmScreenUtil.$getPanelDataElements({board: this._board, type: PANEL_TYP_INITIATIVE_TRACKER});
|
||||
|
||||
if ($elesData.length === 1) return this._setLinkedTrackerFromEle({$eleData: $elesData[0]});
|
||||
|
||||
const {$modalInner, doClose, pGetResolved} = UiUtil.getShowModal({
|
||||
isMinHeight0: true,
|
||||
isHeaderBorder: true,
|
||||
title: "Select Tracker",
|
||||
});
|
||||
|
||||
const $selTracker = $(`<select class="form-control input-xs mr-1">
|
||||
<option value="-1" disabled>Select tracker</option>
|
||||
${$elesData.map(($e, i) => `<option value="${i}">${$e.data("getSummary")()}</option>`).join("")}
|
||||
</select>`)
|
||||
.on("change", () => $selTracker.removeClass("error-background"));
|
||||
|
||||
const $btnSubmit = $(`<button class="btn btn-primary btn-xs">Connect</button>`)
|
||||
.on("click", () => {
|
||||
const ix = Number($selTracker.val());
|
||||
if (!~ix) {
|
||||
$selTracker.addClass("error-background");
|
||||
return;
|
||||
}
|
||||
|
||||
doClose(true, ix);
|
||||
});
|
||||
|
||||
$$($modalInner)`
|
||||
${$selTracker}
|
||||
${$btnSubmit}
|
||||
`;
|
||||
|
||||
const ixSel = await pGetResolved();
|
||||
if (ixSel == null) return;
|
||||
|
||||
this._setLinkedTrackerFromEle({$eleData: $elesData[ixSel]});
|
||||
});
|
||||
|
||||
const $stg = $$`<div class="ve-flex-vh-center w-100 h-100 min-h-0">
|
||||
${$btnConnect}
|
||||
</div>`;
|
||||
|
||||
const hkIsVisible = () => $stg.toggleVe(!this._state.isActive && this._state.cntPanelsAvailable);
|
||||
this._addHookBase("isActive", hkIsVisible);
|
||||
this._addHookBase("cntPanelsAvailable", hkIsVisible);
|
||||
hkIsVisible();
|
||||
|
||||
return $stg;
|
||||
}
|
||||
|
||||
_render_$getStgCreature () {
|
||||
const dispCreature = e_({
|
||||
tag: "div",
|
||||
clazz: "ve-flex-col w-100 h-100 min-h-0",
|
||||
});
|
||||
|
||||
const lock = new VeLock({name: "Creature display"});
|
||||
|
||||
const hkCreature = async () => {
|
||||
const mon = (this._state.creatureName && this._state.creatureSource)
|
||||
? await DmScreenUtil.pGetScaledCreature({
|
||||
name: this._state.creatureName,
|
||||
source: this._state.creatureSource,
|
||||
scaledCr: this._state.creatureScaledCr,
|
||||
scaledSummonSpellLevel: this._state.creatureScaledSummonSpellLevel,
|
||||
scaledSummonClassLevel: this._state.creatureScaledSummonClassLevel,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!mon) return dispCreature.innerHTML = `<div class="dnd-font italic small-caps ve-muted ve-flex-vh-center w-100 h-100">No active creature.</div>`;
|
||||
|
||||
dispCreature.innerHTML = `<table class="w-100 stats"><tbody>${Renderer.monster.getCompactRenderedString(mon, {isShowScalers: false})}</tbody></table>`;
|
||||
};
|
||||
|
||||
this._addHookBase("creaturePulse", async () => {
|
||||
try {
|
||||
await lock.pLock();
|
||||
await hkCreature();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
})().then(null);
|
||||
|
||||
const $stg = $$`<div class="ve-flex-col w-100 h-100 min-h-0 overflow-y-auto">
|
||||
${dispCreature}
|
||||
</div>`;
|
||||
|
||||
this._addHookBase("isActive", () => $stg.toggleVe(this._state.isActive))();
|
||||
|
||||
return $stg;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_setLinkedTrackerFromEle ({$eleData}) {
|
||||
this._trackerLinked = $eleData.data("getApi")().doConnectCreatureViewer({creatureViewer: this});
|
||||
this._state.isActive = true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
setCreatureState (state) {
|
||||
if (state == null) return;
|
||||
|
||||
this._proxyAssignSimple(
|
||||
"state",
|
||||
Object.fromEntries(
|
||||
Object.entries(state)
|
||||
.map(([k, v]) => [`creature${k.uppercaseFirst()}`, v]),
|
||||
),
|
||||
);
|
||||
|
||||
// Avoid render spam
|
||||
this._state.creaturePulse = !this._state.creaturePulse;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_getDefaultState () {
|
||||
return {
|
||||
isActive: false,
|
||||
cntPanelsAvailable: 0,
|
||||
|
||||
creatureName: null,
|
||||
creatureSource: null,
|
||||
creatureScaledCr: null,
|
||||
creatureScaledSummonSpellLevel: null,
|
||||
creatureScaledSummonClassLevel: null,
|
||||
creaturePulse: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
142
js/dmscreen/dmscreen-mapper.js
Normal file
142
js/dmscreen/dmscreen-mapper.js
Normal file
@@ -0,0 +1,142 @@
|
||||
export class DmMapper {
|
||||
static $getMapper (board, state) {
|
||||
const $wrpPanel = $(`<div class="w-100 h-100 dm-map__root dm__panel-bg dm__data-anchor"/>`) // root class used to identify for saving
|
||||
.data("getState", () => mapper.getSaveableState());
|
||||
const mapper = new DmMapperRoot(board, $wrpPanel);
|
||||
mapper.setStateFrom(state);
|
||||
mapper.render($wrpPanel);
|
||||
return $wrpPanel;
|
||||
}
|
||||
|
||||
static _getProps ({catId}) {
|
||||
const prop = catId === Parser.CAT_ID_ADVENTURE ? "adventure" : "book";
|
||||
return {prop, propData: `${prop}Data`};
|
||||
}
|
||||
|
||||
static async pHandleMenuButtonClick (menu) {
|
||||
const chosenDoc = await SearchWidget.pGetUserAdventureBookSearch({
|
||||
fnFilterResults: doc => doc.hasMaps,
|
||||
contentIndexName: "entity_AdventuresBooks_maps",
|
||||
pFnGetDocExtras: async ({doc}) => {
|
||||
// Load the adventure/book, and scan it for maps
|
||||
const {propData} = this._getProps({catId: doc.c});
|
||||
const {page, source, hash} = SearchWidget.docToPageSourceHash(doc);
|
||||
const adventureBookPack = await DataLoader.pCacheAndGet(page, source, hash);
|
||||
let hasMaps = false;
|
||||
const walker = MiscUtil.getWalker({
|
||||
isBreakOnReturn: true,
|
||||
keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST,
|
||||
isNoModification: true,
|
||||
});
|
||||
walker.walk(
|
||||
adventureBookPack[propData],
|
||||
{
|
||||
object: (obj) => {
|
||||
if (obj.type === "image" && obj.mapRegions?.length) return hasMaps = true;
|
||||
},
|
||||
},
|
||||
);
|
||||
return {hasMaps};
|
||||
},
|
||||
});
|
||||
|
||||
if (!chosenDoc) return;
|
||||
|
||||
menu.doClose();
|
||||
|
||||
const {$modalInner, doClose} = UiUtil.getShowModal({
|
||||
title: `Select Map\u2014${chosenDoc.n}`,
|
||||
isWidth100: true,
|
||||
isHeight100: true,
|
||||
isUncappedHeight: true,
|
||||
});
|
||||
|
||||
$modalInner.append(`<div class="ve-flex-vh-center w-100 h-100"><i class="dnd-font ve-muted">Loading...</i></div>`);
|
||||
|
||||
const {page, source, hash} = SearchWidget.docToPageSourceHash(chosenDoc);
|
||||
const adventureBookPack = await DataLoader.pCacheAndGet(page, source, hash);
|
||||
|
||||
const mapDatas = [];
|
||||
const walker = MiscUtil.getWalker();
|
||||
|
||||
const {prop, propData} = this._getProps({catId: chosenDoc.c});
|
||||
|
||||
adventureBookPack[propData].data.forEach((chap, ixChap) => {
|
||||
let cntChapImages = 0;
|
||||
|
||||
const handlers = {
|
||||
object (obj) {
|
||||
if (obj.mapRegions) {
|
||||
const out = {
|
||||
...Renderer.get().getMapRegionData(obj),
|
||||
page: chosenDoc.q,
|
||||
source: adventureBookPack[prop].source,
|
||||
hash: UrlUtil.URL_TO_HASH_BUILDER[chosenDoc.q](adventureBookPack[prop]),
|
||||
};
|
||||
mapDatas.push(out);
|
||||
|
||||
if (obj.title) {
|
||||
out.name = Renderer.stripTags(obj.title);
|
||||
} else {
|
||||
out.name = `${(adventureBookPack[prop].contents[ixChap] || {}).name || "(Unknown)"}, Map ${cntChapImages + 1}`;
|
||||
}
|
||||
|
||||
cntChapImages++;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
};
|
||||
|
||||
walker.walk(
|
||||
chap,
|
||||
handlers,
|
||||
);
|
||||
});
|
||||
|
||||
if (!mapDatas.length) {
|
||||
$modalInner
|
||||
.empty()
|
||||
.append(`<div class="ve-flex-vh-center w-100 h-100"><span class="dnd-font">Adventure did not contain any valid maps!</span></div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
$modalInner
|
||||
.empty()
|
||||
.removeClass("ve-flex-col")
|
||||
.addClass("ve-text-center");
|
||||
|
||||
mapDatas.map(mapData => {
|
||||
$(`<div class="m-1 p-1 clickable dm-map__picker-wrp-img relative">
|
||||
<div class="dm-map__picker-img" style="background-image: url(${encodeURI(mapData.hrefThumbnail || mapData.href)})"></div>
|
||||
<span class="absolute ve-text-center dm-map__picker-disp-name">${mapData.name.escapeQuotes()}</span>
|
||||
</div>`)
|
||||
.click(() => {
|
||||
doClose();
|
||||
menu.pnl.doPopulate_AdventureBookDynamicMap({state: mapData});
|
||||
})
|
||||
.appendTo($modalInner);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DmMapperRoot extends BaseComponent {
|
||||
/**
|
||||
* @param board DM Screen board.
|
||||
* @param $wrpPanel Panel wrapper element for us to populate.
|
||||
*/
|
||||
constructor (board, $wrpPanel) {
|
||||
super();
|
||||
this._board = board;
|
||||
this._$wrpPanel = $wrpPanel;
|
||||
}
|
||||
|
||||
render ($parent) {
|
||||
$parent.empty();
|
||||
|
||||
$parent.append(`<div class="ve-flex-vh-center w-100 h-100"><i class="dnd-font ve-muted">Loading...</i></div>`);
|
||||
|
||||
RenderMap.$pGetRendered(this._state)
|
||||
.then($ele => $parent.empty().append($ele));
|
||||
}
|
||||
}
|
||||
224
js/dmscreen/dmscreen-moneyconverter.js
Normal file
224
js/dmscreen/dmscreen-moneyconverter.js
Normal file
@@ -0,0 +1,224 @@
|
||||
// a simple money converter, i.e.: input x electrum, y silver, z copper and get the total in gold, or in any other type of coin chosen.
|
||||
export class MoneyConverter {
|
||||
static make$Converter (board, state) {
|
||||
const disabledCurrency = state.d || {};
|
||||
|
||||
const COIN_WEIGHT = 0.02;
|
||||
const CURRENCY = [
|
||||
new MoneyConverterUnit("Copper", 1, "cp"),
|
||||
new MoneyConverterUnit("Silver", 10, "sp"),
|
||||
new MoneyConverterUnit("Electrum", 50, "ep"),
|
||||
new MoneyConverterUnit("Gold", 100, "gp"),
|
||||
new MoneyConverterUnit("Platinum", 1000, "pp"),
|
||||
new MoneyConverterUnit("Nib (WDH)", 1, "nib"),
|
||||
new MoneyConverterUnit("Shard (WDH)", 10, "shard"),
|
||||
new MoneyConverterUnit("Taol (WDH)", 200, "taol"),
|
||||
new MoneyConverterUnit("Dragon (WDH)", 100, "dgn"),
|
||||
new MoneyConverterUnit("Sun (WDH)", 1000, "sun"),
|
||||
new MoneyConverterUnit("Harbor Moon (WDH)", 5000, "moon"),
|
||||
];
|
||||
const CURRENCY_INDEXED = [...CURRENCY].map((it, i) => {
|
||||
it.ix = i;
|
||||
return it;
|
||||
}).reverse();
|
||||
const DEFAULT_CURRENCY = 3;
|
||||
|
||||
const $wrpConverter = $(`<div class="dm_money dm__panel-bg split-column"/>`);
|
||||
|
||||
const doUpdate = () => {
|
||||
if (!$wrpRows.find(`.dm-money__row`).length) {
|
||||
addRow();
|
||||
}
|
||||
|
||||
Object.entries(disabledCurrency).forEach(([currency, disabled]) => {
|
||||
$selOut.find(`option[value=${currency}]`).toggle(!disabled);
|
||||
});
|
||||
// if the current choice is disabled, deselect it, and restart
|
||||
if (disabledCurrency[$selOut.val()]) {
|
||||
$selOut.val("-1");
|
||||
doUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
const $rows = $wrpRows.find(`.dm-money__row`)
|
||||
.removeClass("form-control--error");
|
||||
$iptSplit.removeClass("form-control--error");
|
||||
|
||||
const outCurrency = Number($selOut.val()) || 0;
|
||||
|
||||
const outParts = [];
|
||||
let totalWeight = 0;
|
||||
|
||||
const splitBetweenStr = ($iptSplit.val() || "").trim();
|
||||
let split = 1;
|
||||
if (splitBetweenStr) {
|
||||
const splitBetweenNum = Number(splitBetweenStr);
|
||||
if (isNaN(splitBetweenNum)) $iptSplit.addClass("form-control--error");
|
||||
else split = splitBetweenNum;
|
||||
}
|
||||
|
||||
if (outCurrency === -1) { // only split, don't convert
|
||||
const totals = [];
|
||||
const extras = [];
|
||||
const allowedCategories = new Set();
|
||||
|
||||
$rows.each((i, e) => {
|
||||
const $e = $(e);
|
||||
const strVal = ($e.find(`input`).val() || "").trim();
|
||||
if (strVal) {
|
||||
const asNum = Number(strVal);
|
||||
if (isNaN(asNum)) $e.addClass("form-control--error");
|
||||
else {
|
||||
const ix = Number($e.find(`select`).val());
|
||||
totals[ix] = (totals[ix] || 0) + asNum;
|
||||
allowedCategories.add(CURRENCY[ix]._cat);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (split > 1) {
|
||||
CURRENCY_INDEXED.forEach((c, i) => {
|
||||
const it = totals[c.ix];
|
||||
if (it) {
|
||||
let remainder = (it % split) * c.mult;
|
||||
totals[c.ix] = Math.floor(it / split);
|
||||
|
||||
for (let j = i + 1; j < CURRENCY_INDEXED.length; ++j) {
|
||||
const nxtCurrency = CURRENCY_INDEXED[j];
|
||||
|
||||
// skip and convert to a smaller denomination as required
|
||||
if (disabledCurrency[nxtCurrency.ix]) continue;
|
||||
|
||||
if (remainder >= nxtCurrency.mult) {
|
||||
totals[nxtCurrency.ix] = (totals[nxtCurrency.ix] || 0) + Math.floor(remainder / nxtCurrency.mult);
|
||||
remainder %= nxtCurrency.mult;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CURRENCY_INDEXED.forEach(c => {
|
||||
const it = totals[c.ix] || 0;
|
||||
const itExtra = extras[c.ix] || 0;
|
||||
if (it || itExtra) {
|
||||
const val = it + itExtra;
|
||||
totalWeight += val * COIN_WEIGHT;
|
||||
outParts.push(`${val.toLocaleString()} ${c.abbv}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let total = 0;
|
||||
$rows.each((i, e) => {
|
||||
const $e = $(e);
|
||||
const strVal = ($e.find(`input`).val() || "").trim();
|
||||
if (strVal) {
|
||||
const asNum = Number(strVal);
|
||||
if (isNaN(asNum)) $e.addClass("form-control--error");
|
||||
else {
|
||||
total += asNum * (CURRENCY[$e.find(`select`).val()] || CURRENCY[0]).mult;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const totalSplit = Math.floor(total / split);
|
||||
|
||||
const toCurrencies = CURRENCY_INDEXED.filter(it => !disabledCurrency[it.ix] && it.ix <= outCurrency);
|
||||
let copper = totalSplit;
|
||||
toCurrencies.forEach(c => {
|
||||
if (copper >= c.mult) {
|
||||
const remainder = copper % c.mult;
|
||||
const theseCoins = Math.floor(copper / c.mult);
|
||||
totalWeight += COIN_WEIGHT * theseCoins;
|
||||
copper = remainder;
|
||||
outParts.push(`${theseCoins.toLocaleString()} ${c.abbv}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$iptOut.val(`${outParts.join("; ")}${totalWeight ? ` (${totalWeight.toLocaleString()} lb.)` : ""}`);
|
||||
|
||||
board.doSaveStateDebounced();
|
||||
};
|
||||
|
||||
const buildCurrency$Select = (isOutput) => $(`<select class="form-control input-sm" style="padding: 5px">${isOutput ? `<option value="-1">(No conversion)</option>` : ""}${CURRENCY.map((c, i) => `<option value="${i}">${c.n}</option>`).join("")}</select>`);
|
||||
|
||||
const addRow = (currency, count) => {
|
||||
const $row = $(`<div class="dm-money__row"/>`).appendTo($wrpRows);
|
||||
const $iptCount = $(`<input type="number" step="1" placeholder="Coins" class="form-control input-sm">`).appendTo($row).change(doUpdate);
|
||||
if (count != null) $iptCount.val(count);
|
||||
const $selCurrency = buildCurrency$Select().appendTo($row).change(doUpdate);
|
||||
$selCurrency.val(currency == null ? DEFAULT_CURRENCY : currency);
|
||||
const $btnRemove = $(`<button class="btn btn-sm btn-danger" title="Remove Row"><span class="glyphicon glyphicon-trash"></span></button>`).appendTo($row).click(() => {
|
||||
$row.remove();
|
||||
doUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
const $wrpRows = $(`<div class="dm-money__rows"/>`).appendTo($wrpConverter);
|
||||
|
||||
const $wrpCtrl = $(`<div class="split dm-money__ctrl"/>`).appendTo($wrpConverter);
|
||||
const $wrpCtrlLhs = $(`<div class="dm-money__ctrl__lhs split-child" style="width: 66%;"/>`).appendTo($wrpCtrl);
|
||||
const $wrpBtnAddSettings = $(`<div class="split"/>`).appendTo($wrpCtrlLhs);
|
||||
const $btnAddRow = $(`<button class="btn btn-primary btn-sm" title="Add Row"><span class="glyphicon glyphicon-plus"/></button>`)
|
||||
.appendTo($wrpBtnAddSettings)
|
||||
.click(() => {
|
||||
addRow();
|
||||
doUpdate();
|
||||
});
|
||||
const $btnSettings = $(`<button class="btn btn-default btn-sm" title="Settings"><span class="glyphicon glyphicon-cog"/></button>`)
|
||||
.appendTo($wrpBtnAddSettings)
|
||||
.click(() => {
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
title: "Settings",
|
||||
cbClose: () => doUpdate(),
|
||||
});
|
||||
[...CURRENCY_INDEXED].reverse().forEach(cx => {
|
||||
UiUtil.$getAddModalRowCb($modalInner, `Disable ${cx.n} in Output`, disabledCurrency, cx.ix);
|
||||
});
|
||||
});
|
||||
const $iptOut = $(`<input class="form-control input-sm dm-money__out" disabled/>`)
|
||||
.appendTo($wrpCtrlLhs)
|
||||
.mousedown(async () => {
|
||||
await MiscUtil.pCopyTextToClipboard($iptOut.val());
|
||||
JqueryUtil.showCopiedEffect($iptOut);
|
||||
});
|
||||
|
||||
const $wrpCtrlRhs = $(`<div class="dm-money__ctrl__rhs split-child" style="width: 33%;"/>`).appendTo($wrpCtrl);
|
||||
const $iptSplit = $(`<input type="number" min="1" step="1" placeholder="Split Between..." class="form-control input-sm">`).appendTo($wrpCtrlRhs).change(doUpdate);
|
||||
const $selOut = buildCurrency$Select(true).appendTo($wrpCtrlRhs).change(doUpdate);
|
||||
|
||||
$wrpConverter.data("getState", () => {
|
||||
return {
|
||||
c: $selOut.val(),
|
||||
s: $iptSplit.val(),
|
||||
r: $wrpRows.find(`.dm-money__row`).map((i, e) => {
|
||||
const $e = $(e);
|
||||
return {
|
||||
c: $e.find(`select`).val(),
|
||||
n: $e.find(`input`).val(),
|
||||
};
|
||||
}).get(),
|
||||
d: disabledCurrency,
|
||||
};
|
||||
});
|
||||
|
||||
if (state) {
|
||||
$selOut.val(state.c == null ? DEFAULT_CURRENCY : state.c);
|
||||
$iptSplit.val(state.s);
|
||||
(state.r || []).forEach(r => addRow(r.c, r.n));
|
||||
}
|
||||
|
||||
doUpdate();
|
||||
|
||||
return $wrpConverter;
|
||||
}
|
||||
}
|
||||
|
||||
class MoneyConverterUnit {
|
||||
constructor (name, multiplier, abbreviation) {
|
||||
this.n = name;
|
||||
this.mult = multiplier;
|
||||
this.abbv = abbreviation;
|
||||
}
|
||||
}
|
||||
165
js/dmscreen/dmscreen-panels.js
Normal file
165
js/dmscreen/dmscreen-panels.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
PANEL_TYP_INITIATIVE_TRACKER, PANEL_TYP_INITIATIVE_TRACKER_CREATURE_VIEWER,
|
||||
PANEL_TYP_INITIATIVE_TRACKER_PLAYER_V0,
|
||||
PANEL_TYP_INITIATIVE_TRACKER_PLAYER_V1,
|
||||
} from "./dmscreen-consts.js";
|
||||
import {InitiativeTracker} from "./initiativetracker/dmscreen-initiativetracker.js";
|
||||
import {InitiativeTrackerPlayerV0, InitiativeTrackerPlayerV1} from "./dmscreen-playerinitiativetracker.js";
|
||||
import {InitiativeTrackerCreatureViewer} from "./dmscreen-initiativetrackercreatureviewer.js";
|
||||
|
||||
export class PanelContentManagerFactory {
|
||||
static _PANEL_TYPES = {};
|
||||
|
||||
static registerPanelType ({panelType, Cls}) {
|
||||
this._PANEL_TYPES[panelType] = Cls;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static async pFromSavedState ({board, saved, ixTab, panel}) {
|
||||
if (!this._PANEL_TYPES[saved.t]) return undefined;
|
||||
|
||||
const ContentManager = new this._PANEL_TYPES[saved.t]({board, panel});
|
||||
await ContentManager.pLoadState({ixTab, saved});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static getSaveableContent (
|
||||
{
|
||||
type,
|
||||
toSaveTitle,
|
||||
$content,
|
||||
},
|
||||
) {
|
||||
if (!this._PANEL_TYPES[type]) return undefined;
|
||||
|
||||
return this._PANEL_TYPES[type]
|
||||
.getSaveableContent({
|
||||
type,
|
||||
toSaveTitle,
|
||||
$content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
class _PanelContentManager {
|
||||
static _PANEL_TYPE = null;
|
||||
static _TITLE = null;
|
||||
static _IS_STATELESS = false;
|
||||
|
||||
static _register () {
|
||||
PanelContentManagerFactory.registerPanelType({panelType: this._PANEL_TYPE, Cls: this});
|
||||
return null;
|
||||
}
|
||||
|
||||
static getSaveableContent (
|
||||
{
|
||||
type,
|
||||
toSaveTitle,
|
||||
$content,
|
||||
},
|
||||
) {
|
||||
return {
|
||||
t: type,
|
||||
r: toSaveTitle,
|
||||
s: this._IS_STATELESS
|
||||
? {}
|
||||
: $($content.children()[0]).data("getState")(),
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
constructor (
|
||||
{
|
||||
board,
|
||||
panel,
|
||||
},
|
||||
) {
|
||||
this._board = board;
|
||||
this._panel = panel;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {jQuery}
|
||||
*/
|
||||
_$getPanelElement ({state}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
async pDoPopulate ({state = {}, title = null} = {}) {
|
||||
this._panel.set$ContentTab(
|
||||
this.constructor._PANEL_TYPE,
|
||||
state,
|
||||
$(`<div class="panel-content-wrapper-inner"></div>`).append(this._$getPanelElement({state})),
|
||||
title || this.constructor._TITLE,
|
||||
true,
|
||||
);
|
||||
|
||||
this._board.fireBoardEvent({type: "panelPopulate", payload: {type: this.constructor._PANEL_TYPE}});
|
||||
}
|
||||
|
||||
_doHandleTabRenamed ({ixTab, saved}) {
|
||||
if (saved.r != null) this._panel.tabDatas[ixTab].tabRenamed = true;
|
||||
}
|
||||
|
||||
async pLoadState ({ixTab, saved}) {
|
||||
await this.pDoPopulate({state: saved.s, title: saved.r});
|
||||
this._doHandleTabRenamed({ixTab, saved});
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelContentManager_InitiativeTracker extends _PanelContentManager {
|
||||
static _PANEL_TYPE = PANEL_TYP_INITIATIVE_TRACKER;
|
||||
static _TITLE = "Initiative Tracker";
|
||||
|
||||
static _ = this._register();
|
||||
|
||||
_$getPanelElement ({state}) {
|
||||
return InitiativeTracker.$getPanelElement(this._board, state);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelContentManager_InitiativeTrackerCreatureViewer extends _PanelContentManager {
|
||||
static _PANEL_TYPE = PANEL_TYP_INITIATIVE_TRACKER_CREATURE_VIEWER;
|
||||
static _TITLE = "Creature Viewer";
|
||||
static _IS_STATELESS = true;
|
||||
|
||||
static _ = this._register();
|
||||
|
||||
_$getPanelElement ({state}) {
|
||||
return InitiativeTrackerCreatureViewer.$getPanelElement(this._board, state);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelContentManager_InitiativeTrackerPlayerViewV1 extends _PanelContentManager {
|
||||
static _PANEL_TYPE = PANEL_TYP_INITIATIVE_TRACKER_PLAYER_V1;
|
||||
static _TITLE = "Initiative Tracker";
|
||||
static _IS_STATELESS = true;
|
||||
|
||||
static _ = this._register();
|
||||
|
||||
_$getPanelElement ({state}) {
|
||||
return InitiativeTrackerPlayerV1.$getPanelElement(this._board, state);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelContentManager_InitiativeTrackerPlayerViewV0 extends _PanelContentManager {
|
||||
static _PANEL_TYPE = PANEL_TYP_INITIATIVE_TRACKER_PLAYER_V0;
|
||||
static _TITLE = "Initiative Tracker";
|
||||
static _IS_STATELESS = true;
|
||||
|
||||
static _ = this._register();
|
||||
|
||||
_$getPanelElement ({state}) {
|
||||
return InitiativeTrackerPlayerV0.$getPanelElement(this._board, state);
|
||||
}
|
||||
}
|
||||
323
js/dmscreen/dmscreen-playerinitiativetracker.js
Normal file
323
js/dmscreen/dmscreen-playerinitiativetracker.js
Normal file
@@ -0,0 +1,323 @@
|
||||
import {PANEL_TYP_INITIATIVE_TRACKER} from "./dmscreen-consts.js";
|
||||
import {
|
||||
InitiativeTrackerPlayerMessageHandlerV0,
|
||||
InitiativeTrackerPlayerMessageHandlerV1,
|
||||
InitiativeTrackerPlayerUiV0,
|
||||
InitiativeTrackerPlayerUiV1,
|
||||
} from "../initiativetracker/initiativetracker-player.js";
|
||||
import {DmScreenUtil} from "./dmscreen-util.js";
|
||||
|
||||
// region v1
|
||||
export class InitiativeTrackerPlayerV1 {
|
||||
static $getPanelElement (board, state) {
|
||||
const $meta = $(`<div class="initp__meta"/>`).hide();
|
||||
const $head = $(`<div class="initp__header"/>`).hide();
|
||||
const $rows = $(`<div class="ve-flex-col"></div>`).hide();
|
||||
|
||||
const $wrpTracker = $$`<div class="initp__wrp_active">
|
||||
${$meta}
|
||||
${$head}
|
||||
${$rows}
|
||||
</div>`;
|
||||
|
||||
const view = new InitiativeTrackerPlayerMessageHandlerScreenV1();
|
||||
view.setElements($meta, $head, $rows);
|
||||
|
||||
let ui;
|
||||
const $btnConnectRemote = $(`<button class="btn btn-primary mb-2 min-w-200p" title="Connect to a tracker outside of this browser tab.">Connect to Remote Tracker</button>`)
|
||||
.click(async () => {
|
||||
$btnConnectRemote.detach();
|
||||
$btnConnectLocal.detach();
|
||||
|
||||
const $iptPlayerName = $(`<input class="form-control input-sm code">`)
|
||||
.change(() => $iptPlayerName.removeClass("form-control--error"))
|
||||
.disableSpellcheck();
|
||||
const $iptServerToken = $(`<input class="form-control input-sm code">`)
|
||||
.change(() => $iptServerToken.removeClass("form-control--error"))
|
||||
.disableSpellcheck();
|
||||
const $btnGenConnect = $(`<button class="btn btn-primary btn-xs mr-2">Connect</button>`);
|
||||
|
||||
const $btnCancel = $(`<button class="btn btn-default btn-xs">Back</button>`)
|
||||
.click(() => {
|
||||
// restore original state
|
||||
$wrpClient.remove();
|
||||
view.$wrpInitial.append($btnConnectRemote).append($btnConnectLocal);
|
||||
});
|
||||
|
||||
const $wrpClient = $$`<div class="ve-flex-col w-100">
|
||||
<div class="ve-flex-vh-center px-4 mb-2">
|
||||
<span style="min-width: fit-content;" class="mr-2">Player Name</span>
|
||||
${$iptPlayerName}
|
||||
</div>
|
||||
|
||||
<div class="ve-flex-vh-center px-4 mb-2">
|
||||
<span style="min-width: fit-content;" class="mr-2">Server Token</span>
|
||||
${$iptServerToken}
|
||||
</div>
|
||||
|
||||
<div class="split px-4 ve-flex-vh-center">
|
||||
${$btnGenConnect}${$btnCancel}
|
||||
</div>
|
||||
</div>`.appendTo(view.$wrpInitial);
|
||||
|
||||
$btnGenConnect.click(async () => {
|
||||
if (!$iptPlayerName.val().trim()) return $iptPlayerName.addClass("form-control--error");
|
||||
if (!$iptServerToken.val().trim()) return $iptServerToken.addClass("form-control--error");
|
||||
|
||||
try {
|
||||
$btnGenConnect.attr("disabled", true);
|
||||
|
||||
ui = new InitiativeTrackerPlayerUiV1(view, $iptPlayerName.val(), $iptServerToken.val());
|
||||
await ui.pInit();
|
||||
InitiativeTrackerPlayerMessageHandlerScreenV1.initUnloadMessage();
|
||||
} catch (e) {
|
||||
$btnGenConnect.attr("disabled", false);
|
||||
JqueryUtil.doToast({content: `Failed to connect. ${VeCt.STR_SEE_CONSOLE}`, type: "danger"});
|
||||
setTimeout(() => { throw e; });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const $btnConnectLocal = $(`<button class="btn btn-primary min-w-200p">Connect to Local Tracker</button>`)
|
||||
.click(async () => {
|
||||
const $elesData = DmScreenUtil.$getPanelDataElements({board, type: PANEL_TYP_INITIATIVE_TRACKER});
|
||||
|
||||
if (!$elesData.length) return JqueryUtil.doToast({content: "No local trackers detected!", type: "warning"});
|
||||
|
||||
if ($elesData.length === 1) {
|
||||
try {
|
||||
const token = await $elesData[0].data("pDoConnectLocalV1")(view);
|
||||
ui = new InitiativeTrackerPlayerUiV1(view, "Local", token);
|
||||
await ui.pInit();
|
||||
InitiativeTrackerPlayerMessageHandlerScreenV1.initUnloadMessage();
|
||||
} catch (e) {
|
||||
JqueryUtil.doToast({content: `Failed to connect. ${VeCt.STR_SEE_CONSOLE}`, type: "danger"});
|
||||
setTimeout(() => { throw e; });
|
||||
}
|
||||
} else {
|
||||
$btnConnectRemote.detach();
|
||||
$btnConnectLocal.detach();
|
||||
|
||||
const $selTracker = $(`<select class="form-control input-xs mr-1">
|
||||
<option value="-1" disabled>Select a local tracker</option>
|
||||
</select>`).change(() => $selTracker.removeClass("form-control--error"));
|
||||
$elesData.forEach(($e, i) => $selTracker.append(`<option value="${i}">${$e.data("getSummary")()}</option>`));
|
||||
$selTracker.val("-1");
|
||||
|
||||
const $btnOk = $(`<button class="btn btn-primary btn-xs">OK</button>`)
|
||||
.click(async () => {
|
||||
// jQuery reads the disabled value as null
|
||||
if ($selTracker.val() == null) return $selTracker.addClass("form-control--error");
|
||||
|
||||
$btnOk.prop("disabled", true);
|
||||
|
||||
try {
|
||||
const token = await $elesData[Number($selTracker.val())].data("pDoConnectLocalV1")(view);
|
||||
ui = new InitiativeTrackerPlayerUiV1(view, "Local", token);
|
||||
await ui.pInit();
|
||||
InitiativeTrackerPlayerMessageHandlerScreenV1.initUnloadMessage();
|
||||
} catch (e) {
|
||||
JqueryUtil.doToast({content: `Failed to connect. ${VeCt.STR_SEE_CONSOLE}`, type: "danger"});
|
||||
// restore original state
|
||||
$btnCancel.remove(); $wrpSel.remove();
|
||||
view.$wrpInitial.append($btnConnectRemote).append($btnConnectLocal);
|
||||
setTimeout(() => { throw e; });
|
||||
}
|
||||
});
|
||||
|
||||
const $wrpSel = $$`<div class="ve-flex-vh-center mb-2">
|
||||
${$selTracker}
|
||||
${$btnOk}
|
||||
</div>`.appendTo(view.$wrpInitial);
|
||||
|
||||
const $btnCancel = $(`<button class="btn btn-default btn-xs">Back</button>`)
|
||||
.click(() => {
|
||||
// restore original state
|
||||
$btnCancel.remove(); $wrpSel.remove();
|
||||
view.$wrpInitial.append($btnConnectRemote).append($btnConnectLocal);
|
||||
})
|
||||
.appendTo(view.$wrpInitial);
|
||||
}
|
||||
});
|
||||
|
||||
view.$wrpInitial = $$`<div class="ve-flex-vh-center h-100 ve-flex-col dm__panel-bg">
|
||||
${$btnConnectRemote}
|
||||
${$btnConnectLocal}
|
||||
</div>`.appendTo($wrpTracker);
|
||||
|
||||
return $wrpTracker;
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerPlayerMessageHandlerScreenV1 extends InitiativeTrackerPlayerMessageHandlerV1 {
|
||||
constructor () {
|
||||
super(true);
|
||||
|
||||
this._$wrpInitial = null;
|
||||
}
|
||||
|
||||
initUi () {
|
||||
if (this._isUiInit) return;
|
||||
this._isUiInit = true;
|
||||
|
||||
this._$meta.show();
|
||||
this._$head.show();
|
||||
this._$rows.show();
|
||||
this._$wrpInitial.addClass("hidden");
|
||||
}
|
||||
|
||||
set $wrpInitial ($wrpInitial) { this._$wrpInitial = $wrpInitial; }
|
||||
get $wrpInitial () { return this._$wrpInitial; }
|
||||
|
||||
static initUnloadMessage () {
|
||||
$(window).on("beforeunload", evt => {
|
||||
const message = `The connection will be closed`;
|
||||
(evt || window.event).message = message;
|
||||
return message;
|
||||
});
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// region v0
|
||||
export class InitiativeTrackerPlayerV0 {
|
||||
static $getPanelElement (board, state) {
|
||||
const $meta = $(`<div class="initp__meta"/>`).hide();
|
||||
const $head = $(`<div class="initp__header"/>`).hide();
|
||||
const $rows = $(`<div class="ve-flex-col"></div>`).hide();
|
||||
|
||||
const $wrpTracker = $$`<div class="initp__wrp_active">
|
||||
${$meta}
|
||||
${$head}
|
||||
${$rows}
|
||||
</div>`;
|
||||
|
||||
const view = new InitiativeTrackerPlayerMessageHandlerScreenV0();
|
||||
view.setElements($meta, $head, $rows);
|
||||
|
||||
const $btnConnectRemote = $(`<button class="btn btn-primary mb-2 min-w-200p" title="Connect to a tracker outside of this browser tab.">Connect to Remote Tracker</button>`)
|
||||
.click(() => {
|
||||
$btnConnectRemote.detach();
|
||||
$btnConnectLocal.detach();
|
||||
|
||||
const $iptServerToken = $(`<input class="form-control input-sm code">`).disableSpellcheck();
|
||||
const $btnGenClientToken = $(`<button class="btn btn-primary btn-xs">Generate Client Token</button>`);
|
||||
const $iptClientToken = $(`<input class="form-control input-sm code copyable">`).disableSpellcheck();
|
||||
|
||||
const $btnCancel = $(`<button class="btn btn-default btn-xs">Back</button>`)
|
||||
.click(() => {
|
||||
// restore original state
|
||||
$wrpClient.remove();
|
||||
view.$wrpInitial.append($btnConnectRemote).append($btnConnectLocal);
|
||||
});
|
||||
|
||||
const $wrpClient = $$`<div class="ve-flex-col w-100">
|
||||
<div class="ve-flex-vh-center px-4 mb-2">
|
||||
<span style="min-width: fit-content;" class="mr-2">Server Token</span>
|
||||
${$iptServerToken}
|
||||
</div>
|
||||
|
||||
<div class="ve-flex-v-center ve-flex-h-right px-4 mb-2">
|
||||
${$btnGenClientToken}
|
||||
</div>
|
||||
|
||||
<div class="ve-flex-vh-center px-4 mb-2">
|
||||
<span style="min-width: fit-content;" class="mr-2">Client Token</span>
|
||||
${$iptClientToken}
|
||||
</div>
|
||||
|
||||
<div class="ve-flex-vh-center px-4">
|
||||
${$btnCancel}
|
||||
</div>
|
||||
</div>`.appendTo(view.$wrpInitial);
|
||||
|
||||
const ui = new InitiativeTrackerPlayerUiV0(view, $iptServerToken, $btnGenClientToken, $iptClientToken);
|
||||
ui.init();
|
||||
});
|
||||
|
||||
const $btnConnectLocal = $(`<button class="btn btn-primary min-w-200p" title="Connect to a tracker in this browser tab.">Connect to Local Tracker</button>`)
|
||||
.click(async () => {
|
||||
const $elesData = DmScreenUtil.$getPanelDataElements({board, type: PANEL_TYP_INITIATIVE_TRACKER});
|
||||
|
||||
if ($elesData.length) {
|
||||
if ($elesData.length === 1) {
|
||||
await $elesData[0].data("pDoConnectLocalV0")(view);
|
||||
} else {
|
||||
$btnConnectRemote.detach();
|
||||
$btnConnectLocal.detach();
|
||||
|
||||
const $selTracker = $(`<select class="form-control input-xs mr-1">
|
||||
<option value="-1" disabled>Select a local tracker</option>
|
||||
</select>`).change(() => $selTracker.removeClass("error-background"));
|
||||
$elesData.forEach(($e, i) => $selTracker.append(`<option value="${i}">${$e.data("getSummary")()}</option>`));
|
||||
$selTracker.val("-1");
|
||||
|
||||
const $btnOk = $(`<button class="btn btn-primary btn-xs">OK</button>`)
|
||||
.click(async () => {
|
||||
if ($selTracker.val() === "-1") return $selTracker.addClass("error-background");
|
||||
|
||||
await $elesData[Number($selTracker.val())].data("pDoConnectLocalV0")(view);
|
||||
|
||||
// restore original state
|
||||
$btnCancel.remove(); $wrpSel.remove();
|
||||
view.$wrpInitial.append($btnConnectRemote).append($btnConnectLocal);
|
||||
});
|
||||
|
||||
const $wrpSel = $$`<div class="ve-flex-vh-center mb-2">
|
||||
${$selTracker}
|
||||
${$btnOk}
|
||||
</div>`.appendTo(view.$wrpInitial);
|
||||
|
||||
const $btnCancel = $(`<button class="btn btn-default btn-xs">Back</button>`)
|
||||
.click(() => {
|
||||
// restore original state
|
||||
$btnCancel.remove(); $wrpSel.remove();
|
||||
view.$wrpInitial.append($btnConnectRemote).append($btnConnectLocal);
|
||||
})
|
||||
.appendTo(view.$wrpInitial);
|
||||
}
|
||||
} else {
|
||||
JqueryUtil.doToast({content: "No local trackers detected!", type: "warning"});
|
||||
}
|
||||
});
|
||||
|
||||
view.$wrpInitial = $$`<div class="ve-flex-vh-center h-100 ve-flex-col dm__panel-bg">
|
||||
${$btnConnectRemote}
|
||||
${$btnConnectLocal}
|
||||
</div>`.appendTo($wrpTracker);
|
||||
|
||||
return $wrpTracker;
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerPlayerMessageHandlerScreenV0 extends InitiativeTrackerPlayerMessageHandlerV0 {
|
||||
constructor () {
|
||||
super(true);
|
||||
|
||||
this._$wrpInitial = null;
|
||||
}
|
||||
|
||||
initUi () {
|
||||
if (this._isUiInit) return;
|
||||
this._isUiInit = true;
|
||||
|
||||
this._$meta.show();
|
||||
this._$head.show();
|
||||
this._$rows.show();
|
||||
this._$wrpInitial.addClass("hidden");
|
||||
|
||||
$(window).on("beforeunload", evt => {
|
||||
if (this._clientData.client.isActive) {
|
||||
const message = `The connection will be closed`;
|
||||
(evt || window.event).message = message;
|
||||
return message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set $wrpInitial ($wrpInitial) { this._$wrpInitial = $wrpInitial; }
|
||||
get $wrpInitial () { return this._$wrpInitial; }
|
||||
}
|
||||
// endregion
|
||||
3413
js/dmscreen/dmscreen-timetracker.js
Normal file
3413
js/dmscreen/dmscreen-timetracker.js
Normal file
File diff suppressed because it is too large
Load Diff
25
js/dmscreen/dmscreen-util.js
Normal file
25
js/dmscreen/dmscreen-util.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export class DmScreenUtil {
|
||||
// FIXME(Future) switch to a better interface than `data`
|
||||
static $getPanelDataElements ({board, type, selector = ".dm__data-anchor"}) {
|
||||
return board.getPanelsByType(type)
|
||||
.flatMap(it => it.tabDatas.filter(td => td.type === type).map(td => td.$content.find(selector)));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static async pGetScaledCreature ({name, source, scaledCr, scaledSummonSpellLevel, scaledSummonClassLevel}) {
|
||||
const mon = await DataLoader.pCacheAndGet(
|
||||
UrlUtil.PG_BESTIARY,
|
||||
source,
|
||||
UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY]({name: name, source: source}),
|
||||
);
|
||||
|
||||
if (scaledCr == null && scaledSummonSpellLevel == null && scaledSummonClassLevel == null) return mon;
|
||||
|
||||
if (scaledCr != null) return ScaleCreature.scale(mon, scaledCr);
|
||||
if (scaledSummonSpellLevel != null) return ScaleSpellSummonedCreature.scale(mon, scaledCr);
|
||||
if (scaledSummonClassLevel != null) return ScaleClassSummonedCreature.scale(mon, scaledCr);
|
||||
|
||||
throw new Error(`Should never occur!`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export class InitiativeTrackerConditionUtil {
|
||||
static getNewRowState ({name, color, turns}) {
|
||||
return {
|
||||
id: CryptUtil.uid(),
|
||||
entity: {
|
||||
name: name ?? "",
|
||||
color: color ?? MiscUtil.randomColor(),
|
||||
turns: turns ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import {InitiativeTrackerUtil, UtilConditions} from "../../initiativetracker/initiativetracker-utils.js";
|
||||
import {InitiativeTrackerConditionCustomEdit} from "./dmscreen-initiativetracker-conditioncustom.js";
|
||||
import {InitiativeTrackerConditionUtil} from "./dmscreen-initiativetracker-condition.js";
|
||||
|
||||
class _UtilConditionsCustomView {
|
||||
static $getBtnCondition ({comp, cbSubmit, cbClick}) {
|
||||
const $btn = $(`<button class="btn btn-default btn-xs dm-init-cond__btn-cond" title="SHIFT to add with "Unlimited" duration; CTRL to add with 1-turn duration; SHIFT+CTRL to add with 10-turn duration."></button>`)
|
||||
.on("click", evt => {
|
||||
cbClick({
|
||||
name: comp._state.name,
|
||||
color: comp._state.color,
|
||||
turns: comp._state.turns,
|
||||
});
|
||||
|
||||
if (evt.shiftKey && EventUtil.isCtrlMetaKey(evt)) return cbSubmit({turns: 10});
|
||||
|
||||
if (EventUtil.isCtrlMetaKey(evt)) return cbSubmit({turns: 1});
|
||||
|
||||
if (evt.shiftKey) return cbSubmit({turns: null});
|
||||
});
|
||||
|
||||
comp._addHookBase("color", () => $btn.css({"background-color": `${comp._state.color}`}))();
|
||||
comp._addHookBase("name", () => $btn.text(comp._state.name || "\u00A0"))();
|
||||
|
||||
return $btn;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderableCollectionConditionsCustomView extends RenderableCollectionGenericRows {
|
||||
constructor (
|
||||
{
|
||||
comp,
|
||||
$wrpRows,
|
||||
rdState,
|
||||
cbDoSubmit,
|
||||
},
|
||||
) {
|
||||
super(comp, "conditionsCustom", $wrpRows);
|
||||
this._rdState = rdState;
|
||||
this._cbDoSubmit = cbDoSubmit;
|
||||
}
|
||||
|
||||
_$getWrpRow () {
|
||||
return $(`<div class="ve-flex-vh-center w-33 my-1"></div>`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_populateRow ({comp, $wrpRow, entity}) {
|
||||
_UtilConditionsCustomView.$getBtnCondition({
|
||||
comp,
|
||||
cbClick: ({name, color, turns}) => {
|
||||
this._comp._state.name = name;
|
||||
this._comp._state.color = color;
|
||||
this._comp._state.turns = turns;
|
||||
},
|
||||
cbSubmit: ({turns}) => {
|
||||
this._comp._state.turns = turns;
|
||||
this._cbDoSubmit({rdState: this._rdState});
|
||||
},
|
||||
}).appendTo($wrpRow);
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerConditionAdd extends BaseComponent {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.cbDoClose = null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor ({conditionsCustom}) {
|
||||
super();
|
||||
this.__state.conditionsCustom = conditionsCustom;
|
||||
}
|
||||
|
||||
getConditionsCustom () {
|
||||
return MiscUtil.copyFast(this._state.conditionsCustom);
|
||||
}
|
||||
|
||||
async pGetShowModalResults () {
|
||||
const rdState = new this.constructor._RenderState();
|
||||
|
||||
const {$modalInner, doClose, pGetResolved} = UiUtil.getShowModal({
|
||||
isMinHeight0: true,
|
||||
isHeaderBorder: true,
|
||||
title: "Add Condition",
|
||||
$titleSplit: this._render_$getBtnEditCustom({rdState}),
|
||||
});
|
||||
rdState.cbDoClose = doClose;
|
||||
|
||||
$$($modalInner)`
|
||||
${this._render_$getStgConditionsStandard({rdState})}
|
||||
|
||||
<hr class="hr-3">
|
||||
|
||||
${this._render_$getStgConditionsCustom({rdState})}
|
||||
|
||||
${this._render_$getStgIpts({rdState})}
|
||||
${this._render_$getStgSubmit({rdState})}
|
||||
`;
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
_render_$getBtnEditCustom ({rdState}) {
|
||||
return $(`<button class="btn btn-default btn-xs" title="Manage Custom Conditions"><span class="glyphicon glyphicon-cog"></span></button>`)
|
||||
.on("click", async () => {
|
||||
const compEdit = new InitiativeTrackerConditionCustomEdit({conditionsCustom: MiscUtil.copyFast(this._state.conditionsCustom)});
|
||||
await compEdit.pGetShowModalResults();
|
||||
this._state.conditionsCustom = compEdit.getConditionsCustom();
|
||||
});
|
||||
}
|
||||
|
||||
_render_$getStgConditionsStandard ({rdState}) {
|
||||
const $wrps = InitiativeTrackerUtil.CONDITIONS
|
||||
.map(cond => {
|
||||
const $btn = _UtilConditionsCustomView.$getBtnCondition({
|
||||
comp: BaseComponent.fromObject({
|
||||
name: cond.name,
|
||||
color: cond.color,
|
||||
turns: cond.turns,
|
||||
}, "*"),
|
||||
cbClick: ({name, color, turns}) => {
|
||||
this._state.name = name;
|
||||
this._state.color = color;
|
||||
},
|
||||
cbSubmit: ({turns}) => {
|
||||
this._state.turns = turns;
|
||||
this._doSubmit({rdState});
|
||||
},
|
||||
});
|
||||
|
||||
return $$`<div class="ve-flex-vh-center w-33 my-1">${$btn}</div>`;
|
||||
});
|
||||
|
||||
return $$`
|
||||
<div class="ve-flex-col w-100 h-100 min-h-0 ve-flex-v-center">
|
||||
<div class="ve-flex-wrap w-100 h-100 min-h-0 dm-init-cond__wrp-btns">
|
||||
${$wrps}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_render_$getStgConditionsCustom ({rdState}) {
|
||||
const $wrpRows = $(`<div class="ve-flex-wrap w-100 min-h-0 dm-init-cond__wrp-btns"></div>`);
|
||||
|
||||
const compRows = new _RenderableCollectionConditionsCustomView({
|
||||
comp: this,
|
||||
$wrpRows,
|
||||
rdState,
|
||||
cbDoSubmit: this._doSubmit.bind(this),
|
||||
});
|
||||
this._addHookBase("conditionsCustom", () => compRows.render())();
|
||||
|
||||
const $stg = $$`<div class="ve-flex-col w-100 h-100 min-h-0 ve-flex-v-center">
|
||||
${$wrpRows}
|
||||
<hr class="hr-3">
|
||||
</div>`;
|
||||
|
||||
this._addHookBase("conditionsCustom", () => $stg.toggleVe(!!this._state.conditionsCustom.length))();
|
||||
|
||||
return $stg;
|
||||
}
|
||||
|
||||
_render_$getStgIpts ({rdState}) {
|
||||
const $iptName = ComponentUiUtil.$getIptStr(this, "name", {html: `<input class="form-control">`})
|
||||
.on("keydown", evt => {
|
||||
if (evt.key !== "Enter") return;
|
||||
$iptName.trigger("change");
|
||||
this._doSubmit({rdState});
|
||||
});
|
||||
|
||||
const $iptColor = ComponentUiUtil.$getIptColor(this, "color", {html: `<input class="form-control" type="color">`});
|
||||
|
||||
const $iptTurns = ComponentUiUtil.$getIptInt(this, "turns", null, {isAllowNull: true, fallbackOnNaN: null, html: `<input class="form-control" placeholder="Unlimited">`})
|
||||
.on("keydown", evt => {
|
||||
if (evt.key !== "Enter") return;
|
||||
$iptTurns.trigger("change");
|
||||
this._doSubmit({rdState});
|
||||
});
|
||||
|
||||
const $btnSave = $(`<button class="btn btn-default w-100" title="Save as New Custom Condition"><span class="glyphicon glyphicon-floppy-disk"></span></button>`)
|
||||
.click(() => {
|
||||
this._state.conditionsCustom = [
|
||||
...this._state.conditionsCustom,
|
||||
InitiativeTrackerConditionUtil.getNewRowState({
|
||||
name: this._state.name,
|
||||
color: this._state.color,
|
||||
turns: this._state.turns,
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
||||
return $$`
|
||||
<div class="ve-flex-v-center mb-2">
|
||||
<div class="small-caps col-5 pr-1">Name</div>
|
||||
<div class="small-caps col-2 px-1">Color</div>
|
||||
<div class="small-caps col-4 px-1">Duration</div>
|
||||
<div class="col-1 pl-1"> </div>
|
||||
</div>
|
||||
<div class="ve-flex-v-center mb-3">
|
||||
<div class="col-5 pr-1">${$iptName}</div>
|
||||
<div class="col-2 px-1">${$iptColor}</div>
|
||||
<div class="col-4 px-1">${$iptTurns}</div>
|
||||
<div class="col-1 pl-1">${$btnSave}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_render_$getStgSubmit ({rdState}) {
|
||||
const $btnAdd = $(`<button class="btn btn-primary w-100">Set Condition</button>`)
|
||||
.click(() => this._doSubmit({rdState}));
|
||||
return $$`
|
||||
<div class="ve-flex-v-center">
|
||||
${$btnAdd}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_doSubmit ({rdState}) {
|
||||
rdState.cbDoClose(
|
||||
true,
|
||||
UtilConditions.getDefaultState({
|
||||
name: this._state.name,
|
||||
color: this._state.color,
|
||||
turns: this._state.turns,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
_getDefaultState () {
|
||||
return {
|
||||
name: "",
|
||||
color: MiscUtil.randomColor(),
|
||||
turns: null,
|
||||
conditionsCustom: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import {InitiativeTrackerConditionUtil} from "./dmscreen-initiativetracker-condition.js";
|
||||
|
||||
class _RenderableCollectionConditionsCustomEdit extends RenderableCollectionGenericRows {
|
||||
constructor (
|
||||
{
|
||||
comp,
|
||||
$wrpRows,
|
||||
},
|
||||
) {
|
||||
super(comp, "conditionsCustom", $wrpRows);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_populateRow ({comp, $wrpRow, entity}) {
|
||||
const $iptName = ComponentUiUtil.$getIptStr(comp, "name");
|
||||
|
||||
const $iptColor = ComponentUiUtil.$getIptColor(comp, "color")
|
||||
.addClass("w-100");
|
||||
|
||||
const $iptTurns = ComponentUiUtil.$getIptInt(comp, "turns", null, {isAllowNull: true, fallbackOnNaN: null})
|
||||
.addClass("mr-2")
|
||||
.placeholder("Unlimited");
|
||||
|
||||
const $btnDelete = this._utils.$getBtnDelete({entity});
|
||||
|
||||
$$($wrpRow)`
|
||||
<div class="ve-flex-vh-center w-100 my-1">
|
||||
<div class="col-5 pr-1 ve-flex-v-center">${$iptName}</div>
|
||||
<div class="col-2 px-1 ve-flex-v-center">${$iptColor}</div>
|
||||
<div class="col-5 pr-1 ve-flex-v-center">
|
||||
${$iptTurns}
|
||||
<div class="ve-flex-vh-center btn-group">
|
||||
${$btnDelete}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerConditionCustomEdit extends BaseComponent {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.cbDoClose = null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor ({conditionsCustom}) {
|
||||
super();
|
||||
this._state.conditionsCustom = conditionsCustom;
|
||||
}
|
||||
|
||||
getConditionsCustom () {
|
||||
return MiscUtil.copyFast(this._state.conditionsCustom);
|
||||
}
|
||||
|
||||
async pGetShowModalResults () {
|
||||
const rdState = new this.constructor._RenderState();
|
||||
|
||||
const {$modalInner, $modalFooter, doClose, pGetResolved} = UiUtil.getShowModal({
|
||||
title: "Manage Custom Conditions",
|
||||
isHeaderBorder: true,
|
||||
hasFooter: true,
|
||||
});
|
||||
rdState.cbDoClose = doClose;
|
||||
|
||||
const $btnAdd = $(`<button class="btn btn-default btn-xs bb-0 bbr-0 bbl-0" title="Add"><span class="glyphicon glyphicon-plus"></span></button>`)
|
||||
.on("click", () => {
|
||||
this._state.conditionsCustom = [...this._state.conditionsCustom, InitiativeTrackerConditionUtil.getNewRowState()];
|
||||
});
|
||||
|
||||
const $wrpRows = $(`<div class="ve-flex-col h-100 min-h-0 overflow-y-auto"></div>`);
|
||||
|
||||
const compRows = new _RenderableCollectionConditionsCustomEdit({comp: this, $wrpRows});
|
||||
this._addHookBase("conditionsCustom", () => compRows.render())();
|
||||
|
||||
$$($modalInner)`
|
||||
<div class="ve-flex-col mt-2 h-100 min-h-0">
|
||||
<div class="ve-flex-vh-center w-100 mb-2 bb-1p-trans">
|
||||
<div class="col-5">Name</div>
|
||||
<div class="col-2">Color</div>
|
||||
<div class="col-4">Turns</div>
|
||||
<div class="col-1 ve-flex-v-center ve-flex-h-right">${$btnAdd}</div>
|
||||
</div>
|
||||
|
||||
${$wrpRows}
|
||||
</div>
|
||||
`;
|
||||
|
||||
$$($modalFooter)`
|
||||
${this._render_$getFooter({rdState})}
|
||||
`;
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
_render_$getFooter ({rdState}) {
|
||||
const $btnSave = $(`<button class="btn btn-primary btn-sm w-100">Save</button>`)
|
||||
.click(() => rdState.cbDoClose(true));
|
||||
|
||||
return $$`<div class="w-100 py-3 no-shrink">
|
||||
${$btnSave}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_getDefaultState () {
|
||||
return {
|
||||
conditionsCustom: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export class InitiativeTrackerConst {
|
||||
static SORT_ORDER_ALPHA = "ALPHA";
|
||||
static SORT_ORDER_NUM = "NUMBER";
|
||||
|
||||
static SORT_DIR_ASC = "ASC";
|
||||
static SORT_DIR_DESC = "DESC";
|
||||
|
||||
static DIR_FORWARDS = 1;
|
||||
static DIR_BACKWARDS = -1;
|
||||
static DIR_NEUTRAL = 0;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
InitiativeTrackerRowDataViewDefaultParty,
|
||||
} from "./dmscreen-initiativetracker-rowsdefaultparty.js";
|
||||
|
||||
export class InitiativeTrackerDefaultParty extends BaseComponent {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.cbDoClose = null;
|
||||
this.fnsCleanup = [];
|
||||
}
|
||||
};
|
||||
|
||||
constructor ({comp, roller, rowStateBuilder}) {
|
||||
super();
|
||||
this._comp = comp;
|
||||
this._roller = roller;
|
||||
this._rowStateBuilder = rowStateBuilder;
|
||||
this._prop = "rowsDefaultParty";
|
||||
|
||||
this._viewRowsDefaultParty = null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
pGetShowModalResults () {
|
||||
const rdState = new this.constructor._RenderState();
|
||||
|
||||
const {$modalInner, $modalFooter, pGetResolved, doClose} = UiUtil.getShowModal({
|
||||
title: "Edit Default Party",
|
||||
isHeaderBorder: true,
|
||||
isUncappedHeight: true,
|
||||
hasFooter: true,
|
||||
cbClose: () => rdState.fnsCleanup.forEach(fn => fn()),
|
||||
$titleSplit: this._render_$getBtnAdd({rdState}),
|
||||
});
|
||||
rdState.cbDoClose = doClose;
|
||||
|
||||
this._render_renderBody({rdState, $modalInner});
|
||||
this._render_renderFooter({rdState, $modalFooter});
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
_render_$getBtnAdd ({rdState}) {
|
||||
return $(`<button class="btn btn-default btn-xs" title="Add Player"><span class="glyphicon glyphicon-plus"></span></button>`)
|
||||
.on("click", async () => {
|
||||
this._comp._state[this._prop] = [
|
||||
...this._comp._state[this._prop],
|
||||
await this._rowStateBuilder.pGetNewRowState(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_render_renderBody ({rdState, $modalInner}) {
|
||||
this._viewRowsDefaultParty = new InitiativeTrackerRowDataViewDefaultParty({
|
||||
comp: this._comp,
|
||||
prop: this._prop,
|
||||
roller: this._roller,
|
||||
rowStateBuilder: this._rowStateBuilder,
|
||||
});
|
||||
this._viewRowsDefaultPartyMeta = this._viewRowsDefaultParty.getRenderedView();
|
||||
this._viewRowsDefaultPartyMeta.$ele.appendTo($modalInner);
|
||||
rdState.fnsCleanup.push(this._viewRowsDefaultPartyMeta.cbDoCleanup);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_render_renderFooter ({rdState, $modalFooter}) {
|
||||
const $btnSave = $(`<button class="btn btn-primary btn-sm w-100">Save</button>`)
|
||||
.click(() => rdState.cbDoClose(true));
|
||||
|
||||
$$($modalFooter)`<div class="w-100 py-3 no-shrink">
|
||||
${$btnSave}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pGetConvertedDefaultPartyActiveRows () {
|
||||
const rows = MiscUtil.copyFast(this._comp._state.rowsDefaultParty);
|
||||
|
||||
if (!this._comp._state.isRollInit) return rows;
|
||||
|
||||
await rows.pSerialAwaitMap(async row => {
|
||||
const {entity} = row;
|
||||
const {initiativeModifier} = await this._rowStateBuilder.pGetRowInitiativeMeta({row});
|
||||
|
||||
// Skip rolling no-modifier-exists initiative for player rows, as we assume the user wants to input them
|
||||
// manually.
|
||||
if (initiativeModifier == null) return;
|
||||
|
||||
entity.initiative = await this._roller.pGetRollInitiative({initiativeModifier, name: entity.name});
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import {InitiativeTrackerStatColumnFactory} from "./dmscreen-initiativetracker-statcolumns.js";
|
||||
|
||||
class _ConvertedEncounter {
|
||||
constructor () {
|
||||
this.isStatsAddColumns = false;
|
||||
|
||||
this.statsCols = [];
|
||||
this.rows = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerEncounterConverter {
|
||||
constructor (
|
||||
{
|
||||
roller,
|
||||
rowStateBuilderActive,
|
||||
importIsAddPlayers,
|
||||
importIsRollGroups,
|
||||
isRollInit,
|
||||
isRollHp,
|
||||
},
|
||||
) {
|
||||
this._roller = roller;
|
||||
this._rowStateBuilderActive = rowStateBuilderActive;
|
||||
|
||||
this._importIsAddPlayers = importIsAddPlayers;
|
||||
this._importIsRollGroups = importIsRollGroups;
|
||||
this._isRollInit = isRollInit;
|
||||
this._isRollHp = isRollHp;
|
||||
}
|
||||
|
||||
async pGetConverted ({entityInfos, encounterInfo}) {
|
||||
const out = new _ConvertedEncounter();
|
||||
|
||||
await this._pGetConverted_pPlayers({entityInfos, encounterInfo, out});
|
||||
await this._pGetConverted_pCreatures({entityInfos, encounterInfo, out});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async _pGetConverted_pPlayers ({entityInfos, encounterInfo, out}) {
|
||||
if (!this._importIsAddPlayers) return;
|
||||
|
||||
await this._pGetConverted_pPlayers_advanced({entityInfos, encounterInfo, out});
|
||||
await this._pGetConverted_pPlayers_simple({entityInfos, encounterInfo, out});
|
||||
}
|
||||
|
||||
async _pGetConverted_pPlayers_advanced ({entityInfos, encounterInfo, out}) {
|
||||
if (!encounterInfo.isAdvanced || !encounterInfo.playersAdvanced) return;
|
||||
|
||||
const colNameIndex = {};
|
||||
encounterInfo.colsExtraAdvanced = encounterInfo.colsExtraAdvanced || [];
|
||||
if (encounterInfo.colsExtraAdvanced.length) out.isStatsAddColumns = true;
|
||||
|
||||
encounterInfo.colsExtraAdvanced.forEach((col, i) => colNameIndex[i] = (col?.name || "").toLowerCase());
|
||||
|
||||
const {ixsColLookup, ixExtrasHp, statsCols} = this._pGetConverted_pPlayers_advanced_getExtrasInfo({encounterInfo});
|
||||
out.statsCols.push(...statsCols);
|
||||
|
||||
await encounterInfo.playersAdvanced
|
||||
.pSerialAwaitMap(async playerDetails => {
|
||||
out.rows.push(
|
||||
await this._rowStateBuilderActive
|
||||
.pGetNewRowState({
|
||||
isActive: false,
|
||||
isPlayerVisible: true,
|
||||
name: playerDetails.name || "",
|
||||
initiative: null,
|
||||
conditions: [],
|
||||
...this._pGetConverted_pPlayers_advanced_extras({
|
||||
playerDetails,
|
||||
ixExtrasHp,
|
||||
ixsColLookup,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_pGetConverted_pPlayers_advanced_getExtrasInfo ({encounterInfo}) {
|
||||
const statsCols = [];
|
||||
const ixsColLookup = {};
|
||||
let ixExtrasHp = null;
|
||||
|
||||
encounterInfo.colsExtraAdvanced.forEach((col, i) => {
|
||||
let colName = col?.name || "";
|
||||
if (colName.toLowerCase() === "hp") {
|
||||
ixExtrasHp = i;
|
||||
return;
|
||||
}
|
||||
|
||||
const newCol = InitiativeTrackerStatColumnFactory.fromEncounterAdvancedColName({colName});
|
||||
ixsColLookup[i] = newCol;
|
||||
statsCols.push(newCol);
|
||||
});
|
||||
|
||||
return {ixsColLookup, ixExtrasHp, statsCols};
|
||||
}
|
||||
|
||||
_pGetConverted_pPlayers_advanced_extras ({playerDetails, ixExtrasHp, ixsColLookup}) {
|
||||
const out = {
|
||||
hpCurrent: null,
|
||||
hpMax: null,
|
||||
};
|
||||
|
||||
if (!playerDetails.extras?.length) return out;
|
||||
|
||||
const rowStatColData = playerDetails.extras
|
||||
.map((extra, i) => {
|
||||
const val = extra?.value || "";
|
||||
if (i === ixExtrasHp) return null;
|
||||
|
||||
const meta = ixsColLookup[i];
|
||||
return meta.getInitialCellStateData({obj: {value: val}});
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (ixExtrasHp == null) {
|
||||
return {
|
||||
...out,
|
||||
rowStatColData,
|
||||
};
|
||||
}
|
||||
|
||||
const [hpCurrent, hpMax] = (playerDetails.extras[ixExtrasHp]?.value || "")
|
||||
.split("/")
|
||||
.map(it => {
|
||||
const clean = it.trim();
|
||||
if (!clean) return null;
|
||||
if (isNaN(clean)) return null;
|
||||
return Number(clean);
|
||||
});
|
||||
|
||||
return {
|
||||
...out,
|
||||
hpCurrent,
|
||||
hpMax: hpCurrent != null && hpMax == null ? hpCurrent : hpCurrent,
|
||||
rowStatColData,
|
||||
};
|
||||
}
|
||||
|
||||
async _pGetConverted_pPlayers_simple ({entityInfos, encounterInfo, out}) {
|
||||
if (encounterInfo.isAdvanced || !encounterInfo.playersSimple) return;
|
||||
|
||||
await encounterInfo.playersSimple
|
||||
.pSerialAwaitMap(async playerGroup => {
|
||||
await [...new Array(playerGroup.count || 1)]
|
||||
.pSerialAwaitMap(async () => {
|
||||
out.rows.push(
|
||||
await this._rowStateBuilderActive
|
||||
.pGetNewRowState({
|
||||
name: "",
|
||||
hpCurrent: null,
|
||||
hpMax: null,
|
||||
initiative: null,
|
||||
isActive: false,
|
||||
conditions: [],
|
||||
isPlayerVisible: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async _pGetConverted_pCreatures ({entityInfos, encounterInfo, out}) {
|
||||
if (!entityInfos?.length) return;
|
||||
|
||||
await entityInfos
|
||||
.filter(Boolean)
|
||||
.pSerialAwaitMap(async entityInfo => {
|
||||
const groupInit = this._importIsRollGroups && this._isRollInit ? await this._roller.pGetRollInitiative({mon: entityInfo.entity}) : null;
|
||||
const groupHp = this._importIsRollGroups ? await this._roller.pGetOrRollHp(entityInfo.entity, {isRollHp: this._isRollHp}) : null;
|
||||
|
||||
await [...new Array(entityInfo.count || 1)]
|
||||
.pSerialAwaitMap(async () => {
|
||||
const hpVal = this._importIsRollGroups
|
||||
? groupHp
|
||||
: await this._roller.pGetOrRollHp(entityInfo.entity, {isRollHp: this._isRollHp});
|
||||
|
||||
out.rows.push(
|
||||
await this._rowStateBuilderActive
|
||||
.pGetNewRowState({
|
||||
rows: out.rows,
|
||||
name: entityInfo.entity.name,
|
||||
displayName: entityInfo.entity._displayName,
|
||||
scaledCr: entityInfo.entity._scaledCr,
|
||||
scaledSummonSpellLevel: entityInfo.entity._summonedBySpell_level,
|
||||
scaledSummonClassLevel: entityInfo.entity._summonedByClass_level,
|
||||
initiative: this._isRollInit
|
||||
? this._importIsRollGroups ? groupInit : await this._roller.pGetRollInitiative({mon: entityInfo.entity})
|
||||
: null,
|
||||
isActive: false,
|
||||
source: entityInfo.entity.source,
|
||||
conditions: [],
|
||||
hpCurrent: hpVal,
|
||||
hpMax: hpVal,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
export class InitiativeTrackerSettingsImport extends BaseComponent {
|
||||
static _PROPS_TRACKED = [
|
||||
"isRollInit",
|
||||
"isRollHp",
|
||||
"importIsRollGroups",
|
||||
"importIsAddPlayers",
|
||||
"importIsAppend",
|
||||
];
|
||||
|
||||
constructor ({state}) {
|
||||
super();
|
||||
|
||||
this._proxyAssignSimple(
|
||||
"state",
|
||||
InitiativeTrackerSettingsImport._PROPS_TRACKED
|
||||
.mergeMap(prop => ({[prop]: state[prop]})),
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getStateUpdate () {
|
||||
return MiscUtil.copyFast(this._state);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
pGetShowModalResults () {
|
||||
const {$modalInner, $modalFooter, pGetResolved, doClose} = UiUtil.getShowModal({
|
||||
title: "Import Settings",
|
||||
isUncappedHeight: true,
|
||||
hasFooter: true,
|
||||
});
|
||||
|
||||
UiUtil.addModalSep($modalInner);
|
||||
this._pGetShowModalResults_renderSection_isRolls({$modalInner});
|
||||
UiUtil.addModalSep($modalInner);
|
||||
this._pGetShowModalResults_renderSection_import({$modalInner});
|
||||
|
||||
this._pGetShowModalResults_renderFooter({$modalFooter, doClose});
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_pGetShowModalResults_renderSection_isRolls ({$modalInner}) {
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isRollInit", text: "Roll creature initiative"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isRollHp", text: "Roll creature hit points"});
|
||||
}
|
||||
|
||||
_pGetShowModalResults_renderSection_import ({$modalInner}) {
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "importIsRollGroups", text: "Roll groups of creatures together"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "importIsAddPlayers", text: "Add players"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "importIsAppend", text: "Add to existing tracker state"});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_pGetShowModalResults_renderFooter ({$modalFooter, doClose}) {
|
||||
const $btnSave = $(`<button class="btn btn-primary btn-sm w-100">Save</button>`)
|
||||
.click(() => doClose(true));
|
||||
|
||||
$$($modalFooter)`<div class="w-100 py-3 no-shrink">
|
||||
${$btnSave}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
class _MonstersToLoad {
|
||||
constructor (
|
||||
{
|
||||
count,
|
||||
name,
|
||||
source,
|
||||
isRollHp,
|
||||
displayName,
|
||||
customName,
|
||||
scaledCr,
|
||||
scaledSummonSpellLevel,
|
||||
scaledSummonClassLevel,
|
||||
},
|
||||
) {
|
||||
this.count = count;
|
||||
this.name = name;
|
||||
this.source = source;
|
||||
this.isRollHp = isRollHp;
|
||||
this.displayName = displayName;
|
||||
this.customName = customName;
|
||||
this.scaledCr = scaledCr;
|
||||
this.scaledSummonSpellLevel = scaledSummonSpellLevel;
|
||||
this.scaledSummonClassLevel = scaledSummonClassLevel;
|
||||
}
|
||||
}
|
||||
|
||||
class _InitiativeTrackerMonsterAddCustomizer extends BaseComponent {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.cbDoClose = null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor ({mon}) {
|
||||
super();
|
||||
this._mon = mon;
|
||||
}
|
||||
|
||||
async pGetShowModalResults () {
|
||||
const rdState = new this.constructor._RenderState();
|
||||
|
||||
const {$modalInner, $modalFooter, doClose, pGetResolved} = UiUtil.getShowModal({
|
||||
title: `Customize Creature \u2014 ${this._mon.name}`,
|
||||
isHeaderBorder: true,
|
||||
hasFooter: true,
|
||||
isMinHeight0: true,
|
||||
});
|
||||
rdState.cbDoClose = doClose;
|
||||
|
||||
const $iptCustomName = ComponentUiUtil.$getIptStr(this, "customName");
|
||||
|
||||
$$($modalInner)`
|
||||
<div class="ve-flex-col py-2 w-100 h-100 overflow-y-auto">
|
||||
<label class="split-v-center mb-2">
|
||||
<span class="w-200p text-right no-shrink mr-2 bold">Custom Name:</span>
|
||||
${$iptCustomName}
|
||||
</label>
|
||||
${this._render_$getRowScaler()}
|
||||
</div>
|
||||
`;
|
||||
|
||||
$$($modalFooter)`
|
||||
${this._render_$getFooter({rdState})}
|
||||
`;
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
_render_$getRowScaler () {
|
||||
const isShowCrScaler = Parser.crToNumber(this._mon.cr) !== VeCt.CR_UNKNOWN;
|
||||
const isShowSpellLevelScaler = !isShowCrScaler && this._mon.summonedBySpellLevel != null;
|
||||
const isShowClassLevelScaler = !isShowSpellLevelScaler && this._mon.summonedByClass != null;
|
||||
|
||||
if (!isShowCrScaler && !isShowSpellLevelScaler && !isShowClassLevelScaler) return null;
|
||||
|
||||
if (isShowSpellLevelScaler) {
|
||||
const sel = Renderer.monster.getSelSummonSpellLevel(this._mon)
|
||||
.on("change", async () => {
|
||||
const val = Number(sel.val());
|
||||
this._state.scaledSummonSpellLevel = !~val ? null : val;
|
||||
if (this._state.scaledSummonSpellLevel == null) return delete this._state.displayName;
|
||||
this._state.displayName = (await ScaleSpellSummonedCreature.scale(this._mon, this._state.scaledSummonSpellLevel))._displayName;
|
||||
});
|
||||
|
||||
return $$`<label class="split-v-center mb-2">
|
||||
<span class="w-200p text-right no-shrink mr-2 bold">Spell Level:</span>
|
||||
${sel}
|
||||
</label>`;
|
||||
}
|
||||
|
||||
if (isShowClassLevelScaler) {
|
||||
const sel = Renderer.monster.getSelSummonClassLevel(this._mon)
|
||||
.on("change", async () => {
|
||||
const val = Number(sel.val());
|
||||
this._state.scaledSummonClassLevel = !~val ? null : val;
|
||||
if (this._state.scaledSummonClassLevel == null) return delete this._state.displayName;
|
||||
this._state.displayName = (await ScaleClassSummonedCreature.scale(this._mon, this._state.scaledSummonClassLevel))._displayName;
|
||||
});
|
||||
|
||||
return $$`<label class="split-v-center mb-2">
|
||||
<span class="w-200p text-right no-shrink mr-2 bold">Class Level:</span>
|
||||
${sel}
|
||||
</label>`;
|
||||
}
|
||||
|
||||
const $dispScaledCr = $(`<span class="inline-block"></span>`);
|
||||
this._addHookBase("scaledCr", () => $dispScaledCr.text(this._state.scaledCr ? Parser.numberToCr(this._state.scaledCr) : `${(this._mon.cr.cr || this._mon.cr)} (default)`))();
|
||||
|
||||
const $btnScaleCr = $(`<button class="btn btn-default btn-xs mr-2"><span class="glyphicon glyphicon-signal"></span></button>`)
|
||||
.on("click", async () => {
|
||||
const crBase = this._mon.cr.cr || this._mon.cr;
|
||||
|
||||
const cr = await InputUiUtil.pGetUserScaleCr({default: crBase});
|
||||
if (cr == null) return;
|
||||
|
||||
if (crBase === cr) {
|
||||
delete this._state.scaledCr;
|
||||
delete this._state.displayName;
|
||||
return;
|
||||
}
|
||||
this._state.scaledCr = Parser.crToNumber(cr);
|
||||
this._state.displayName = (await ScaleCreature.scale(this._mon, this._state.scaledCr))._displayName;
|
||||
});
|
||||
|
||||
return $$`<label class="split-v-center mb-2">
|
||||
<span class="w-200p text-right no-shrink mr-2 bold">CR:</span>
|
||||
<span class="ve-flex-v-center mr-auto">
|
||||
${$btnScaleCr}
|
||||
${$dispScaledCr}
|
||||
</span>
|
||||
</label>`;
|
||||
}
|
||||
|
||||
_render_$getFooter ({rdState}) {
|
||||
const $btnSave = $(`<button class="btn btn-primary btn-sm w-100">Save</button>`)
|
||||
.click(() => {
|
||||
rdState.cbDoClose(
|
||||
true,
|
||||
MiscUtil.copyFast(this.__state),
|
||||
);
|
||||
});
|
||||
|
||||
return $$`<div class="w-100 py-3 no-shrink">
|
||||
${$btnSave}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_getDefaultState () {
|
||||
return {
|
||||
customName: null,
|
||||
displayName: null,
|
||||
scaledCr: null,
|
||||
scaledSummonSpellLevel: null,
|
||||
scaledSummonClassLevel: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerMonsterAdd extends BaseComponent {
|
||||
static _RESULTS_MAX_DISPLAY = 75; // hard cap at 75 results
|
||||
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.cbDoClose = null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor ({board, isRollHp}) {
|
||||
super();
|
||||
this._board = board;
|
||||
|
||||
this._state.isRollHp = isRollHp;
|
||||
}
|
||||
|
||||
_getDefaultState () {
|
||||
return {
|
||||
isRollHp: false,
|
||||
cntToAdd: 1,
|
||||
cntToAddCustom: 13,
|
||||
};
|
||||
}
|
||||
|
||||
_getCntToAdd () {
|
||||
return this._state.cntToAdd === -1
|
||||
? Math.max(1, this._state.cntToAddCustom)
|
||||
: this._state.cntToAdd;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_$getCbCntToAdd ({cnt}) {
|
||||
const $cb = $(`<input type="radio" class="ui-search__ipt-search-sub-ipt">`);
|
||||
$cb.on("change", () => {
|
||||
this._state.cntToAdd = cnt;
|
||||
});
|
||||
this._addHookBase("cntToAdd", () => $cb.prop("checked", this._state.cntToAdd === cnt))();
|
||||
return $cb;
|
||||
}
|
||||
|
||||
_$getIptCntToAddCustom () {
|
||||
const $iptCntToAddCustom = ComponentUiUtil.$getIptInt(
|
||||
this,
|
||||
"cntToAddCustom",
|
||||
1,
|
||||
{
|
||||
html: `<input type="number" class="form-control ui-search__ipt-search-sub-ipt-custom">`,
|
||||
min: 1,
|
||||
},
|
||||
);
|
||||
|
||||
this._addHookBase("cntToAdd", () => {
|
||||
if (this._state.cntToAdd !== -1) return;
|
||||
$iptCntToAddCustom.select();
|
||||
})();
|
||||
|
||||
$iptCntToAddCustom.click(() => {
|
||||
this._state.cntToAdd = -1;
|
||||
});
|
||||
|
||||
return $iptCntToAddCustom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<[boolean, _MonstersToLoad]>}
|
||||
*/
|
||||
async pGetShowModalResults () {
|
||||
const rdState = new this.constructor._RenderState();
|
||||
|
||||
const flags = {
|
||||
doClickFirst: false,
|
||||
isWait: false,
|
||||
};
|
||||
|
||||
const {$modalInner, doClose, pGetResolved} = UiUtil.getShowModal();
|
||||
rdState.cbDoClose = doClose;
|
||||
|
||||
const $iptSearch = $(`<input class="ui-search__ipt-search search form-control" autocomplete="off" placeholder="Search...">`)
|
||||
.blurOnEsc();
|
||||
|
||||
$$`<div class="split no-shrink">
|
||||
${$iptSearch}
|
||||
|
||||
<div class="ui-search__ipt-search-sub-wrp ve-flex-v-center pr-0">
|
||||
<div class="mr-1">Add</div>
|
||||
<label class="ui-search__ipt-search-sub-lbl">${this._$getCbCntToAdd({cnt: 1})} 1</label>
|
||||
<label class="ui-search__ipt-search-sub-lbl">${this._$getCbCntToAdd({cnt: 2})} 2</label>
|
||||
<label class="ui-search__ipt-search-sub-lbl">${this._$getCbCntToAdd({cnt: 3})} 3</label>
|
||||
<label class="ui-search__ipt-search-sub-lbl">${this._$getCbCntToAdd({cnt: 5})} 5</label>
|
||||
<label class="ui-search__ipt-search-sub-lbl">${this._$getCbCntToAdd({cnt: 8})} 8</label>
|
||||
<label class="ui-search__ipt-search-sub-lbl">${this._$getCbCntToAdd({cnt: -1})} ${this._$getIptCntToAddCustom()}</label>
|
||||
</div>
|
||||
|
||||
<label class="ui-search__ipt-search-sub-wrp ve-flex-vh-center">${ComponentUiUtil.$getCbBool(this, "isRollHp").addClass("mr-1")} <span>Roll HP</span></label>
|
||||
</div>`.appendTo($modalInner);
|
||||
|
||||
const $results = $(`<div class="ui-search__wrp-results"></div>`).appendTo($modalInner);
|
||||
|
||||
const showMsgIpt = () => {
|
||||
flags.isWait = true;
|
||||
$results.empty().append(SearchWidget.getSearchEnter());
|
||||
};
|
||||
|
||||
const showMsgDots = () => $results.empty().append(SearchWidget.getSearchLoading());
|
||||
|
||||
const showNoResults = () => {
|
||||
flags.isWait = true;
|
||||
$results.empty().append(SearchWidget.getSearchNoResults());
|
||||
};
|
||||
|
||||
const $ptrRows = {_: []};
|
||||
|
||||
const doSearch = () => {
|
||||
const searchTerm = $iptSearch.val().trim();
|
||||
|
||||
const index = this._board.availContent["Creature"];
|
||||
const results = index.search(searchTerm, {
|
||||
fields: {
|
||||
n: {boost: 5, expand: true},
|
||||
s: {expand: true},
|
||||
},
|
||||
bool: "AND",
|
||||
expand: true,
|
||||
});
|
||||
const resultCount = results.length ? results.length : index.documentStore.length;
|
||||
const toProcess = results.length ? results : Object.values(index.documentStore.docs).slice(0, 75).map(it => ({doc: it}));
|
||||
|
||||
$results.empty();
|
||||
$ptrRows._ = [];
|
||||
if (toProcess.length) {
|
||||
if (flags.doClickFirst) {
|
||||
this._render_pHandleClickRow({rdState}, toProcess[0]);
|
||||
flags.doClickFirst = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const results = toProcess.slice(0, this.constructor._RESULTS_MAX_DISPLAY);
|
||||
|
||||
results.forEach(res => {
|
||||
const $row = this._render_$getSearchRow({rdState, res}).appendTo($results);
|
||||
SearchWidget.bindRowHandlers({result: res, $row, $ptrRows, fnHandleClick: this._render_pHandleClickRow.bind(this, {rdState}), $iptSearch});
|
||||
$ptrRows._.push($row);
|
||||
});
|
||||
|
||||
if (resultCount > this.constructor._RESULTS_MAX_DISPLAY) {
|
||||
const diff = resultCount - this.constructor._RESULTS_MAX_DISPLAY;
|
||||
$results.append(`<div class="ui-search__row ui-search__row--readonly">...${diff} more result${diff === 1 ? " was" : "s were"} hidden. Refine your search!</div>`);
|
||||
}
|
||||
} else {
|
||||
if (!searchTerm.trim()) showMsgIpt();
|
||||
else showNoResults();
|
||||
}
|
||||
};
|
||||
|
||||
SearchWidget.bindAutoSearch($iptSearch, {
|
||||
flags,
|
||||
fnSearch: doSearch,
|
||||
fnShowWait: showMsgDots,
|
||||
$ptrRows,
|
||||
});
|
||||
|
||||
$iptSearch.focus();
|
||||
doSearch();
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
async _render_pHandleClickRow ({rdState}, res) {
|
||||
await rdState.cbDoClose(
|
||||
true,
|
||||
new _MonstersToLoad({
|
||||
count: this._getCntToAdd(),
|
||||
name: res.doc.n,
|
||||
source: res.doc.s,
|
||||
isRollHp: this._state.isRollHp,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
_render_$getSearchRow ({rdState, res}) {
|
||||
const $btnCustomize = $(`<button class="btn btn-default btn-xxs" title="Customize"><span class="glyphicon glyphicon-stats"></span></button>`)
|
||||
.on("click", async evt => {
|
||||
evt.stopPropagation();
|
||||
await this._render_pHandleClickCustomize({rdState, res});
|
||||
});
|
||||
|
||||
return $$`
|
||||
<div class="ui-search__row ve-flex-v-center" tabindex="0">
|
||||
<span>${res.doc.n}</span>
|
||||
<div class="ve-flex-vh-center">
|
||||
<span class="mr-2">${res.doc.s ? `<i title="${Parser.sourceJsonToFull(res.doc.s)}">${Parser.sourceJsonToAbv(res.doc.s)}${res.doc.p ? ` p${res.doc.p}` : ""}</i>` : ""}</span>
|
||||
${$btnCustomize}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async _render_pHandleClickCustomize ({rdState, res}) {
|
||||
const mon = await DataLoader.pCacheAndGet(UrlUtil.PG_BESTIARY, res.doc.s, res.doc.u);
|
||||
if (!mon) return;
|
||||
|
||||
const comp = new _InitiativeTrackerMonsterAddCustomizer({mon});
|
||||
|
||||
const resModal = await comp.pGetShowModalResults();
|
||||
if (resModal == null) return;
|
||||
|
||||
const [isDataEntered, data] = resModal;
|
||||
if (!isDataEntered) return;
|
||||
|
||||
await rdState.cbDoClose(
|
||||
true,
|
||||
new _MonstersToLoad({
|
||||
count: this._getCntToAdd(),
|
||||
name: res.doc.n,
|
||||
source: res.doc.s,
|
||||
isRollHp: this._state.isRollHp,
|
||||
...data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
|
||||
class _InitiativeTrackerNetworkingP2pMetaV1 {
|
||||
constructor () {
|
||||
this.rows = [];
|
||||
this.serverInfo = null;
|
||||
this.serverPeer = null;
|
||||
}
|
||||
}
|
||||
|
||||
class _InitiativeTrackerNetworkingP2pMetaV0 {
|
||||
constructor () {
|
||||
this.rows = [];
|
||||
this.serverInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerNetworking {
|
||||
constructor ({board}) {
|
||||
this._board = board;
|
||||
|
||||
this._p2pMetaV1 = new _InitiativeTrackerNetworkingP2pMetaV1();
|
||||
this._p2pMetaV0 = new _InitiativeTrackerNetworkingP2pMetaV0();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
sendStateToClients ({fnGetToSend}) {
|
||||
return this._sendMessageToClients({fnGetToSend});
|
||||
}
|
||||
|
||||
sendShowImageMessageToClients ({imageHref}) {
|
||||
return this._sendMessageToClients({
|
||||
fnGetToSend: () => ({
|
||||
type: "showImage",
|
||||
payload: {
|
||||
imageHref,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
_sendMessageToClients ({fnGetToSend}) {
|
||||
let toSend = null;
|
||||
|
||||
// region V1
|
||||
if (this._p2pMetaV1.serverPeer) {
|
||||
if (!this._p2pMetaV1.serverPeer.hasConnections()) return;
|
||||
|
||||
toSend ||= fnGetToSend();
|
||||
this._p2pMetaV1.serverPeer.sendMessage(toSend);
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region V0
|
||||
if (this._p2pMetaV0.serverInfo) {
|
||||
this._p2pMetaV0.rows = this._p2pMetaV0.rows.filter(row => !row.isDeleted);
|
||||
this._p2pMetaV0.serverInfo = this._p2pMetaV0.serverInfo.filter(row => {
|
||||
if (row.isDeleted) {
|
||||
row.server.close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
toSend ||= fnGetToSend();
|
||||
try {
|
||||
this._p2pMetaV0.serverInfo.filter(info => info.server.isActive).forEach(info => info.server.sendMessage(toSend));
|
||||
} catch (e) { setTimeout(() => { throw e; }); }
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @param opts
|
||||
* @param opts.doUpdateExternalStates
|
||||
* @param [opts.$btnStartServer]
|
||||
* @param [opts.$btnGetToken]
|
||||
* @param [opts.$btnGetLink]
|
||||
* @param [opts.fnDispServerStoppedState]
|
||||
* @param [opts.fnDispServerRunningState]
|
||||
*/
|
||||
async startServerV1 (opts) {
|
||||
opts = opts || {};
|
||||
|
||||
if (this._p2pMetaV1.serverPeer) {
|
||||
await this._p2pMetaV1.serverPeer.pInit();
|
||||
return {
|
||||
isRunning: true,
|
||||
token: this._p2pMetaV1.serverPeer?.token,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (opts.$btnStartServer) opts.$btnStartServer.prop("disabled", true);
|
||||
this._p2pMetaV1.serverPeer = new PeerVeServer();
|
||||
await this._p2pMetaV1.serverPeer.pInit();
|
||||
if (opts.$btnGetToken) opts.$btnGetToken.prop("disabled", false);
|
||||
if (opts.$btnGetLink) opts.$btnGetLink.prop("disabled", false);
|
||||
|
||||
this._p2pMetaV1.serverPeer.on("connection", connection => {
|
||||
const pConnected = new Promise(resolve => {
|
||||
connection.on("open", () => {
|
||||
resolve(true);
|
||||
opts.doUpdateExternalStates();
|
||||
});
|
||||
});
|
||||
const pTimeout = MiscUtil.pDelay(5 * 1000, false);
|
||||
Promise.race([pConnected, pTimeout])
|
||||
.then(didConnect => {
|
||||
if (!didConnect) {
|
||||
JqueryUtil.doToast({content: `Connecting to "${connection.label.escapeQuotes()}" has taken more than 5 seconds! The connection may need to be re-attempted.`, type: "warning"});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(window).on("beforeunload", evt => {
|
||||
const message = `The connection will be closed`;
|
||||
(evt || window.event).message = message;
|
||||
return message;
|
||||
});
|
||||
|
||||
if (opts.fnDispServerRunningState) opts.fnDispServerRunningState();
|
||||
|
||||
return {
|
||||
isRunning: true,
|
||||
token: this._p2pMetaV1.serverPeer?.token,
|
||||
};
|
||||
} catch (e) {
|
||||
if (opts.fnDispServerStoppedState) opts.fnDispServerStoppedState();
|
||||
if (opts.$btnStartServer) opts.$btnStartServer.prop("disabled", false);
|
||||
this._p2pMetaV1.serverPeer = null;
|
||||
JqueryUtil.doToast({content: `Failed to start server! ${VeCt.STR_SEE_CONSOLE}`, type: "danger"});
|
||||
setTimeout(() => { throw e; });
|
||||
}
|
||||
|
||||
return {
|
||||
isRunning: false,
|
||||
token: this._p2pMetaV1.serverPeer?.token,
|
||||
};
|
||||
}
|
||||
|
||||
handleClick_playerWindowV1 ({doUpdateExternalStates}) {
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
title: "Configure Player View",
|
||||
isUncappedHeight: true,
|
||||
isHeight100: true,
|
||||
cbClose: () => {
|
||||
if (this._p2pMetaV1.rows.length) this._p2pMetaV1.rows.forEach(row => row.$row.detach());
|
||||
if (this._p2pMetaV1.serverPeer) this._p2pMetaV1.serverPeer.offTemp("connection");
|
||||
},
|
||||
});
|
||||
|
||||
const $wrpHelp = UiUtil.$getAddModalRow($modalInner, "div");
|
||||
|
||||
const fnDispServerStoppedState = () => {
|
||||
$btnStartServer.html(`<span class="glyphicon glyphicon-play"></span> Start Server`).prop("disabled", false);
|
||||
$btnGetToken.prop("disabled", true);
|
||||
$btnGetLink.prop("disabled", true);
|
||||
};
|
||||
|
||||
const fnDispServerRunningState = () => {
|
||||
$btnStartServer.html(`<span class="glyphicon glyphicon-play"></span> Server Running`).prop("disabled", true);
|
||||
$btnGetToken.prop("disabled", false);
|
||||
$btnGetLink.prop("disabled", false);
|
||||
};
|
||||
|
||||
const $btnStartServer = $(`<button class="btn btn-default mr-2"></button>`)
|
||||
.click(async () => {
|
||||
const {isRunning} = await this.startServerV1({doUpdateExternalStates, $btnStartServer, $btnGetToken, $btnGetLink, fnDispServerStoppedState, fnDispServerRunningState});
|
||||
if (!isRunning) return;
|
||||
|
||||
this._p2pMetaV1.serverPeer.onTemp("connection", showConnected);
|
||||
showConnected();
|
||||
});
|
||||
|
||||
const $btnGetToken = $(`<button class="btn btn-default mr-2" disabled><span class="glyphicon glyphicon-copy"></span> Copy Token</button>`).appendTo($wrpHelp)
|
||||
.click(async () => {
|
||||
await MiscUtil.pCopyTextToClipboard(this._p2pMetaV1.serverPeer.token);
|
||||
JqueryUtil.showCopiedEffect($btnGetToken);
|
||||
});
|
||||
|
||||
const $btnGetLink = $(`<button class="btn btn-default" disabled><span class="glyphicon glyphicon-link"></span> Copy Link</button>`).appendTo($wrpHelp)
|
||||
.click(async () => {
|
||||
const cleanOrigin = window.location.origin.replace(/\/+$/, "");
|
||||
const url = `${cleanOrigin}/inittrackerplayerview.html#v1:${this._p2pMetaV1.serverPeer.token}`;
|
||||
await MiscUtil.pCopyTextToClipboard(url);
|
||||
JqueryUtil.showCopiedEffect($btnGetLink);
|
||||
});
|
||||
|
||||
if (this._p2pMetaV1.serverPeer) fnDispServerRunningState();
|
||||
else fnDispServerStoppedState();
|
||||
|
||||
$$`<div class="row w-100">
|
||||
<div class="col-12">
|
||||
<p>
|
||||
The Player View is part of a peer-to-peer system to allow players to connect to a DM's initiative tracker. Players should use the <a href="inittrackerplayerview.html">Initiative Tracker Player View</a> page to connect to the DM's instance. As a DM, the usage is as follows:
|
||||
<ol>
|
||||
<li>Start the server.</li>
|
||||
<li>Copy your link/token and share it with your players.</li>
|
||||
<li>Wait for them to connect!</li>
|
||||
</ol>
|
||||
</p>
|
||||
<p>${$btnStartServer}${$btnGetLink}${$btnGetToken}</p>
|
||||
<p><i>Please note that this system is highly experimental. Your experience may vary.</i></p>
|
||||
</div>
|
||||
</div>`.appendTo($wrpHelp);
|
||||
|
||||
UiUtil.addModalSep($modalInner);
|
||||
|
||||
const $wrpConnected = UiUtil.$getAddModalRow($modalInner, "div").addClass("flx-col");
|
||||
|
||||
const showConnected = () => {
|
||||
if (!this._p2pMetaV1.serverPeer) return $wrpConnected.html(`<div class="w-100 ve-flex-vh-center"><i>No clients connected.</i></div>`);
|
||||
|
||||
let stack = `<div class="w-100"><h5>Connected Clients:</h5><ul>`;
|
||||
this._p2pMetaV1.serverPeer.getActiveConnections()
|
||||
.map(it => it.label || "(Unknown)")
|
||||
.sort(SortUtil.ascSortLower)
|
||||
.forEach(it => stack += `<li>${it.escapeQuotes()}</li>`);
|
||||
stack += "</ul></div>";
|
||||
$wrpConnected.html(stack);
|
||||
};
|
||||
|
||||
if (this._p2pMetaV1.serverPeer) this._p2pMetaV1.serverPeer.onTemp("connection", showConnected);
|
||||
|
||||
showConnected();
|
||||
}
|
||||
|
||||
// nop on receiving a message; we want to send only
|
||||
// TODO expand this, to allow e.g. players to set statuses or assign damage/healing (at DM approval?)
|
||||
_playerWindowV0_DM_MESSAGE_RECEIVER = function () {};
|
||||
|
||||
_playerWindowV0_DM_ERROR_HANDLER = function (err) {
|
||||
if (!this.isClosed) {
|
||||
// TODO: this could be better at handling `err.error == "RTCError: User-Initiated Abort, reason=Close called"`
|
||||
JqueryUtil.doToast({
|
||||
content: `Server error:\n${err ? (err.message || err.error || err) : "(Unknown error)"}`,
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async _playerWindowV0_pGetServerTokens ({rowMetas}) {
|
||||
const targetRows = rowMetas.filter(it => !it.isDeleted).filter(it => !it.isActive);
|
||||
if (targetRows.every(it => it.isActive)) {
|
||||
return JqueryUtil.doToast({
|
||||
content: "No rows require Server Token generation!",
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
let anyInvalidNames = false;
|
||||
targetRows.forEach(row => {
|
||||
row.$iptName.removeClass("error-background");
|
||||
if (!row.$iptName.val().trim()) {
|
||||
anyInvalidNames = true;
|
||||
row.$iptName.addClass("error-background");
|
||||
}
|
||||
});
|
||||
if (anyInvalidNames) return;
|
||||
|
||||
const names = targetRows.map(row => {
|
||||
row.isActive = true;
|
||||
|
||||
row.$iptName.attr("disabled", true);
|
||||
row.$btnGenServerToken.attr("disabled", true);
|
||||
|
||||
return row.$iptName.val();
|
||||
});
|
||||
|
||||
if (this._p2pMetaV0.serverInfo) {
|
||||
await this._p2pMetaV0.serverInfo;
|
||||
|
||||
const serverInfo = await PeerUtilV0.pInitialiseServersAddToExisting(
|
||||
names,
|
||||
this._p2pMetaV0.serverInfo,
|
||||
this._playerWindowV0_DM_MESSAGE_RECEIVER,
|
||||
this._playerWindowV0_DM_ERROR_HANDLER,
|
||||
);
|
||||
|
||||
return targetRows.map((row, i) => {
|
||||
row.name = serverInfo[i].name;
|
||||
row.serverInfo = serverInfo[i];
|
||||
row.$iptTokenServer.val(serverInfo[i].textifiedSdp).attr("disabled", false);
|
||||
|
||||
serverInfo[i].rowMeta = row;
|
||||
|
||||
row.$iptTokenClient.attr("disabled", false);
|
||||
row.$btnAcceptClientToken.attr("disabled", false);
|
||||
|
||||
return serverInfo[i].textifiedSdp;
|
||||
});
|
||||
} else {
|
||||
this._p2pMetaV0.serverInfo = (async () => {
|
||||
this._p2pMetaV0.serverInfo = await PeerUtilV0.pInitialiseServers(names, this._playerWindowV0_DM_MESSAGE_RECEIVER, this._playerWindowV0_DM_ERROR_HANDLER);
|
||||
|
||||
targetRows.forEach((row, i) => {
|
||||
row.name = this._p2pMetaV0.serverInfo[i].name;
|
||||
row.serverInfo = this._p2pMetaV0.serverInfo[i];
|
||||
row.$iptTokenServer.val(this._p2pMetaV0.serverInfo[i].textifiedSdp).attr("disabled", false);
|
||||
|
||||
this._p2pMetaV0.serverInfo[i].rowMeta = row;
|
||||
|
||||
row.$iptTokenClient.attr("disabled", false);
|
||||
row.$btnAcceptClientToken.attr("disabled", false);
|
||||
});
|
||||
})();
|
||||
|
||||
await this._p2pMetaV0.serverInfo;
|
||||
return targetRows.map(row => row.serverInfo.textifiedSdp);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick_playerWindowV0 ({doUpdateExternalStates}) {
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
title: "Configure Player View",
|
||||
isUncappedHeight: true,
|
||||
isHeight100: true,
|
||||
cbClose: () => {
|
||||
if (this._p2pMetaV0.rows.length) this._p2pMetaV0.rows.forEach(row => row.$row.detach());
|
||||
},
|
||||
});
|
||||
|
||||
const $wrpHelp = UiUtil.$getAddModalRow($modalInner, "div");
|
||||
const $btnAltGenAll = $(`<button class="btn btn-primary btn-text-insert">Generate All</button>`).click(() => $btnGenServerTokens.click());
|
||||
const $btnAltCopyAll = $(`<button class="btn btn-primary btn-text-insert">Copy Server Tokens</button>`).click(() => $btnCopyServers.click());
|
||||
$$`<div class="ve-flex w-100">
|
||||
<div class="col-12">
|
||||
<p>
|
||||
The Player View is part of a peer-to-peer (i.e., serverless) system to allow players to connect to a DM's initiative tracker. Players should use the <a href="inittrackerplayerview.html">Initiative Tracker Player View</a> page to connect to the DM's instance. As a DM, the usage is as follows:
|
||||
<ol>
|
||||
<li>Add the required number of players, and input (preferably unique) player names.</li>
|
||||
<li>Click "${$btnAltGenAll}," which will generate a "server token" per player. You can click "${$btnAltCopyAll}" to copy them all as a single block of text, or click on the "Server Token" values to copy them individually. Distribute these tokens to your players (via a messaging service of your choice; we recommend <a href="https://discordapp.com/">Discord</a>). Each player should paste their token into the <a href="inittrackerplayerview.html">Initiative Tracker Player View</a>, following the instructions provided therein.</li>
|
||||
<li>
|
||||
Get a resulting "client token" from each player via a messaging service of your choice. Then, either:
|
||||
<ol type="a">
|
||||
<li>Click the "Accept Multiple Clients" button, and paste in text containing multiple client tokens. <b>This will try to find tokens in <i>any</i> text, ignoring everything else.</b> Pasting a chatroom log (containing, for example, usernames and timestamps mixed with tokens) is the expected usage.</li>
|
||||
<li>Paste each token into the appropriate "Client Token" field and "Accept Client" on each. A token can be identified by the slugified player name in the first few characters.</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</p>
|
||||
<p>Once a player's client has been "accepted," it will receive updates from the DM's tracker. <i>Please note that this system is highly experimental. Your experience may vary.</i></p>
|
||||
</div>
|
||||
</div>`.appendTo($wrpHelp);
|
||||
|
||||
UiUtil.addModalSep($modalInner);
|
||||
|
||||
const $wrpTop = UiUtil.$getAddModalRow($modalInner, "div");
|
||||
|
||||
const $btnAddClient = $(`<button class="btn btn-xs btn-primary" title="Add Client">Add Player</button>`).click(() => addClientRow());
|
||||
|
||||
const $btnCopyServers = $(`<button class="btn btn-xs btn-primary" title="Copy any available server tokens to the clipboard">Copy Server Tokens</button>`)
|
||||
.click(async () => {
|
||||
const targetRows = this._p2pMetaV0.rows.filter(it => !it.isDeleted && !it.$iptTokenClient.attr("disabled"));
|
||||
if (!targetRows.length) {
|
||||
JqueryUtil.doToast({
|
||||
content: `No free server tokens to copy. Generate some!`,
|
||||
type: "warning",
|
||||
});
|
||||
} else {
|
||||
await MiscUtil.pCopyTextToClipboard(targetRows.map(it => it.$iptTokenServer.val()).join("\n\n"));
|
||||
JqueryUtil.showCopiedEffect($btnGenServerTokens);
|
||||
}
|
||||
});
|
||||
|
||||
const $btnAcceptClients = $(`<button class="btn btn-xs btn-primary" title="Open a prompt into which text containing client tokens can be pasted">Accept Multiple Clients</button>`)
|
||||
.click(() => {
|
||||
const {$modalInner, doClose} = UiUtil.getShowModal({title: "Accept Multiple Clients"});
|
||||
|
||||
const $iptText = $(`<textarea class="form-control dm-init-pl__textarea block mb-2"></textarea>`)
|
||||
.keydown(() => $iptText.removeClass("error-background"));
|
||||
|
||||
const $btnAccept = $(`<button class="btn btn-xs btn-primary block ve-text-center" title="Add Client">Accept Multiple Clients</button>`)
|
||||
.click(async () => {
|
||||
$iptText.removeClass("error-background");
|
||||
const txt = $iptText.val();
|
||||
if (!txt.trim() || !PeerUtilV0.containsAnyTokens(txt)) {
|
||||
$iptText.addClass("error-background");
|
||||
} else {
|
||||
const connected = await PeerUtilV0.pConnectClientsToServers(this._p2pMetaV0.serverInfo, txt);
|
||||
this._board.doBindAlertOnNavigation();
|
||||
connected.forEach(serverInfo => {
|
||||
serverInfo.rowMeta.$iptTokenClient.val(serverInfo._tempTokenToDisplay || "").attr("disabled", true);
|
||||
serverInfo.rowMeta.$btnAcceptClientToken.attr("disabled", true);
|
||||
delete serverInfo._tempTokenToDisplay;
|
||||
});
|
||||
doClose();
|
||||
doUpdateExternalStates();
|
||||
}
|
||||
});
|
||||
|
||||
$$`<div>
|
||||
<p>Paste text containing one or more client tokens, and click "Accept Multiple Clients"</p>
|
||||
${$iptText}
|
||||
<div class="ve-flex-vh-center">${$btnAccept}</div>
|
||||
</div>`.appendTo($modalInner);
|
||||
});
|
||||
|
||||
$$`
|
||||
<div class="ve-flex w-100">
|
||||
<div class="col-12">
|
||||
<div class="ve-flex-inline-v-center mr-2">
|
||||
<span class="mr-1">Add a player (client):</span>
|
||||
${$btnAddClient}
|
||||
</div>
|
||||
<div class="ve-flex-inline-v-center mr-2">
|
||||
<span class="mr-1">Copy all un-paired server tokens:</span>
|
||||
${$btnCopyServers}
|
||||
</div>
|
||||
<div class="ve-flex-inline-v-center mr-2">
|
||||
<span class="mr-1">Mass-accept clients:</span>
|
||||
${$btnAcceptClients}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`.appendTo($wrpTop);
|
||||
|
||||
UiUtil.addModalSep($modalInner);
|
||||
|
||||
const $btnGenServerTokens = $(`<button class="btn btn-primary btn-xs">Generate All</button>`)
|
||||
.click(() => this._playerWindowV0_pGetServerTokens({rowMetas: this._p2pMetaV0.rows}));
|
||||
|
||||
UiUtil.$getAddModalRow($modalInner, "div")
|
||||
.append($$`
|
||||
<div class="ve-flex w-100">
|
||||
<div class="col-2 bold">Player Name</div>
|
||||
<div class="col-3-5 bold">Server Token</div>
|
||||
<div class="col-1 ve-text-center">${$btnGenServerTokens}</div>
|
||||
<div class="col-3-5 bold">Client Token</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const _get$rowTemplate = (
|
||||
$iptName,
|
||||
$iptTokenServer,
|
||||
$btnGenServerToken,
|
||||
$iptTokenClient,
|
||||
$btnAcceptClientToken,
|
||||
$btnDeleteClient,
|
||||
) => $$`<div class="w-100 mb-2 ve-flex">
|
||||
<div class="col-2 pr-1">${$iptName}</div>
|
||||
<div class="col-3-5 px-1">${$iptTokenServer}</div>
|
||||
<div class="col-1 px-1 ve-flex-vh-center">${$btnGenServerToken}</div>
|
||||
<div class="col-3-5 px-1">${$iptTokenClient}</div>
|
||||
<div class="col-1-5 px-1 ve-flex-vh-center">${$btnAcceptClientToken}</div>
|
||||
<div class="col-0-5 pl-1 ve-flex-vh-center">${$btnDeleteClient}</div>
|
||||
</div>`;
|
||||
|
||||
const clientRowMetas = [];
|
||||
const addClientRow = () => {
|
||||
const rowMeta = {id: CryptUtil.uid()};
|
||||
clientRowMetas.push(rowMeta);
|
||||
|
||||
const $iptName = $(`<input class="form-control input-sm">`)
|
||||
.keydown(evt => {
|
||||
$iptName.removeClass("error-background");
|
||||
if (evt.key === "Enter") $btnGenServerToken.click();
|
||||
});
|
||||
|
||||
const $iptTokenServer = $(`<input class="form-control input-sm copyable code" readonly disabled>`)
|
||||
.click(async () => {
|
||||
await MiscUtil.pCopyTextToClipboard($iptTokenServer.val());
|
||||
JqueryUtil.showCopiedEffect($iptTokenServer);
|
||||
}).disableSpellcheck();
|
||||
|
||||
const $btnGenServerToken = $(`<button class="btn btn-xs btn-primary" title="Generate Server Token">Generate</button>`)
|
||||
.click(() => this._playerWindowV0_pGetServerTokens({rowMetas: [rowMeta]}));
|
||||
|
||||
const $iptTokenClient = $(`<input class="form-control input-sm code" disabled>`)
|
||||
.keydown(evt => {
|
||||
$iptTokenClient.removeClass("error-background");
|
||||
if (evt.key === "Enter") $btnAcceptClientToken.click();
|
||||
}).disableSpellcheck();
|
||||
|
||||
const $btnAcceptClientToken = $(`<button class="btn btn-xs btn-primary" title="Accept Client Token" disabled>Accept Client</button>`)
|
||||
.click(async () => {
|
||||
const token = $iptTokenClient.val();
|
||||
if (PeerUtilV0.isValidToken(token)) {
|
||||
try {
|
||||
await PeerUtilV0.pConnectClientsToServers([rowMeta.serverInfo], token);
|
||||
this._board.doBindAlertOnNavigation();
|
||||
$iptTokenClient.prop("disabled", true);
|
||||
$btnAcceptClientToken.prop("disabled", true);
|
||||
doUpdateExternalStates();
|
||||
} catch (e) {
|
||||
JqueryUtil.doToast({
|
||||
content: `Failed to accept client token! Are you sure it was valid? (See the log for more details.)`,
|
||||
type: "danger",
|
||||
});
|
||||
setTimeout(() => { throw e; });
|
||||
}
|
||||
} else $iptTokenClient.addClass("error-background");
|
||||
});
|
||||
|
||||
const $btnDeleteClient = $(`<button class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span></button>`)
|
||||
.click(() => {
|
||||
rowMeta.$row.remove();
|
||||
rowMeta.isDeleted = true;
|
||||
if (rowMeta.serverInfo) {
|
||||
rowMeta.serverInfo.server.close();
|
||||
rowMeta.serverInfo.isDeleted = true;
|
||||
}
|
||||
const ix = clientRowMetas.indexOf(rowMeta);
|
||||
if (~ix) clientRowMetas.splice(ix, 1);
|
||||
|
||||
if (!clientRowMetas.length) addClientRow();
|
||||
});
|
||||
|
||||
rowMeta.$row = _get$rowTemplate(
|
||||
$iptName,
|
||||
$iptTokenServer,
|
||||
$btnGenServerToken,
|
||||
$iptTokenClient,
|
||||
$btnAcceptClientToken,
|
||||
$btnDeleteClient,
|
||||
).appendTo($wrpRowsInner);
|
||||
|
||||
rowMeta.$iptName = $iptName;
|
||||
rowMeta.$iptTokenServer = $iptTokenServer;
|
||||
rowMeta.$btnGenServerToken = $btnGenServerToken;
|
||||
rowMeta.$iptTokenClient = $iptTokenClient;
|
||||
rowMeta.$btnAcceptClientToken = $btnAcceptClientToken;
|
||||
this._p2pMetaV0.rows.push(rowMeta);
|
||||
|
||||
return rowMeta;
|
||||
};
|
||||
|
||||
const $wrpRows = UiUtil.$getAddModalRow($modalInner, "div");
|
||||
const $wrpRowsInner = $(`<div class="w-100"></div>`).appendTo($wrpRows);
|
||||
|
||||
if (this._p2pMetaV0.rows.length) this._p2pMetaV0.rows.forEach(row => row.$row.appendTo($wrpRowsInner));
|
||||
else addClientRow();
|
||||
}
|
||||
|
||||
async pHandleDoConnectLocalV0 ({clientView}) {
|
||||
// generate a stub/fake row meta
|
||||
const rowMeta = {
|
||||
id: CryptUtil.uid(),
|
||||
$row: $(),
|
||||
$iptName: $(`<input value="local">`),
|
||||
$iptTokenServer: $(),
|
||||
$btnGenServerToken: $(),
|
||||
$iptTokenClient: $(),
|
||||
$btnAcceptClientToken: $(),
|
||||
};
|
||||
|
||||
this._p2pMetaV0.rows.push(rowMeta);
|
||||
|
||||
const serverTokens = await this._playerWindowV0_pGetServerTokens({rowMetas: [rowMeta]});
|
||||
const clientData = await PeerUtilV0.pInitialiseClient(
|
||||
serverTokens[0],
|
||||
msg => clientView.handleMessage(msg),
|
||||
() => {}, // ignore local errors
|
||||
);
|
||||
clientView.clientData = clientData;
|
||||
await PeerUtilV0.pConnectClientsToServers([rowMeta.serverInfo], clientData.textifiedSdp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export class InitiativeTrackerRoller {
|
||||
static _getRollName (name) {
|
||||
return `Initiative Tracker${name ? ` \u2014 ${name}` : ""}`;
|
||||
}
|
||||
|
||||
async pGetRollInitiative ({mon, name, initiativeModifier}) {
|
||||
name ??= mon?.name;
|
||||
initiativeModifier ??= mon ? Parser.getAbilityModifier(mon.dex) : 0;
|
||||
|
||||
return Renderer.dice.pRoll2(`1d20${UiUtil.intToBonus(initiativeModifier)}`, {
|
||||
isUser: false,
|
||||
name: this.constructor._getRollName(name ?? mon?.name),
|
||||
label: "Initiative",
|
||||
}, {isResultUsed: true});
|
||||
}
|
||||
|
||||
async pGetOrRollHp (mon, {isRollHp}) {
|
||||
if (!isRollHp && mon.hp.average && !isNaN(mon.hp.average)) return Number(mon.hp.average);
|
||||
|
||||
if (isRollHp && mon.hp.formula) {
|
||||
return Renderer.dice.pRoll2(mon.hp.formula, {
|
||||
isUser: false,
|
||||
name: this.constructor._getRollName(mon?.name),
|
||||
label: "HP",
|
||||
}, {isResultUsed: true});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
import {
|
||||
IS_PLAYER_VISIBLE_ALL,
|
||||
IS_PLAYER_VISIBLE_NONE,
|
||||
} from "./dmscreen-initiativetracker-statcolumns.js";
|
||||
import {InitiativeTrackerConditionAdd} from "./dmscreen-initiativetracker-conditionadd.js";
|
||||
import {InitiativeTrackerUi} from "./dmscreen-initiativetracker-ui.js";
|
||||
import {InitiativeTrackerConst} from "./dmscreen-initiativetracker-consts.js";
|
||||
import {InitiativeTrackerSort} from "./dmscreen-initiativetracker-sort.js";
|
||||
import {RenderableCollectionConditions} from "../../initiativetracker/initiativetracker-utils.js";
|
||||
import {
|
||||
InitiativeTrackerRowDataViewBase,
|
||||
RenderableCollectionRowDataBase,
|
||||
} from "./dmscreen-initiativetracker-rowsbase.js";
|
||||
import {InitiativeTrackerRowStateBuilderActive} from "./dmscreen-initiativetracker-rowstatebuilder.js";
|
||||
|
||||
class _RenderableCollectionRowDataActive extends RenderableCollectionRowDataBase {
|
||||
constructor (
|
||||
{
|
||||
comp,
|
||||
$wrpRows,
|
||||
roller,
|
||||
networking,
|
||||
rowStateBuilder,
|
||||
},
|
||||
) {
|
||||
super({comp, prop: "rows", $wrpRows, roller, networking, rowStateBuilder});
|
||||
}
|
||||
|
||||
async _pPopulateRow_pGetMonsterMeta ({comp}) {
|
||||
const isMon = !!comp._state.source;
|
||||
|
||||
const mon = isMon
|
||||
? await this._rowStateBuilder.pGetScaledCreature({isMon, ...comp._state})
|
||||
: null;
|
||||
const fluff = mon ? await Renderer.monster.pGetFluff(mon) : null;
|
||||
|
||||
return {
|
||||
isMon,
|
||||
mon,
|
||||
fluff,
|
||||
};
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_monster ({comp, $wrpLhs, isMon, mon, fluff}) {
|
||||
if (!isMon) return;
|
||||
|
||||
const $dispOrdinal = $(`<span class="dm-init__number"></span>`);
|
||||
comp._addHookBase("ordinal", () => $dispOrdinal.text(`(${comp._state.ordinal})`))();
|
||||
comp._addHookBase("isShowOrdinal", () => $dispOrdinal.toggleVe(comp._state.isShowOrdinal))();
|
||||
|
||||
const $lnk = $(this._pPopulateRow_monster_getRenderedLink({comp}))
|
||||
.attr("tabindex", "-1");
|
||||
comp._addHookBase("customName", () => {
|
||||
$lnk.text(comp._state.customName ? comp._state.customName : comp._state.displayName || comp._state.name);
|
||||
})();
|
||||
|
||||
const $btnRename = $(`<button class="btn btn-default btn-xs dm-init-lockable dm-init__btn-creature" title="Rename (SHIFT to Reset)" tabindex="-1"><span class="glyphicon glyphicon-pencil"></span></button>`)
|
||||
.click(async evt => {
|
||||
if (this._comp._state.isLocked) return;
|
||||
|
||||
if (evt.shiftKey) return comp._state.customName = null;
|
||||
|
||||
const customName = await InputUiUtil.pGetUserString({title: "Enter Name"});
|
||||
if (customName == null || !customName.trim()) return;
|
||||
comp._state.customName = customName;
|
||||
});
|
||||
|
||||
const $btnDuplicate = $(`<button class="btn btn-success btn-xs dm-init-lockable dm-init__btn-creature" title="Add Another (SHIFT for Roll New)" tabindex="-1"><span class="glyphicon glyphicon-plus"></span></button>`)
|
||||
.click(async (evt) => {
|
||||
if (this._comp._state.isLocked) return;
|
||||
|
||||
const isRollNew = !!evt.shiftKey;
|
||||
|
||||
const initiative = isRollNew ? await this._roller.pGetRollInitiative({mon}) : comp._state.initiative;
|
||||
const isActive = isRollNew ? (initiative === comp._state.initiative) : comp._state.isActive;
|
||||
|
||||
const hpMax = isRollNew ? await this._roller.pGetOrRollHp(mon, {isRollHp: this._comp._state.isRollHp}) : comp._state.hpMax;
|
||||
|
||||
const similarCreatureRows = this._rowStateBuilder.getSimilarRows({rowEntity: comp._state});
|
||||
|
||||
this._comp._state[this._prop] = InitiativeTrackerSort.getSortedRows({
|
||||
rows: [
|
||||
...this._comp._state[this._prop],
|
||||
await this._rowStateBuilder.pGetNewRowState({
|
||||
isActive,
|
||||
isPlayerVisible: comp._state.isPlayerVisible,
|
||||
name: comp._state.name,
|
||||
displayName: comp._state.displayName,
|
||||
scaledCr: comp._state.scaledCr,
|
||||
scaledSummonSpellLevel: comp._state.scaledSummonSpellLevel,
|
||||
scaledSummonClassLevel: comp._state.scaledSummonClassLevel,
|
||||
customName: comp._state.customName,
|
||||
source: comp._state.source,
|
||||
hpCurrent: hpMax, // Always reset to max HP
|
||||
hpMax: hpMax,
|
||||
initiative,
|
||||
ordinal: Math.max(...similarCreatureRows.map(row => row.entity.ordinal)) + 1,
|
||||
rowStatColData: isRollNew ? this._rowStateBuilder._getInitialRowStatColData({mon, fluff}) : MiscUtil.copyFast(comp._state.rowStatColData),
|
||||
conditions: [],
|
||||
}),
|
||||
]
|
||||
.filter(Boolean),
|
||||
sortBy: this._comp._state.sort,
|
||||
sortDir: this._comp._state.dir,
|
||||
});
|
||||
});
|
||||
|
||||
$$`<div class="dm-init__wrp-creature split">
|
||||
<span class="dm-init__wrp-creature-link">
|
||||
${$lnk}
|
||||
${$dispOrdinal}
|
||||
</span>
|
||||
<div class="ve-flex-v-center btn-group mr-3p">
|
||||
${$btnRename}
|
||||
${$btnDuplicate}
|
||||
</div>
|
||||
</div>`.appendTo($wrpLhs);
|
||||
}
|
||||
|
||||
_pPopulateRow_monster_getRenderedLink ({comp}) {
|
||||
if (
|
||||
comp._state.scaledCr == null
|
||||
&& comp._state.scaledSummonSpellLevel == null
|
||||
&& comp._state.scaledSummonClassLevel == null
|
||||
) return Renderer.get().render(`{@creature ${comp._state.name}|${comp._state.source}}`);
|
||||
|
||||
const parts = [
|
||||
comp._state.name,
|
||||
comp._state.source,
|
||||
comp._state.displayName,
|
||||
comp._state.scaledCr != null
|
||||
? `${VeCt.HASH_SCALED}=${Parser.numberToCr(comp._state.scaledCr)}`
|
||||
: comp._state.scaledSummonSpellLevel != null
|
||||
? `${VeCt.HASH_SCALED_SPELL_SUMMON}=${comp._state.scaledSummonSpellLevel}`
|
||||
: comp._state.scaledSummonClassLevel != null
|
||||
? `${VeCt.HASH_SCALED_CLASS_SUMMON}=${comp._state.scaledSummonClassLevel}`
|
||||
: null,
|
||||
];
|
||||
return Renderer.get().render(`{@creature ${parts.join("|")}}`);
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_conditions ({comp, $wrpLhs}) {
|
||||
const $btnAddCond = $(`<button class="btn btn-warning btn-xs dm-init__row-btn dm-init__row-btn-flag" title="Add Condition" tabindex="-1"><span class="glyphicon glyphicon-flag"></span></button>`)
|
||||
.on("click", async () => {
|
||||
const compAdd = new InitiativeTrackerConditionAdd({conditionsCustom: MiscUtil.copyFast(this._comp._state.conditionsCustom)});
|
||||
const [isDataEntered, conditionToAdd] = await compAdd.pGetShowModalResults();
|
||||
|
||||
// Always update the set of custom conditions
|
||||
this._comp._state.conditionsCustom = compAdd.getConditionsCustom();
|
||||
|
||||
if (!isDataEntered) return;
|
||||
|
||||
comp._state.conditions = [
|
||||
...comp._state.conditions,
|
||||
conditionToAdd,
|
||||
];
|
||||
});
|
||||
|
||||
const $wrpConds = $(`<div class="init__wrp_conds h-100"></div>`);
|
||||
|
||||
$$`<div class="split">
|
||||
${$wrpConds}
|
||||
${$btnAddCond}
|
||||
</div>`.appendTo($wrpLhs);
|
||||
|
||||
const collectionConditions = new RenderableCollectionConditions({
|
||||
comp: comp,
|
||||
$wrpRows: $wrpConds,
|
||||
});
|
||||
comp._addHookBase("conditions", () => collectionConditions.render())();
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_initiative ({comp, $wrpRhs}) {
|
||||
const $iptInitiative = ComponentUiUtil.$getIptNumber(
|
||||
comp,
|
||||
"initiative",
|
||||
null,
|
||||
{
|
||||
isAllowNull: true,
|
||||
fallbackOnNaN: null,
|
||||
html: `<input class="form-control input-sm score dm-init-lockable dm-init__row-input ve-text-center dm-init__ipt--rhs">`,
|
||||
},
|
||||
)
|
||||
.on("click", () => $iptInitiative.select())
|
||||
.appendTo($wrpRhs);
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_btns ({comp, entity, $wrpRhs}) {
|
||||
const $btnVisible = InitiativeTrackerUi.$getBtnPlayerVisible(
|
||||
comp._state.isPlayerVisible,
|
||||
() => comp._state.isPlayerVisible = $btnVisible.hasClass("btn-primary")
|
||||
? IS_PLAYER_VISIBLE_ALL
|
||||
: IS_PLAYER_VISIBLE_NONE,
|
||||
false,
|
||||
)
|
||||
.title("Shown in player view")
|
||||
.addClass("dm-init__row-btn")
|
||||
.addClass("dm-init__btn_eye")
|
||||
.appendTo($wrpRhs);
|
||||
|
||||
$(`<button class="btn btn-danger btn-xs dm-init__row-btn dm-init-lockable" title="Delete (SHIFT to Also Delete Similar)" tabindex="-1"><span class="glyphicon glyphicon-trash"></span></button>`)
|
||||
.appendTo($wrpRhs)
|
||||
.on("click", evt => {
|
||||
if (this._comp._state.isLocked) return;
|
||||
|
||||
if (evt.shiftKey) {
|
||||
return this._utils.doDeleteMultiple({
|
||||
entities: this._rowStateBuilder.getSimilarRows({
|
||||
rows: this._comp[this._prop],
|
||||
rowEntity: entity.entity,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
this._utils.doDelete({entity});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_doHandleTurnStart ({rows, direction, row, isSkipRoundStart}) {
|
||||
const {isRoundStart = false} = isSkipRoundStart ? {} : this._doHandleRoundStart({rows, direction, row});
|
||||
|
||||
this._comp._getRenderedCollection({prop: this._prop})[row.id]?.cbOnTurnStart({state: row, direction});
|
||||
|
||||
return {isRoundStart};
|
||||
}
|
||||
|
||||
_doHandleRoundStart ({rows, direction, row}) {
|
||||
if (!rows.length) return {isRoundStart: false};
|
||||
|
||||
if (rows[0]?.id !== row?.id) return {isRoundStart: false};
|
||||
|
||||
if (direction === InitiativeTrackerConst.DIR_FORWARDS) ++this._comp._state.round;
|
||||
|
||||
const rendereds = this._comp._getRenderedCollection({prop: this._prop});
|
||||
rows.forEach(row => rendereds[row.id]?.cbOnRoundStart({state: row, direction}));
|
||||
|
||||
return {isRoundStart: true};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_getSortedRowsCopy ({rows}) {
|
||||
return InitiativeTrackerSort.getSortedRows({
|
||||
rows: MiscUtil.copyFast(rows),
|
||||
sortBy: this._comp._state.sort,
|
||||
sortDir: this._comp._state.dir,
|
||||
});
|
||||
}
|
||||
|
||||
doEnsureAtLeastOneRowActive ({isSilent = false} = {}) {
|
||||
if (this._isAnyRowActive()) return;
|
||||
|
||||
const rows = this._getSortedRowsCopy({rows: (isSilent ? this._comp.__state : this._comp._state)[this._prop]});
|
||||
if (!rows?.length) return;
|
||||
|
||||
const [rowActive] = rows;
|
||||
|
||||
this._doHandleRowSetActive({rows, rowActive, direction: InitiativeTrackerConst.DIR_NEUTRAL});
|
||||
|
||||
if (isSilent) this._comp.__state.rows = rows;
|
||||
else this._comp._triggerCollectionUpdate(this._prop);
|
||||
}
|
||||
|
||||
_isAnyRowActive () {
|
||||
return this._comp._state[this._prop].some(it => it.entity.isActive);
|
||||
}
|
||||
|
||||
_doHandleRowSetActive ({rows, rowActive, direction, isSkipRoundStart}) {
|
||||
const similarRows = this._rowStateBuilder.getSimilarRows({
|
||||
rows,
|
||||
rowEntity: rowActive.entity,
|
||||
});
|
||||
|
||||
if (!similarRows.some(row => row.id === rowActive.id)) throw new Error(`Active row should be "similar" to itself!`); // Should never occur
|
||||
|
||||
const isRoundStart = similarRows
|
||||
.map(row => {
|
||||
const {entity} = row;
|
||||
|
||||
if (
|
||||
this._comp._state.sort === InitiativeTrackerConst.SORT_ORDER_NUM
|
||||
&& entity.initiative !== rowActive.entity.initiative
|
||||
) return false;
|
||||
|
||||
entity.isActive = true;
|
||||
|
||||
return this._doHandleTurnStart({rows, direction, row, isSkipRoundStart}).isRoundStart;
|
||||
})
|
||||
.some(Boolean);
|
||||
|
||||
return {isRoundStart};
|
||||
}
|
||||
|
||||
_pDoShiftActiveRow_doInitialShift ({direction}) {
|
||||
const rows = this._getSortedRowsCopy({rows: this._comp._state[this._prop]});
|
||||
|
||||
if (!this._isAnyRowActive()) return this.doEnsureAtLeastOneRowActive();
|
||||
|
||||
if (direction === InitiativeTrackerConst.DIR_BACKWARDS) rows.reverse();
|
||||
|
||||
const rowsActive = rows.filter(({entity}) => entity.isActive);
|
||||
|
||||
// If advancing, tick down conditions
|
||||
if (direction === InitiativeTrackerConst.DIR_FORWARDS) {
|
||||
rowsActive
|
||||
.forEach(({entity}) => {
|
||||
entity.conditions = entity.conditions
|
||||
.filter(cond => !(cond.entity.turns != null && (--cond.entity.turns <= 0)));
|
||||
});
|
||||
}
|
||||
|
||||
rowsActive.forEach(({entity}) => entity.isActive = false);
|
||||
|
||||
const ixLastActive = rows.indexOf(rowsActive.last());
|
||||
const ixNextActive = ixLastActive + 1 < rows.length ? ixLastActive + 1 : 0;
|
||||
rows[ixNextActive].entity.isActive = true;
|
||||
|
||||
const {isRoundStart} = this._doHandleRowSetActive({rows, rowActive: rows[ixNextActive], direction});
|
||||
|
||||
return {
|
||||
isRoundStart,
|
||||
rows: InitiativeTrackerSort.getSortedRows({
|
||||
rows,
|
||||
sortBy: this._comp._state.sort,
|
||||
sortDir: this._comp._state.dir,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async _pDoShiftActiveRow_pDoRerollAndShift ({direction, rows}) {
|
||||
await this._pMutRowsRerollInitiative({rows});
|
||||
|
||||
rows = InitiativeTrackerSort.getSortedRows({
|
||||
rows,
|
||||
sortBy: this._comp._state.sort,
|
||||
sortDir: this._comp._state.dir,
|
||||
});
|
||||
|
||||
rows.forEach(({entity}) => entity.isActive = false);
|
||||
const rowActive = rows[0];
|
||||
rowActive.entity.isActive = true;
|
||||
|
||||
// FIXME(Future) this results in turn-start triggering twice on rows which were at the top of the pre-reroll order
|
||||
// and the post-reroll order (current turn-start callbacks are idempotent, so this is not relevant)
|
||||
this._doHandleRowSetActive({rows, rowActive, direction, isSkipRoundStart: true});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async pDoShiftActiveRow ({direction}) {
|
||||
const {isRoundStart, rows} = this._pDoShiftActiveRow_doInitialShift({direction});
|
||||
|
||||
if (direction !== InitiativeTrackerConst.DIR_FORWARDS || !isRoundStart || !this._comp._state.isRerollInitiativeEachRound) return this._comp._state[this._prop] = rows;
|
||||
|
||||
this._comp._state[this._prop] = await this._pDoShiftActiveRow_pDoRerollAndShift({direction, rows});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async _pMutRowsRerollInitiative ({rows}) {
|
||||
rows = rows || this._comp._state[this._prop];
|
||||
|
||||
if (!this._comp._state.isRollGroups) {
|
||||
return rows
|
||||
.pSerialAwaitMap(async row => {
|
||||
const {entity} = row;
|
||||
const {mon, initiativeModifier} = await this._rowStateBuilder.pGetRowInitiativeMeta({row});
|
||||
entity.initiative = await this._roller.pGetRollInitiative({mon, initiativeModifier, name: mon ? null : entity.name});
|
||||
});
|
||||
}
|
||||
|
||||
await Object.values(
|
||||
await rows
|
||||
.pSerialAwaitReduce(
|
||||
async (accum, row) => {
|
||||
const rowEntityHash = InitiativeTrackerRowStateBuilderActive.getSimilarRowEntityHash({rowEntity: row.entity});
|
||||
const {initiativeModifier} = await this._rowStateBuilder.pGetRowInitiativeMeta({row});
|
||||
// Add the initiative modifier to the key, such that e.g. creatures with non-standard initiative
|
||||
// modifiers are rolled outwith the group
|
||||
const k = [rowEntityHash, initiativeModifier].join("__");
|
||||
(accum[k] ||= []).push(row);
|
||||
return accum;
|
||||
},
|
||||
{},
|
||||
),
|
||||
)
|
||||
.pSerialAwaitMap(async rows => {
|
||||
const [row] = rows;
|
||||
const {mon, initiativeModifier} = await this._rowStateBuilder.pGetRowInitiativeMeta({row});
|
||||
const initiative = await this._roller.pGetRollInitiative({mon, initiativeModifier, name: mon ? null : row.entity.name});
|
||||
rows.forEach(({entity}) => entity.initiative = initiative);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerRowDataViewActive extends InitiativeTrackerRowDataViewBase {
|
||||
_TextHeaderLhs = "Creature/Status";
|
||||
_ClsRenderableCollectionRowData = _RenderableCollectionRowDataActive;
|
||||
|
||||
_render_$getWrpHeaderRhs ({rdState}) {
|
||||
return $$`<div class="dm-init__row-rhs">
|
||||
<div class="dm-init__header dm-init__header--input dm-init__header--input-wide" title="Hit Points">HP</div>
|
||||
<div class="dm-init__header dm-init__header--input" title="Initiative Score">#</div>
|
||||
<div class="dm-init__spc-header-buttons"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_render_bindHooksRows ({rdState}) {
|
||||
const hkRowsSync = () => {
|
||||
// Sort rows
|
||||
this._comp.__state.rows = InitiativeTrackerSort.getSortedRows({
|
||||
rows: this._comp._state.rows,
|
||||
sortBy: this._comp._state.sort,
|
||||
sortDir: this._comp._state.dir,
|
||||
});
|
||||
|
||||
// Ensure a row is active
|
||||
this._compRows.doEnsureAtLeastOneRowActive({isSilent: true});
|
||||
|
||||
// region Show/hide creature ordinals
|
||||
const ordinalsToShow = new Set(
|
||||
Object.entries(
|
||||
this._rowStateBuilder.getSimilarRowCounts({rows: this._comp.__state.rows}),
|
||||
)
|
||||
.filter(([, v]) => v > 1)
|
||||
.map(([name]) => name),
|
||||
);
|
||||
this._comp.__state.rows
|
||||
.forEach(({entity}) => entity.isShowOrdinal = ordinalsToShow.has(InitiativeTrackerRowStateBuilderActive.getSimilarRowEntityHash({rowEntity: entity})));
|
||||
// endregion
|
||||
};
|
||||
this._comp._addHookBase(this._prop, hkRowsSync)();
|
||||
rdState.fnsCleanup.push(() => this._comp._removeHookBase(this._prop, hkRowsSync));
|
||||
|
||||
const hkRowsAsync = async () => {
|
||||
try {
|
||||
await this._compRowsLock.pLock();
|
||||
await this._compRows.pRender();
|
||||
|
||||
// region Scroll active rows into view
|
||||
const rendereds = this._comp._getRenderedCollection({prop: this._prop});
|
||||
const renderedsActive = this._comp._state.rows
|
||||
.filter(row => row.entity.isActive)
|
||||
.map(row => rendereds[row.id])
|
||||
.filter(Boolean);
|
||||
|
||||
if (!renderedsActive.length) return;
|
||||
|
||||
// First scroll the last active row into view to scroll down as far as necessary...
|
||||
renderedsActive.last()?.$wrpRow?.[0]?.scrollIntoView({block: "nearest", inline: "nearest"});
|
||||
// ...then scroll the first active row into view, as this is the one we prioritize
|
||||
renderedsActive[0]?.$wrpRow?.[0]?.scrollIntoView({block: "nearest", inline: "nearest"});
|
||||
// endregion
|
||||
} finally {
|
||||
this._compRowsLock.unlock();
|
||||
}
|
||||
};
|
||||
this._comp._addHookBase(this._prop, hkRowsAsync)();
|
||||
rdState.fnsCleanup.push(() => this._comp._removeHookBase(this._prop, hkRowsAsync));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
pDoShiftActiveRow (...args) { return this._compRows.pDoShiftActiveRow(...args); }
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import {
|
||||
InitiativeTrackerStatColumnFactory,
|
||||
} from "./dmscreen-initiativetracker-statcolumns.js";
|
||||
import {InitiativeTrackerUtil} from "../../initiativetracker/initiativetracker-utils.js";
|
||||
|
||||
class _RenderableCollectionRowStatColData extends RenderableCollectionGenericRows {
|
||||
constructor (
|
||||
{
|
||||
rootComp,
|
||||
comp,
|
||||
$wrpRows,
|
||||
networking,
|
||||
mon,
|
||||
},
|
||||
) {
|
||||
super(comp, "rowStatColData", $wrpRows);
|
||||
this._rootComp = rootComp;
|
||||
this._networking = networking;
|
||||
this._mon = mon;
|
||||
}
|
||||
|
||||
_$getWrpRow () {
|
||||
return $(`<div class="ve-flex-vh-center"></div>`);
|
||||
}
|
||||
|
||||
_populateRow ({comp, $wrpRow, entity}) {
|
||||
const statsColData = this._rootComp._state.statsCols.find(statsCol => statsCol.id === entity.id);
|
||||
if (!statsColData) return {};
|
||||
|
||||
const meta = InitiativeTrackerStatColumnFactory.fromStateData({data: statsColData});
|
||||
meta.$getRendered({comp, mon: this._mon, networking: this._networking}).appendTo($wrpRow);
|
||||
|
||||
return {
|
||||
cbOnTurnStart: ({state, direction}) => {
|
||||
meta.onTurnStart({state, direction, mon: this._mon});
|
||||
},
|
||||
cbOnRoundStart: ({state, direction}) => {
|
||||
meta.onRoundStart({state, direction, mon: this._mon});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @abstract */
|
||||
export class RenderableCollectionRowDataBase extends RenderableCollectionAsyncGenericRows {
|
||||
constructor (
|
||||
{
|
||||
comp,
|
||||
prop,
|
||||
$wrpRows,
|
||||
roller,
|
||||
networking = null,
|
||||
rowStateBuilder,
|
||||
},
|
||||
) {
|
||||
super(comp, prop, $wrpRows);
|
||||
this._roller = roller;
|
||||
this._networking = networking;
|
||||
this._rowStateBuilder = rowStateBuilder;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_$getWrpRow () {
|
||||
return $(`<div class="dm-init__row overflow-hidden pr-1"></div>`);
|
||||
}
|
||||
|
||||
async _pPopulateRow ({comp, $wrpRow, entity}) {
|
||||
const fnsCleanup = [];
|
||||
|
||||
const {isMon, mon, fluff} = await this._pPopulateRow_pGetMonsterMeta({comp});
|
||||
|
||||
comp._addHookBase("isActive", () => $wrpRow.toggleClass("dm-init__row-active", !!comp._state.isActive))();
|
||||
|
||||
this._pPopulateRow_bindParentRowStatColsIsEditableHook({comp, entity, mon, fluff, fnsCleanup});
|
||||
|
||||
const $wrpLhs = $(`<div class="dm-init__row-lhs"></div>`).appendTo($wrpRow);
|
||||
|
||||
this._pPopulateRow_player({comp, $wrpLhs, isMon});
|
||||
this._pPopulateRow_monster({comp, $wrpLhs, isMon, mon, fluff});
|
||||
|
||||
this._pPopulateRow_conditions({comp, $wrpLhs});
|
||||
|
||||
this._pPopulateRow_statsCols({comp, $wrpRow, mon, fnsCleanup});
|
||||
|
||||
const $wrpRhs = $(`<div class="dm-init__row-rhs"></div>`).appendTo($wrpRow);
|
||||
|
||||
this._pPopulateRow_hp({comp, $wrpRhs});
|
||||
this._pPopulateRow_initiative({comp, $wrpRhs});
|
||||
this._pPopulateRow_btns({comp, entity, $wrpRhs});
|
||||
|
||||
return {
|
||||
cbOnTurnStart: ({state, direction}) => {
|
||||
Object.values(comp._getRenderedCollection({prop: "rowStatColData"}))
|
||||
.forEach(rendered => {
|
||||
if (!rendered.cbOnTurnStart) return;
|
||||
|
||||
const stateSub = state.entity.rowStatColData
|
||||
.find(cell => cell.id === rendered.id);
|
||||
rendered.cbOnTurnStart({state: stateSub, direction});
|
||||
});
|
||||
},
|
||||
cbOnRoundStart: ({state, direction}) => {
|
||||
Object.values(comp._getRenderedCollection({prop: "rowStatColData"}))
|
||||
.forEach(rendered => {
|
||||
if (!rendered.cbOnRoundStart) return;
|
||||
|
||||
const stateSub = state.entity.rowStatColData
|
||||
.find(cell => cell.id === rendered.id);
|
||||
rendered.cbOnRoundStart({state: stateSub, direction});
|
||||
});
|
||||
},
|
||||
fnsCleanup,
|
||||
};
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {object}
|
||||
*/
|
||||
async _pPopulateRow_pGetMonsterMeta ({comp}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_bindParentRowStatColsIsEditableHook ({comp, entity, mon, fluff, fnsCleanup}) {
|
||||
const hkParentStatCols = () => {
|
||||
const isRowExists = this._comp._state[this._prop]
|
||||
.some(row => row.id === entity.id);
|
||||
if (!isRowExists) return; // Avoid race condition (row removed, but async render has not yet cleaned it up)
|
||||
|
||||
const rowStatColDataNxt = [];
|
||||
|
||||
this._comp._state.statsCols
|
||||
.forEach(data => {
|
||||
const existing = comp._state.rowStatColData.find(cell => cell.id === data.id);
|
||||
if (existing) {
|
||||
// Copy the parent isEditable flag to the child
|
||||
existing.entity.isEditable = data.isEditable;
|
||||
return rowStatColDataNxt.push(existing);
|
||||
}
|
||||
|
||||
const meta = InitiativeTrackerStatColumnFactory.fromStateData({data});
|
||||
const initialState = meta.getInitialCellStateData({mon, fluff});
|
||||
rowStatColDataNxt.push(initialState);
|
||||
});
|
||||
|
||||
comp._state.rowStatColData = rowStatColDataNxt;
|
||||
};
|
||||
this._comp._addHookBase("statsCols", hkParentStatCols)();
|
||||
fnsCleanup.push(() => this._comp._removeHookBase("statsCols", hkParentStatCols));
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_player ({comp, $wrpLhs, isMon}) {
|
||||
if (isMon) return;
|
||||
|
||||
ComponentUiUtil.$getIptStr(
|
||||
comp,
|
||||
"name",
|
||||
{
|
||||
html: `<input class="form-control input-sm name dm-init__ipt-name dm-init-lockable dm-init__row-input" placeholder="Name">`,
|
||||
},
|
||||
).appendTo($wrpLhs);
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return void
|
||||
*/
|
||||
_pPopulateRow_monster ({comp, $wrpLhs, isMon, mon, fluff}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return void
|
||||
*/
|
||||
_pPopulateRow_conditions ({comp, $wrpLhs}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_statsCols ({comp, $wrpRow, mon, fnsCleanup}) {
|
||||
const $wrp = $(`<div class="dm-init__row-mid"></div>`)
|
||||
.appendTo($wrpRow);
|
||||
|
||||
const hkParentStatsAddCols = () => $wrp.toggleVe(!!this._comp._state.isStatsAddColumns);
|
||||
this._comp._addHookBase("isStatsAddColumns", hkParentStatsAddCols)();
|
||||
fnsCleanup.push(() => this._comp._removeHookBase("isStatsAddColumns", hkParentStatsAddCols));
|
||||
|
||||
const renderableCollection = new _RenderableCollectionRowStatColData({
|
||||
rootComp: this._comp,
|
||||
comp,
|
||||
$wrpRows: $wrp,
|
||||
networking: this._networking,
|
||||
mon,
|
||||
});
|
||||
comp._addHookBase("rowStatColData", () => renderableCollection.render())();
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_hp ({comp, $wrpRhs}) {
|
||||
const $iptHpCurrent = ComponentUiUtil.$getIptNumber(
|
||||
comp,
|
||||
"hpCurrent",
|
||||
null,
|
||||
{
|
||||
isAllowNull: true,
|
||||
fallbackOnNaN: null,
|
||||
html: `<input class="form-control input-sm hp dm-init__row-input text-right w-40p mr-0 br-0">`,
|
||||
},
|
||||
)
|
||||
.on("click", () => $iptHpCurrent.select());
|
||||
|
||||
const $iptHpMax = ComponentUiUtil.$getIptNumber(
|
||||
comp,
|
||||
"hpMax",
|
||||
null,
|
||||
{
|
||||
isAllowNull: true,
|
||||
fallbackOnNaN: null,
|
||||
html: `<input class="form-control input-sm hp-max dm-init__row-input w-40p mr-0 bl-0">`,
|
||||
},
|
||||
)
|
||||
.on("click", () => $iptHpMax.select());
|
||||
|
||||
const hkHpColors = () => {
|
||||
const woundLevel = InitiativeTrackerUtil.getWoundLevel(100 * comp._state.hpCurrent / comp._state.hpMax);
|
||||
if (~woundLevel) {
|
||||
const woundMeta = InitiativeTrackerUtil.getWoundMeta(woundLevel);
|
||||
$iptHpCurrent.css("color", woundMeta.color);
|
||||
$iptHpMax.css("color", woundMeta.color);
|
||||
} else {
|
||||
$iptHpCurrent.css("color", "");
|
||||
$iptHpMax.css("color", "");
|
||||
}
|
||||
};
|
||||
comp._addHookBase("hpCurrent", hkHpColors);
|
||||
comp._addHookBase("hpMax", hkHpColors);
|
||||
hkHpColors();
|
||||
|
||||
$$`<div class="ve-flex relative mr-3p">
|
||||
<div class="text-right">${$iptHpCurrent}</div>
|
||||
<div class="dm-init__sep-fields-slash ve-flex-vh-center">/</div>
|
||||
<div class="text-left">${$iptHpMax}</div>
|
||||
</div>`
|
||||
.appendTo($wrpRhs);
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return void
|
||||
*/
|
||||
_pPopulateRow_initiative ({comp, $wrpRhs}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return void
|
||||
*/
|
||||
_pPopulateRow_btns ({comp, entity, $wrpRhs}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
pDoDeleteExistingRender (rendered) {
|
||||
rendered.fnsCleanup.forEach(fn => fn());
|
||||
}
|
||||
}
|
||||
|
||||
/** @abstract */
|
||||
export class InitiativeTrackerRowDataViewBase {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.fnsCleanup = [];
|
||||
}
|
||||
};
|
||||
|
||||
_TextHeaderLhs;
|
||||
_ClsRenderableCollectionRowData;
|
||||
|
||||
constructor ({comp, prop, roller, networking, rowStateBuilder}) {
|
||||
this._comp = comp;
|
||||
this._prop = prop;
|
||||
this._roller = roller;
|
||||
this._networking = networking;
|
||||
this._rowStateBuilder = rowStateBuilder;
|
||||
|
||||
this._compRowsLock = new VeLock({name: "Row render"});
|
||||
this._compRows = null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getRenderedView () {
|
||||
const rdState = new this.constructor._RenderState();
|
||||
|
||||
const $ele = $$`<div class="dm-init__wrp-header-outer">
|
||||
<div class="dm-init__wrp-header pr-1">
|
||||
<div class="dm-init__row-lhs dm-init__header">
|
||||
<div class="w-100">${this._TextHeaderLhs}</div>
|
||||
</div>
|
||||
|
||||
${this._render_$getWrpHeaderStatsCols({rdState})}
|
||||
|
||||
${this._render_$getWrpHeaderRhs({rdState})}
|
||||
</div>
|
||||
|
||||
${this._render_$getWrpRows({rdState})}
|
||||
</div>`;
|
||||
|
||||
return {
|
||||
$ele,
|
||||
cbDoCleanup: () => rdState.fnsCleanup.forEach(fn => fn()),
|
||||
};
|
||||
}
|
||||
|
||||
_render_$getWrpHeaderStatsCols ({rdState}) {
|
||||
const $wrpHeaderStatsCols = $(`<div class="dm-init__row-mid"></div>`);
|
||||
const hkHeaderStatsCols = () => {
|
||||
$wrpHeaderStatsCols.empty();
|
||||
|
||||
if (!this._comp._state.isStatsAddColumns) return;
|
||||
|
||||
this._comp._state.statsCols.forEach(data => {
|
||||
const meta = InitiativeTrackerStatColumnFactory.fromStateData({data});
|
||||
$wrpHeaderStatsCols.append(meta.$getRenderedHeader());
|
||||
});
|
||||
};
|
||||
this._comp._addHookBase("isStatsAddColumns", hkHeaderStatsCols);
|
||||
this._comp._addHookBase("statsCols", hkHeaderStatsCols);
|
||||
hkHeaderStatsCols();
|
||||
rdState.fnsCleanup.push(
|
||||
() => this._comp._removeHookBase("isStatsAddColumns", hkHeaderStatsCols),
|
||||
() => this._comp._removeHookBase("statsCols", hkHeaderStatsCols),
|
||||
);
|
||||
|
||||
return $wrpHeaderStatsCols;
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {jQuery}
|
||||
*/
|
||||
_render_$getWrpHeaderRhs ({rdState}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
|
||||
_render_$getWrpRows ({rdState}) {
|
||||
const $wrpRows = $(`<div class="dm-init__wrp-entries"></div>`);
|
||||
|
||||
this._compRows = new this._ClsRenderableCollectionRowData({
|
||||
comp: this._comp,
|
||||
$wrpRows,
|
||||
roller: this._roller,
|
||||
networking: this._networking,
|
||||
rowStateBuilder: this._rowStateBuilder,
|
||||
});
|
||||
|
||||
this._render_bindHooksRows({rdState});
|
||||
|
||||
return $wrpRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return void
|
||||
*/
|
||||
_render_bindHooksRows ({rdState}) {
|
||||
throw new Error("Unimplemented!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
InitiativeTrackerRowDataViewBase,
|
||||
RenderableCollectionRowDataBase,
|
||||
} from "./dmscreen-initiativetracker-rowsbase.js";
|
||||
|
||||
class _RenderableCollectionRowDataDefaultParty extends RenderableCollectionRowDataBase {
|
||||
constructor (
|
||||
{
|
||||
comp,
|
||||
$wrpRows,
|
||||
roller,
|
||||
rowStateBuilder,
|
||||
},
|
||||
) {
|
||||
super({comp, prop: "rowsDefaultParty", $wrpRows, roller, networking: null, rowStateBuilder});
|
||||
}
|
||||
|
||||
async _pPopulateRow_pGetMonsterMeta ({comp}) {
|
||||
return {
|
||||
isMon: false,
|
||||
mon: null,
|
||||
fluff: null,
|
||||
};
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_monster ({comp, $wrpLhs, isMon, mon, fluff}) {
|
||||
/* No-op */
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_conditions ({comp, $wrpLhs}) {
|
||||
/* No-op */
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_initiative ({comp, $wrpRhs}) {
|
||||
/* No-op */
|
||||
}
|
||||
|
||||
/* ----- */
|
||||
|
||||
_pPopulateRow_btns ({comp, entity, $wrpRhs}) {
|
||||
$(`<button class="btn btn-danger btn-xs dm-init__row-btn dm-init-lockable" tabindex="-1"><span class="glyphicon glyphicon-trash"></span></button>`)
|
||||
.appendTo($wrpRhs)
|
||||
.on("click", () => {
|
||||
if (this._comp._state.isLocked) return;
|
||||
this._utils.doDelete({entity});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerRowDataViewDefaultParty extends InitiativeTrackerRowDataViewBase {
|
||||
_TextHeaderLhs = "Player";
|
||||
_ClsRenderableCollectionRowData = _RenderableCollectionRowDataDefaultParty;
|
||||
|
||||
_render_$getWrpHeaderRhs ({rdState}) {
|
||||
return $$`<div class="dm-init__row-rhs">
|
||||
<div class="dm-init__header dm-init__header--input dm-init__header--input-wide" title="Hit Points">HP</div>
|
||||
<div class="dm-init__spc-header-buttons--single"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_render_bindHooksRows ({rdState}) {
|
||||
const hkRowsAsync = async () => {
|
||||
try {
|
||||
await this._compRowsLock.pLock();
|
||||
await this._compRows.pRender();
|
||||
} finally {
|
||||
this._compRowsLock.unlock();
|
||||
}
|
||||
};
|
||||
this._comp._addHookBase(this._prop, hkRowsAsync)();
|
||||
rdState.fnsCleanup.push(
|
||||
() => this._comp._removeHookBase(this._prop, hkRowsAsync),
|
||||
() => this._comp._detachCollection(this._prop),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
INITIATIVE_APPLICABILITY_NOT_APPLICABLE,
|
||||
InitiativeTrackerStatColumnFactory,
|
||||
} from "./dmscreen-initiativetracker-statcolumns.js";
|
||||
import {DmScreenUtil} from "../dmscreen-util.js";
|
||||
import {InitiativeTrackerSort} from "./dmscreen-initiativetracker-sort.js";
|
||||
|
||||
/** @abstract */
|
||||
class _InitiativeTrackerRowStateBuilderBase {
|
||||
constructor ({comp, roller}) {
|
||||
this._comp = comp;
|
||||
this._roller = roller;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @param {?array} rows Existing/partial rows, for calculating ordinal.
|
||||
* @param {?boolean} isActive
|
||||
* @param {?boolean} isPlayerVisible
|
||||
* @param {?string} name
|
||||
* @param {?string} displayName
|
||||
* @param {?number} scaledCr
|
||||
* @param {?number} scaledSummonSpellLevel
|
||||
* @param {?number} scaledSummonClassLevel
|
||||
* @param {?string} customName
|
||||
* @param {?string} source
|
||||
* @param {?number} hpCurrent
|
||||
* @param {?number} hpMax
|
||||
* @param {?number} initiative
|
||||
* @param {?number} ordinal
|
||||
* @param {?array} rowStatColData
|
||||
* @param {?array} conditions
|
||||
*/
|
||||
async pGetNewRowState (
|
||||
{
|
||||
rows = null,
|
||||
|
||||
isActive = null,
|
||||
isPlayerVisible = null,
|
||||
name = null,
|
||||
displayName = null,
|
||||
scaledCr = null,
|
||||
scaledSummonSpellLevel = null,
|
||||
scaledSummonClassLevel = null,
|
||||
customName = null,
|
||||
source = null,
|
||||
hpCurrent = null,
|
||||
hpMax = null,
|
||||
initiative = null,
|
||||
ordinal = null,
|
||||
rowStatColData = null,
|
||||
conditions = null,
|
||||
} = {},
|
||||
) {
|
||||
if (rowStatColData == null) rowStatColData = this._getInitialRowStatColData();
|
||||
|
||||
return {
|
||||
id: CryptUtil.uid(),
|
||||
entity: {
|
||||
isActive: !!isActive,
|
||||
isPlayerVisible: !!isPlayerVisible,
|
||||
name,
|
||||
displayName,
|
||||
scaledCr,
|
||||
scaledSummonSpellLevel,
|
||||
scaledSummonClassLevel,
|
||||
customName,
|
||||
source,
|
||||
hpCurrent,
|
||||
hpMax,
|
||||
initiative,
|
||||
ordinal,
|
||||
rowStatColData: rowStatColData ?? [],
|
||||
conditions: conditions ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?object} mon
|
||||
* @param {?object} fluff
|
||||
*/
|
||||
_getInitialRowStatColData ({mon = null, fluff = null} = {}) {
|
||||
return this._comp._state.statsCols
|
||||
.map(data => {
|
||||
return InitiativeTrackerStatColumnFactory.fromStateData({data})
|
||||
.getInitialCellStateData({mon, fluff});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pGetRowInitiativeMeta ({row}) {
|
||||
const out = {mon: null, initiativeModifier: null};
|
||||
|
||||
const {entity} = row;
|
||||
|
||||
const initiativeInfos = this._comp._state.statsCols
|
||||
.map(data => {
|
||||
const cell = entity.rowStatColData.find(cell => cell.id === data.id);
|
||||
if (!cell) return null;
|
||||
return {
|
||||
id: cell.id,
|
||||
...InitiativeTrackerStatColumnFactory.fromStateData({data})
|
||||
.getInitiativeInfo({state: cell}),
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter(info => info.applicability !== INITIATIVE_APPLICABILITY_NOT_APPLICABLE)
|
||||
.sort(InitiativeTrackerSort.sortInitiativeInfos.bind(InitiativeTrackerSort));
|
||||
|
||||
const maxApplicability = Math.max(INITIATIVE_APPLICABILITY_NOT_APPLICABLE, ...initiativeInfos.map(info => info.applicability));
|
||||
if (maxApplicability !== INITIATIVE_APPLICABILITY_NOT_APPLICABLE) {
|
||||
const initiativeInfosApplicable = initiativeInfos.filter(info => info.applicability === maxApplicability);
|
||||
out.initiativeModifier = Math.max(...initiativeInfosApplicable.map(info => info.initiative));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerRowStateBuilderActive extends _InitiativeTrackerRowStateBuilderBase {
|
||||
_prop = "rows";
|
||||
|
||||
async pGetScaledCreature ({isMon, name, source, scaledCr, scaledSummonSpellLevel, scaledSummonClassLevel}) {
|
||||
if (!isMon) return null;
|
||||
return DmScreenUtil.pGetScaledCreature({name, source, scaledCr, scaledSummonSpellLevel, scaledSummonClassLevel});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async pGetNewRowState (
|
||||
{
|
||||
rows = null,
|
||||
|
||||
isActive = null,
|
||||
isPlayerVisible = null,
|
||||
name = null,
|
||||
displayName = null,
|
||||
scaledCr = null,
|
||||
scaledSummonSpellLevel = null,
|
||||
scaledSummonClassLevel = null,
|
||||
customName = null,
|
||||
source = null,
|
||||
hpCurrent = null,
|
||||
hpMax = null,
|
||||
initiative = null,
|
||||
ordinal = null,
|
||||
rowStatColData = null,
|
||||
conditions = null,
|
||||
} = {},
|
||||
) {
|
||||
const isMon = name && source;
|
||||
const mon = await this.pGetScaledCreature({isMon, name, source, scaledCr, scaledSummonSpellLevel, scaledSummonClassLevel});
|
||||
if (isMon && !mon) return null;
|
||||
|
||||
const fluff = mon ? await Renderer.monster.pGetFluff(mon) : null;
|
||||
|
||||
if (isMon) {
|
||||
if (hpCurrent == null && hpMax == null) {
|
||||
hpCurrent = hpMax = await this._roller.pGetOrRollHp(mon, {isRollHp: this._comp._state.isRollHp});
|
||||
}
|
||||
|
||||
if (initiative == null && this._comp._state.isRollInit) {
|
||||
initiative = await this._roller.pGetRollInitiative({mon});
|
||||
}
|
||||
|
||||
if (ordinal == null) {
|
||||
const existingCreatures = this.getSimilarRows({
|
||||
rows,
|
||||
rowEntity: {
|
||||
name,
|
||||
customName,
|
||||
scaledCr,
|
||||
scaledSummonSpellLevel,
|
||||
scaledSummonClassLevel,
|
||||
source,
|
||||
},
|
||||
});
|
||||
|
||||
ordinal = existingCreatures.length + 1;
|
||||
}
|
||||
|
||||
if (isPlayerVisible == null) isPlayerVisible = !this._comp._state.playerInitHideNewMonster;
|
||||
}
|
||||
|
||||
if (!isMon) {
|
||||
if (isPlayerVisible == null) isPlayerVisible = true;
|
||||
}
|
||||
|
||||
if (rowStatColData == null) rowStatColData = this._getInitialRowStatColData({mon, fluff});
|
||||
|
||||
return {
|
||||
id: CryptUtil.uid(),
|
||||
entity: {
|
||||
isActive: !!isActive,
|
||||
isPlayerVisible: !!isPlayerVisible,
|
||||
name,
|
||||
displayName,
|
||||
scaledCr,
|
||||
scaledSummonSpellLevel,
|
||||
scaledSummonClassLevel,
|
||||
customName,
|
||||
source,
|
||||
hpCurrent,
|
||||
hpMax,
|
||||
initiative,
|
||||
ordinal,
|
||||
rowStatColData: rowStatColData ?? [],
|
||||
conditions: conditions ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pGetRowInitiativeMeta ({row}) {
|
||||
const out = await super.pGetRowInitiativeMeta({row});
|
||||
|
||||
const mon = await this.pGetScaledCreature(row.entity);
|
||||
if (!mon) return out;
|
||||
|
||||
out.mon = mon;
|
||||
out.initiativeModifier ||= Parser.getAbilityModifier(mon.dex);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static _SIMILAR_ROW_PROPS = [
|
||||
"name",
|
||||
"customName",
|
||||
"scaledCr",
|
||||
"scaledSummonSpellLevel",
|
||||
"scaledSummonClassLevel",
|
||||
"source",
|
||||
];
|
||||
|
||||
static getSimilarRowEntityHash ({rowEntity}) {
|
||||
return this._SIMILAR_ROW_PROPS
|
||||
.map(prop => JSON.stringify(rowEntity[prop] ?? null))
|
||||
.join("__");
|
||||
}
|
||||
|
||||
getSimilarRows (
|
||||
{
|
||||
rows = null,
|
||||
|
||||
rowEntity,
|
||||
},
|
||||
) {
|
||||
const rowEntityHash = this.constructor.getSimilarRowEntityHash({rowEntity});
|
||||
|
||||
return (rows || this._comp._state[this._prop])
|
||||
.filter(({entity}) => {
|
||||
return this.constructor.getSimilarRowEntityHash({rowEntity: entity}) === rowEntityHash;
|
||||
});
|
||||
}
|
||||
|
||||
getSimilarRowCounts (
|
||||
{
|
||||
rows = null,
|
||||
},
|
||||
) {
|
||||
return (rows || this._comp._state[this._prop])
|
||||
.reduce(
|
||||
(accum, {entity}) => {
|
||||
const rowEntityHash = InitiativeTrackerRowStateBuilderActive.getSimilarRowEntityHash({rowEntity: entity});
|
||||
accum[rowEntityHash] = (accum[rowEntityHash] || 0) + 1;
|
||||
return accum;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerRowStateBuilderDefaultParty extends _InitiativeTrackerRowStateBuilderBase {
|
||||
_prop = "rowsDefaultParty";
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import {InitiativeTrackerDataSerializerBase} from "./dmscreen-initiativetracker-util.js";
|
||||
|
||||
export class InitiativeTrackerStatColumnDataSerializer extends InitiativeTrackerDataSerializerBase {
|
||||
static _FIELD_MAPPINGS = {
|
||||
"id": "id",
|
||||
"isEditable": "e",
|
||||
"isPlayerVisible": "v",
|
||||
"populateWith": "p",
|
||||
"abbreviation": "a",
|
||||
};
|
||||
}
|
||||
|
||||
export class InitiativeTrackerConditionCustomSerializer extends InitiativeTrackerDataSerializerBase {
|
||||
static _FIELD_MAPPINGS = {
|
||||
"id": "id",
|
||||
"entity.name": "n",
|
||||
"entity.color": "c",
|
||||
"entity.turns": "t",
|
||||
};
|
||||
}
|
||||
|
||||
export class InitiativeTrackerRowStatsColDataSerializer extends InitiativeTrackerDataSerializerBase {
|
||||
static _FIELD_MAPPINGS = {
|
||||
"id": "id",
|
||||
"entity.value": "v",
|
||||
"entity.current": "cur",
|
||||
"entity.max": "max",
|
||||
};
|
||||
}
|
||||
|
||||
export class InitiativeTrackerRowDataSerializer extends InitiativeTrackerDataSerializerBase {
|
||||
static _FIELD_MAPPINGS = {
|
||||
"id": "id",
|
||||
|
||||
// region Flattened `"nameMeta"`
|
||||
"entity.name": "n",
|
||||
|
||||
"entity.displayName": "n_d",
|
||||
"entity.scaledCr": "n_scr",
|
||||
"entity.scaledSummonSpellLevel": "n_ssp",
|
||||
"entity.scaledSummonClassLevel": "n_scl",
|
||||
|
||||
// region Used by player tracker
|
||||
"entity.customName": "n_m",
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
"entity.hpCurrent": "h",
|
||||
"entity.hpMax": "g",
|
||||
"entity.initiative": "i",
|
||||
"entity.isActive": "a",
|
||||
"entity.source": "s",
|
||||
"entity.conditions": "c",
|
||||
"entity.isPlayerVisible": "v",
|
||||
|
||||
// region Used by player tracker
|
||||
"entity.hpWoundLevel": "hh",
|
||||
"entity.ordinal": "o",
|
||||
// endregion
|
||||
|
||||
// region Specific handling
|
||||
// "entity.rowStatColData": "k",
|
||||
// endregion
|
||||
};
|
||||
|
||||
static fromSerial (dataSerial) {
|
||||
// Handle legacy data format
|
||||
if (dataSerial.n instanceof Object) {
|
||||
dataSerial.n_d = dataSerial.n.d || null;
|
||||
dataSerial.n_scr = dataSerial.n.scr || null;
|
||||
dataSerial.n_ssp = dataSerial.n.ssp || null;
|
||||
dataSerial.n_scl = dataSerial.n.scl || null;
|
||||
dataSerial.n_m = dataSerial.n.m || null;
|
||||
|
||||
dataSerial.n = dataSerial.n.n;
|
||||
}
|
||||
|
||||
const out = super.fromSerial(dataSerial);
|
||||
|
||||
// Convert legacy data
|
||||
out.id = out.id || CryptUtil.uid();
|
||||
|
||||
out.entity.rowStatColData = (dataSerial.k || [])
|
||||
.map(rowStatColData => {
|
||||
const out = InitiativeTrackerRowStatsColDataSerializer.fromSerial(rowStatColData);
|
||||
// If the cell had no data, the `entity` prop may have been serialized away. Ensure it exists.
|
||||
if (!out.entity) out.entity = {};
|
||||
return out;
|
||||
});
|
||||
|
||||
// Convert legacy data
|
||||
if (out.entity.conditions?.length) {
|
||||
out.entity.conditions = out.entity.conditions
|
||||
.map(cond => {
|
||||
if (cond.id) return cond;
|
||||
return {
|
||||
id: CryptUtil.uid(),
|
||||
entity: {
|
||||
...cond,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Convert legacy data
|
||||
if (out.entity.ordinal == null) out.entity.ordinal = 1;
|
||||
|
||||
// Convert legacy numbers
|
||||
[
|
||||
"scaledCr",
|
||||
"scaledSummonSpellLevel",
|
||||
"scaledSummonClassLevel",
|
||||
"hpCurrent",
|
||||
"hpMax",
|
||||
"initiative",
|
||||
"ordinal",
|
||||
]
|
||||
.forEach(prop => {
|
||||
if (out.entity?.[prop] == null) return;
|
||||
if (isNaN(out.entity?.[prop])) return delete out.entity[prop];
|
||||
out.entity[prop] = Number(out.entity[prop]);
|
||||
});
|
||||
|
||||
// Convert legacy booleans
|
||||
[
|
||||
"isActive",
|
||||
]
|
||||
.forEach(prop => {
|
||||
if (out.entity?.[prop] == null) return;
|
||||
out.entity[prop] = !!out.entity[prop];
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static toSerial (data) {
|
||||
const out = super.toSerial(data);
|
||||
|
||||
out.k = (data.entity.rowStatColData || [])
|
||||
.map(rowStatColData => InitiativeTrackerRowStatsColDataSerializer.toSerial(rowStatColData));
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import {InitiativeTrackerUi} from "./dmscreen-initiativetracker-ui.js";
|
||||
import {
|
||||
GROUP_DISPLAY_NAMES,
|
||||
InitiativeTrackerStatColumnFactory,
|
||||
IS_PLAYER_VISIBLE_ALL,
|
||||
IS_PLAYER_VISIBLE_NONE,
|
||||
IS_PLAYER_VISIBLE_PLAYER_UNITS_ONLY,
|
||||
} from "./dmscreen-initiativetracker-statcolumns.js";
|
||||
|
||||
class _RenderableCollectionStatsCols extends RenderableCollectionGenericRows {
|
||||
constructor (
|
||||
{
|
||||
comp,
|
||||
|
||||
doClose,
|
||||
$wrpRows,
|
||||
},
|
||||
) {
|
||||
super(comp, "statsCols", $wrpRows);
|
||||
this._doClose = doClose;
|
||||
}
|
||||
|
||||
_populateRow ({comp, $wrpRow, entity}) {
|
||||
$wrpRow.addClass("py-1p");
|
||||
|
||||
const meta = InitiativeTrackerStatColumnFactory.fromPopulateWith({populateWith: comp._state.populateWith});
|
||||
|
||||
const $iptAbv = ComponentUiUtil.$getIptStr(comp, "abbreviation");
|
||||
|
||||
const $cbIsEditable = ComponentUiUtil.$getCbBool(comp, "isEditable");
|
||||
|
||||
const $btnVisible = InitiativeTrackerUi.$getBtnPlayerVisible(
|
||||
comp._state.isPlayerVisible,
|
||||
() => comp._state.isPlayerVisible = $btnVisible.hasClass("btn-primary--half")
|
||||
? IS_PLAYER_VISIBLE_PLAYER_UNITS_ONLY
|
||||
: $btnVisible.hasClass("btn-primary")
|
||||
? IS_PLAYER_VISIBLE_ALL
|
||||
: IS_PLAYER_VISIBLE_NONE,
|
||||
true,
|
||||
);
|
||||
|
||||
const $btnDelete = this._utils.$getBtnDelete({entity});
|
||||
|
||||
const $padDrag = this._utils.$getPadDrag({$wrpRow});
|
||||
|
||||
$$($wrpRow)`
|
||||
<div class="col-5 pr-1">${meta.constructor.NAME}</div>
|
||||
<div class="col-3 pr-1">${$iptAbv}</div>
|
||||
<div class="col-1-5 ve-text-center">${$cbIsEditable}</div>
|
||||
<div class="col-1-5 ve-text-center">${$btnVisible}</div>
|
||||
|
||||
<div class="col-0-5 ve-flex-vh-center">${$btnDelete}</div>
|
||||
<div class="col-0-5 ve-flex-vh-center">${$padDrag}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class InitiativeTrackerSettings extends BaseComponent {
|
||||
static _PROPS_TRACKED = [
|
||||
"isRollInit",
|
||||
"isRollHp",
|
||||
"isRollGroups",
|
||||
"isRerollInitiativeEachRound",
|
||||
"playerInitShowExactPlayerHp",
|
||||
"playerInitShowExactMonsterHp",
|
||||
"playerInitHideNewMonster",
|
||||
"playerInitShowOrdinals",
|
||||
"isStatsAddColumns",
|
||||
"statsCols",
|
||||
];
|
||||
|
||||
constructor ({state}) {
|
||||
super();
|
||||
|
||||
this._proxyAssignSimple(
|
||||
"state",
|
||||
{
|
||||
...InitiativeTrackerSettings._PROPS_TRACKED
|
||||
.mergeMap(prop => ({[prop]: state[prop]})),
|
||||
statsCols: this._getStatColsCollectionFormat(state.statsCols),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Convert from classic "flat" format to renderable collection format
|
||||
_getStatColsCollectionFormat (statsCols) {
|
||||
return (statsCols || [])
|
||||
.map(data => {
|
||||
return InitiativeTrackerStatColumnFactory.fromStateData({data})
|
||||
.getAsCollectionRowStateData();
|
||||
});
|
||||
}
|
||||
|
||||
// Convert from renderable collection format to classic "flat" format
|
||||
_getStatColsDataFormat (statsCols) {
|
||||
return (statsCols || [])
|
||||
.map(data => {
|
||||
return InitiativeTrackerStatColumnFactory.fromCollectionRowStateData({data})
|
||||
.getAsStateData();
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getStateUpdate () {
|
||||
const out = MiscUtil.copyFast(this._state);
|
||||
out.statsCols = this._getStatColsDataFormat(out.statsCols);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
pGetShowModalResults () {
|
||||
const {$modalInner, $modalFooter, pGetResolved, doClose} = UiUtil.getShowModal({
|
||||
title: "Settings",
|
||||
isUncappedHeight: true,
|
||||
hasFooter: true,
|
||||
});
|
||||
|
||||
UiUtil.addModalSep($modalInner);
|
||||
this._pGetShowModalResults_renderSection_isRolls({$modalInner});
|
||||
UiUtil.addModalSep($modalInner);
|
||||
this._pGetShowModalResults_renderSection_playerView({$modalInner});
|
||||
UiUtil.addModalSep($modalInner);
|
||||
this._pGetShowModalResults_renderSection_additionalCols({$modalInner});
|
||||
|
||||
this._pGetShowModalResults_renderFooter({$modalFooter, doClose});
|
||||
|
||||
return pGetResolved();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_pGetShowModalResults_renderSection_isRolls ({$modalInner}) {
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isRollInit", text: "Roll initiative"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isRollHp", text: "Roll hit points"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isRollGroups", text: "Roll groups of creatures together"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isRerollInitiativeEachRound", text: "Reroll initiative each round"});
|
||||
}
|
||||
|
||||
_pGetShowModalResults_renderSection_playerView ({$modalInner}) {
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "playerInitShowExactPlayerHp", text: "Player View: Show exact player HP"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "playerInitShowExactMonsterHp", text: "Player View: Show exact monster HP"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "playerInitHideNewMonster", text: "Player View: Auto-hide new monsters"});
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "playerInitShowOrdinals", text: "Player View: Show ordinals", title: "For example, if you add two Goblins, one will be Goblin (1) and the other Goblin (2), rather than having identical names."});
|
||||
}
|
||||
|
||||
_pGetShowModalResults_renderSection_additionalCols ({$modalInner}) {
|
||||
UiUtil.$getAddModalRowCb2({$wrp: $modalInner, comp: this, prop: "isStatsAddColumns", text: "Additional Columns"});
|
||||
this._pGetShowModalResults_renderSection_additionalCols_head({$modalInner});
|
||||
this._pGetShowModalResults_renderSection_additionalCols_body({$modalInner});
|
||||
}
|
||||
|
||||
_pGetShowModalResults_renderSection_additionalCols_head ({$modalInner}) {
|
||||
const getAction = Cls => new ContextUtil.Action(
|
||||
Cls.NAME,
|
||||
() => {
|
||||
this._state.statsCols = [...this._state.statsCols, new Cls().getAsCollectionRowStateData()];
|
||||
},
|
||||
);
|
||||
|
||||
const menuAddStatsCol = ContextUtil.getMenu(
|
||||
InitiativeTrackerStatColumnFactory.getGroupedByUi()
|
||||
.map(group => {
|
||||
const [ClsHead] = group;
|
||||
|
||||
if (group.length === 1) return getAction(ClsHead);
|
||||
|
||||
return new ContextUtil.ActionSubMenu(
|
||||
GROUP_DISPLAY_NAMES[ClsHead.GROUP],
|
||||
group.map(Cls => getAction(Cls)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const $btnAddRow = $(`<button class="btn btn-default btn-xs bb-0 bbr-0 bbl-0" title="Add"><span class="glyphicon glyphicon-plus"></span></button>`)
|
||||
.click(evt => ContextUtil.pOpenMenu(evt, menuAddStatsCol));
|
||||
|
||||
const $wrpTblStatsHead = $$`<div class="ve-flex-vh-center w-100 mb-2 bb-1p-trans">
|
||||
<div class="col-5">Contains</div>
|
||||
<div class="col-3">Abbreviation</div>
|
||||
<div class="col-1-5 ve-text-center help" title="Only affects creatures. Players are always editable.">Editable</div>
|
||||
<div class="col-1-5"> </div>
|
||||
<div class="col-1 ve-flex-v-center ve-flex-h-right">${$btnAddRow}</div>
|
||||
</div>`
|
||||
.appendTo($modalInner);
|
||||
|
||||
this._addHookBase("isStatsAddColumns", () => $wrpTblStatsHead.toggleVe(this._state.isStatsAddColumns))();
|
||||
}
|
||||
|
||||
_pGetShowModalResults_renderSection_additionalCols_body ({$modalInner}) {
|
||||
const $wrpRows = $(`<div class="pr-1 h-120p ve-flex-col overflow-y-auto relative"></div>`).appendTo($modalInner);
|
||||
this._addHookBase("isStatsAddColumns", () => $wrpRows.toggleVe(this._state.isStatsAddColumns))();
|
||||
|
||||
const renderableCollectionStatsCols = new _RenderableCollectionStatsCols(
|
||||
{
|
||||
comp: this,
|
||||
$wrpRows,
|
||||
},
|
||||
);
|
||||
|
||||
this._addHookBase("statsCols", () => {
|
||||
renderableCollectionStatsCols.render();
|
||||
})();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_pGetShowModalResults_renderFooter ({$modalFooter, doClose}) {
|
||||
const $btnSave = $(`<button class="btn btn-primary btn-sm w-100">Save</button>`)
|
||||
.click(() => doClose(true));
|
||||
|
||||
$$($modalFooter)`<div class="w-100 py-3 no-shrink">
|
||||
${$btnSave}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {InitiativeTrackerConst} from "./dmscreen-initiativetracker-consts.js";
|
||||
|
||||
export class InitiativeTrackerSort {
|
||||
static _getSortMultiplier ({sortDir}) {
|
||||
return sortDir === InitiativeTrackerConst.SORT_DIR_DESC ? -1 : 1;
|
||||
}
|
||||
|
||||
static _sortRowsNameBasic ({sortDir}, rowA, rowB) {
|
||||
return this._getSortMultiplier({sortDir}) * SortUtil.ascSortLower(
|
||||
rowA.entity.customName || rowA.entity.name || "",
|
||||
rowB.entity.customName || rowB.entity.name || "",
|
||||
);
|
||||
}
|
||||
|
||||
static _sortRowsInitiativeBasic ({sortDir}, rowA, rowB) {
|
||||
return this._getSortMultiplier({sortDir}) * SortUtil.ascSort(
|
||||
rowA.entity.initiative || 0,
|
||||
rowB.entity.initiative || 0,
|
||||
);
|
||||
}
|
||||
|
||||
static _sortRowsOrdinal (rowA, rowB) {
|
||||
// (Ordinals are always sorted ascending)
|
||||
return SortUtil.ascSort(rowA.entity.ordinal || 0, rowB.entity.ordinal || 0);
|
||||
}
|
||||
|
||||
static _sortRowsId (rowA, rowB) {
|
||||
// (IDs are always sorted ascending)
|
||||
return SortUtil.ascSort(rowA.id, rowB.id);
|
||||
}
|
||||
|
||||
static _sortRowsName ({sortDir}, rowA, rowB) {
|
||||
return this._sortRowsNameBasic({sortDir}, rowA, rowB)
|
||||
|| this._sortRowsOrdinal(rowA, rowB)
|
||||
|| this._sortRowsInitiativeBasic({sortDir}, rowA, rowB)
|
||||
|| this._sortRowsId(rowA, rowB); // Fallback on ID to guarantee stable sort
|
||||
}
|
||||
|
||||
static _sortRowsInitiative ({sortDir}, rowA, rowB) {
|
||||
return this._sortRowsInitiativeBasic({sortDir}, rowA, rowB)
|
||||
|| this._sortRowsNameBasic({sortDir}, rowA, rowB)
|
||||
|| this._sortRowsOrdinal(rowA, rowB)
|
||||
|| this._sortRowsId(rowA, rowB); // Fallback on ID to guarantee stable sort
|
||||
}
|
||||
|
||||
static getSortedRows ({rows, sortBy, sortDir}) {
|
||||
const fnSort = sortBy === InitiativeTrackerConst.SORT_ORDER_ALPHA
|
||||
? this._sortRowsName.bind(this, {sortDir})
|
||||
: this._sortRowsInitiative.bind(this, {sortDir});
|
||||
|
||||
return [...rows].sort(fnSort);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static sortInitiativeInfos (a, b) {
|
||||
return SortUtil.ascSort(b.applicability, a.applicability)
|
||||
|| SortUtil.ascSort(b.initiative, a.initiative)
|
||||
|| SortUtil.ascSort(a.id, b.id); // Fallback on ID to guarantee stable sort
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
import {InitiativeTrackerConst} from "./dmscreen-initiativetracker-consts.js";
|
||||
import {
|
||||
InitiativeTrackerRowStatsColDataSerializer,
|
||||
InitiativeTrackerStatColumnDataSerializer,
|
||||
} from "./dmscreen-initiativetracker-serial.js";
|
||||
import {InitiativeTrackerUtil} from "../../initiativetracker/initiativetracker-utils.js";
|
||||
|
||||
export const GROUP_BASE_STATS = "baseStats";
|
||||
export const GROUP_SAVES = "saves";
|
||||
export const GROUP_ABILITY_BONUS = "abilityBonus";
|
||||
export const GROUP_ABILITY_SCORE = "abilityScore";
|
||||
export const GROUP_SKILL = "skill";
|
||||
export const GROUP_CHECKBOX = "checkbox";
|
||||
export const GROUP_CUSTOM = "custom";
|
||||
|
||||
export const GROUP_DISPLAY_NAMES = {
|
||||
[GROUP_BASE_STATS]: "General",
|
||||
[GROUP_SAVES]: "Saving Throw",
|
||||
[GROUP_ABILITY_BONUS]: "Ability Bonus",
|
||||
[GROUP_ABILITY_SCORE]: "Ability Score",
|
||||
[GROUP_SKILL]: "Skill",
|
||||
[GROUP_CHECKBOX]: "Checkbox",
|
||||
[GROUP_CUSTOM]: "Custom",
|
||||
};
|
||||
|
||||
export const IS_PLAYER_VISIBLE_NONE = 0;
|
||||
export const IS_PLAYER_VISIBLE_ALL = 1;
|
||||
export const IS_PLAYER_VISIBLE_PLAYER_UNITS_ONLY = 2;
|
||||
|
||||
export const INITIATIVE_APPLICABILITY_NOT_APPLICABLE = 0;
|
||||
const _INITIATIVE_APPLICABILITY_APPLICABLE = 1;
|
||||
const _INITIATIVE_APPLICABILITY_EXACT = 2;
|
||||
|
||||
/** @abstract */
|
||||
class _InitiativeTrackerStatColumnBase {
|
||||
static _SERIALIZER_MAPPINGS = {
|
||||
"entity.value": "v",
|
||||
};
|
||||
|
||||
static _registerSerializerMappings () {
|
||||
Object.entries(this._SERIALIZER_MAPPINGS)
|
||||
.forEach(([kFull, kSerial]) => InitiativeTrackerRowStatsColDataSerializer.registerMapping({kFull, kSerial, isAllowDuplicates: true}));
|
||||
return null;
|
||||
}
|
||||
|
||||
static _ = this._registerSerializerMappings();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** Functions as an ID for the column type. */
|
||||
static get POPULATE_WITH () { throw new Error("Unimplemented!"); }
|
||||
|
||||
/** UI group the column type belongs to. */
|
||||
static GROUP;
|
||||
|
||||
static NAME;
|
||||
static ABV_DEFAULT = "";
|
||||
|
||||
constructor (
|
||||
{
|
||||
id,
|
||||
isEditable,
|
||||
isPlayerVisible,
|
||||
abbreviation,
|
||||
} = {},
|
||||
) {
|
||||
this._id = id ?? CryptUtil.uid();
|
||||
this._isEditable = isEditable ?? true;
|
||||
this._isPlayerVisible = isPlayerVisible ?? false;
|
||||
this._abbreviation = abbreviation ?? this.constructor.ABV_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?object} mon
|
||||
* @param {?object} fluff
|
||||
* @return {?*}
|
||||
* @abstract
|
||||
*/
|
||||
_getInitialCellObj ({mon, fluff}) { throw new Error("Unimplemented!"); }
|
||||
|
||||
_getAsData () {
|
||||
return {
|
||||
id: this._id,
|
||||
isEditable: this._isEditable,
|
||||
isPlayerVisible: this._isPlayerVisible,
|
||||
populateWith: this.constructor.POPULATE_WITH,
|
||||
abbreviation: this._abbreviation,
|
||||
};
|
||||
}
|
||||
|
||||
getAsStateData () {
|
||||
return this._getAsData();
|
||||
}
|
||||
|
||||
getAsCollectionRowStateData () {
|
||||
const data = this._getAsData();
|
||||
const out = {
|
||||
id: data.id,
|
||||
entity: {
|
||||
...data,
|
||||
},
|
||||
};
|
||||
delete out.entity.id;
|
||||
return out;
|
||||
}
|
||||
|
||||
getPlayerFriendlyState ({cell}) {
|
||||
return {id: cell.id, entity: cell.entity};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @return {?object} `undefined` if the column should not auto-set at the start of the turn, or, a value the column should
|
||||
* be auto-set to at the start of the turn.
|
||||
*/
|
||||
_getAutoTurnStartObject ({state, mon}) { return undefined; }
|
||||
|
||||
/**
|
||||
* @return {?object} `undefined` if the column should not auto-set at the start of the round, or, a value the column should
|
||||
* be auto-set to at the start of the round.
|
||||
*/
|
||||
_getAutoRoundStartObject ({state, mon}) { return undefined; }
|
||||
|
||||
_isNonNegativeDirection ({direction}) {
|
||||
return ![InitiativeTrackerConst.DIR_FORWARDS, InitiativeTrackerConst.DIR_NEUTRAL].includes(direction);
|
||||
}
|
||||
|
||||
onTurnStart ({state, direction, mon}) {
|
||||
if (this._isNonNegativeDirection({direction})) return;
|
||||
|
||||
const obj = this._getAutoTurnStartObject({state, mon});
|
||||
if (obj !== undefined) Object.assign(state.entity, obj);
|
||||
}
|
||||
|
||||
onRoundStart ({state, direction, mon}) {
|
||||
if (this._isNonNegativeDirection({direction})) return;
|
||||
|
||||
const obj = this._getAutoRoundStartObject({state, mon});
|
||||
if (obj !== undefined) Object.assign(state.entity, obj);
|
||||
}
|
||||
|
||||
$getRenderedHeader () {
|
||||
return $(`<div class="dm-init__stat_head" ${this.constructor.NAME ? `title="${this.constructor.NAME}"` : ""}>${this._abbreviation}</div>`);
|
||||
}
|
||||
|
||||
$getRendered ({comp, mon, networking = null}) {
|
||||
const $ipt = ComponentUiUtil.$getIptStr(comp, "value")
|
||||
.removeClass("input-xs")
|
||||
.addClass("input-sm")
|
||||
.addClass("dm-init__stat_ipt")
|
||||
.addClass("ve-text-center")
|
||||
.on("click", () => $ipt.select());
|
||||
|
||||
if (mon) {
|
||||
comp._addHookBase("isEditable", () => {
|
||||
$ipt.prop("disabled", !comp._state.isEditable);
|
||||
})();
|
||||
}
|
||||
|
||||
return $$`<div class="ve-flex-vh-center">${$ipt}</div>`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @param {?object} mon
|
||||
* @param {?object} fluff
|
||||
* @param {?object} obj
|
||||
*/
|
||||
getInitialCellStateData ({mon = null, fluff = null, obj = null} = {}) {
|
||||
return {
|
||||
id: this._id,
|
||||
entity: {
|
||||
...(obj ?? (this._getInitialCellObj({mon, fluff}) || {})),
|
||||
isEditable: this._isEditable,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getInitiativeInfo ({state}) {
|
||||
return {
|
||||
applicability: INITIATIVE_APPLICABILITY_NOT_APPLICABLE,
|
||||
initiative: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_HpFormula extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return "hpFormula"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "HP Formula";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {value: (mon.hp || {}).formula || ""};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_ArmorClass extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return "armorClass"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Armor Class";
|
||||
static ABV_DEFAULT = "AC";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {value: mon.ac[0] ? (mon.ac[0].ac || mon.ac[0]) : null};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_PassivePerception extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return "passivePerception"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Passive Perception";
|
||||
static ABV_DEFAULT = "PP";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {value: mon.passive};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_Speed extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return "speed"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Speed";
|
||||
static ABV_DEFAULT = "SPD";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: Math.max(0, ...Object.values(mon.speed || {})
|
||||
.map(it => it.number ? it.number : it)
|
||||
.filter(it => typeof it === "number")),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_SpellDc extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return "spellDc"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Spell DC";
|
||||
static ABV_DEFAULT = "DC";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: Math.max(
|
||||
0,
|
||||
...(mon.spellcasting || [])
|
||||
.filter(it => it.headerEntries)
|
||||
.map(it => {
|
||||
return it.headerEntries
|
||||
.map(it => {
|
||||
const found = [0];
|
||||
it
|
||||
.replace(/DC (\d+)/g, (...m) => found.push(Number(m[1])))
|
||||
.replace(/{@dc (\d+)}/g, (...m) => found.push(Number(m[1])));
|
||||
return Math.max(...found);
|
||||
})
|
||||
.filter(Boolean);
|
||||
})
|
||||
.flat(),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_Initiative extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return "initiative"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Initiative";
|
||||
static ABV_DEFAULT = "INIT";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: Parser.getAbilityModifier(mon.dex),
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getInitiativeInfo ({state}) {
|
||||
return {
|
||||
applicability: _INITIATIVE_APPLICABILITY_EXACT,
|
||||
initiative: isNaN(state.entity?.value) ? 0 : Number(state.entity.value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_LegendaryActions extends _InitiativeTrackerStatColumnBase {
|
||||
static _SERIALIZER_MAPPINGS = {
|
||||
"entity.current": "cur",
|
||||
"entity.max": "max",
|
||||
};
|
||||
|
||||
static _ = this._registerSerializerMappings();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static get POPULATE_WITH () { return "legendaryActions"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Legendary Actions";
|
||||
static ABV_DEFAULT = "LA";
|
||||
|
||||
getPlayerFriendlyState ({cell}) {
|
||||
return {
|
||||
...super.getPlayerFriendlyState({cell}),
|
||||
entity: {
|
||||
value: `${cell.entity.current || 0}/${cell.entity.max || 0}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {current: null, max: null};
|
||||
const cnt = mon.legendaryActions ?? (mon.legendary ? 3 : null);
|
||||
return {
|
||||
current: cnt,
|
||||
max: cnt,
|
||||
};
|
||||
}
|
||||
|
||||
_getAutoRoundStartObject ({state, mon}) {
|
||||
return {current: state.entity.max};
|
||||
}
|
||||
|
||||
$getRenderedHeader () {
|
||||
return super.$getRenderedHeader()
|
||||
.addClass("w-48p");
|
||||
}
|
||||
|
||||
$getRendered ({comp, mon}) {
|
||||
const $iptCurrent = ComponentUiUtil.$getIptNumber(
|
||||
comp,
|
||||
"current",
|
||||
null,
|
||||
{
|
||||
isAllowNull: true,
|
||||
fallbackOnNaN: null,
|
||||
html: `<input class="form-control input-sm hp dm-init__row-input text-right w-24p mr-0 br-0">`,
|
||||
},
|
||||
)
|
||||
.on("click", () => $iptCurrent.select());
|
||||
|
||||
const $iptMax = ComponentUiUtil.$getIptNumber(
|
||||
comp,
|
||||
"max",
|
||||
null,
|
||||
{
|
||||
isAllowNull: true,
|
||||
fallbackOnNaN: null,
|
||||
html: `<input class="form-control input-sm hp-max dm-init__row-input w-24p mr-0 bl-0">`,
|
||||
},
|
||||
)
|
||||
.on("click", () => $iptMax.select());
|
||||
|
||||
if (mon) {
|
||||
comp._addHookBase("isEditable", () => {
|
||||
$iptCurrent.prop("disabled", !comp._state.isEditable);
|
||||
$iptMax.prop("disabled", !comp._state.isEditable);
|
||||
})();
|
||||
}
|
||||
|
||||
return $$`<div class="ve-flex relative mr-3p">
|
||||
<div class="text-right">${$iptCurrent}</div>
|
||||
<div class="dm-init__sep-fields-slash ve-flex-vh-center">/</div>
|
||||
<div class="text-left">${$iptMax}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_Image extends _InitiativeTrackerStatColumnBase {
|
||||
static _SERIALIZER_MAPPINGS = {
|
||||
"entity.tokenUrl": "urlt",
|
||||
"entity.imageHref": "hrefi",
|
||||
};
|
||||
|
||||
static _ = this._registerSerializerMappings();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static get POPULATE_WITH () { return "image"; }
|
||||
static GROUP = GROUP_BASE_STATS;
|
||||
static NAME = "Image";
|
||||
static ABV_DEFAULT = "IMG";
|
||||
|
||||
getPlayerFriendlyState ({cell}) {
|
||||
return {
|
||||
...super.getPlayerFriendlyState({cell}),
|
||||
entity: {
|
||||
type: "image",
|
||||
tokenUrl: cell.entity.tokenUrl,
|
||||
imageHref: cell.entity.imageHref,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) {
|
||||
return {
|
||||
tokenUrl: UrlUtil.link(Renderer.get().getMediaUrl("img", "blank-friendly.webp")),
|
||||
imageHref: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
tokenUrl: Renderer.monster.getTokenUrl(mon),
|
||||
imageHref: fluff?.images?.[0].href,
|
||||
};
|
||||
}
|
||||
|
||||
$getRendered ({comp, mon, networking = null}) {
|
||||
const $ele = $$`<div class="mr-3p ve-flex-vh-center w-40p">
|
||||
<img src="${comp._state.tokenUrl}" class="w-30p h-30p" alt="Token Image">
|
||||
</div>`;
|
||||
|
||||
if (networking != null) {
|
||||
$ele.title("Click to Show to Connected Players");
|
||||
|
||||
$ele
|
||||
.on("click", () => {
|
||||
networking.sendShowImageMessageToClients({
|
||||
imageHref: InitiativeTrackerUtil.getImageOrTokenHref({imageHref: comp._state.imageHref, tokenUrl: comp._state.tokenUrl}),
|
||||
});
|
||||
});
|
||||
|
||||
// If no networking, assume this is an "edit" modal, and avoid binding events
|
||||
Renderer.monster.hover.bindFluffImageMouseover({mon, $ele});
|
||||
}
|
||||
|
||||
return $ele;
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_Save extends _InitiativeTrackerStatColumnBase {
|
||||
static _ATT;
|
||||
|
||||
static get POPULATE_WITH () { return `${this._ATT}Save`; }
|
||||
static GROUP = GROUP_SAVES;
|
||||
static get NAME () { return `${Parser.attAbvToFull(this._ATT)} Save`; }
|
||||
static get ABV_DEFAULT () { return this._ATT.toUpperCase(); }
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: mon.save?.[this.constructor._ATT] ? mon.save[this.constructor._ATT] : Parser.getAbilityModifier(mon[this.constructor._ATT]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_AbilityBonus extends _InitiativeTrackerStatColumnBase {
|
||||
static _ATT;
|
||||
|
||||
static get POPULATE_WITH () { return `${this._ATT}Bonus`; }
|
||||
static GROUP = GROUP_ABILITY_BONUS;
|
||||
static get NAME () { return `${Parser.attAbvToFull(this._ATT)} Bonus`; }
|
||||
static get ABV_DEFAULT () { return this._ATT.toUpperCase(); }
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: Parser.getAbilityModifier(mon[this.constructor._ATT]),
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getInitiativeInfo ({state}) {
|
||||
if (this.constructor._ATT !== "dex") return super.getInitiativeInfo({state});
|
||||
return {
|
||||
applicability: _INITIATIVE_APPLICABILITY_APPLICABLE,
|
||||
initiative: isNaN(state.entity?.value) ? 0 : Number(state.entity.value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_AbilityScore extends _InitiativeTrackerStatColumnBase {
|
||||
static _ATT;
|
||||
|
||||
static get POPULATE_WITH () { return `${this._ATT}Score`; }
|
||||
static GROUP = GROUP_ABILITY_SCORE;
|
||||
static get NAME () { return `${Parser.attAbvToFull(this._ATT)} Score`; }
|
||||
static get ABV_DEFAULT () { return this._ATT.toUpperCase(); }
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: mon[this.constructor._ATT],
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getInitiativeInfo ({state}) {
|
||||
if (this.constructor._ATT !== "dex") return super.getInitiativeInfo({state});
|
||||
return {
|
||||
applicability: _INITIATIVE_APPLICABILITY_APPLICABLE,
|
||||
initiative: isNaN(state.entity?.value) ? 0 : Parser.getAbilityModifier(Number(state.entity.value)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_Skill extends _InitiativeTrackerStatColumnBase {
|
||||
static _SKILL;
|
||||
|
||||
static get POPULATE_WITH () { return this._SKILL.toCamelCase(); }
|
||||
static GROUP = GROUP_SKILL;
|
||||
static get NAME () { return this._SKILL.toTitleCase(); }
|
||||
static get ABV_DEFAULT () { return Parser.skillToShort(this._SKILL).toUpperCase(); }
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) {
|
||||
if (!mon) return {value: null};
|
||||
return {
|
||||
value: mon.skill?.[this.constructor._SKILL]
|
||||
? mon.skill[this.constructor._SKILL]
|
||||
: Parser.getAbilityModifier(mon[Parser.skillToAbilityAbv(this.constructor._SKILL)]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _InitiativeTrackerStatColumnCheckboxBase extends _InitiativeTrackerStatColumnBase {
|
||||
static GROUP = GROUP_CHECKBOX;
|
||||
|
||||
static _AUTO_VALUE = undefined;
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) { return {value: false}; }
|
||||
|
||||
$getRendered ({comp, mon}) {
|
||||
const $cb = ComponentUiUtil.$getCbBool(comp, "value")
|
||||
.addClass("dm-init__stat_ipt");
|
||||
|
||||
if (mon) {
|
||||
comp._addHookBase("isEditable", () => {
|
||||
$cb.prop("disabled", !comp._state.isEditable);
|
||||
})();
|
||||
}
|
||||
|
||||
return $$`<label class="dm-init__wrp-stat-cb h-100 ve-flex-vh-center">${$cb}</label>`;
|
||||
}
|
||||
}
|
||||
|
||||
class _InitiativeTrackerStatColumnCheckboxTurnBase extends _InitiativeTrackerStatColumnCheckboxBase {
|
||||
_getAutoTurnStartObject ({state, mon}) { return {value: this.constructor._AUTO_VALUE}; }
|
||||
}
|
||||
|
||||
class _InitiativeTrackerStatColumnCheckboxRoundBase extends _InitiativeTrackerStatColumnCheckboxBase {
|
||||
_getAutoRoundStartObject ({state, mon}) { return {value: this.constructor._AUTO_VALUE}; }
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_Checkbox extends _InitiativeTrackerStatColumnCheckboxBase {
|
||||
static get POPULATE_WITH () { return "cbNeutral"; }
|
||||
static NAME = "Checkbox";
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_CheckboxAutoTurnLow extends _InitiativeTrackerStatColumnCheckboxTurnBase {
|
||||
static get POPULATE_WITH () { return "cbAutoLow"; }
|
||||
static NAME = "Checkbox; clears at start of turn";
|
||||
|
||||
static _AUTO_VALUE = false;
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_CheckboxAutoTurnHigh extends _InitiativeTrackerStatColumnCheckboxTurnBase {
|
||||
static get POPULATE_WITH () { return "cbAutoHigh"; }
|
||||
static NAME = "Checkbox; ticks at start of turn";
|
||||
|
||||
static _AUTO_VALUE = true;
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) { return {value: true}; }
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_CheckboxAutoRoundLow extends _InitiativeTrackerStatColumnCheckboxRoundBase {
|
||||
static get POPULATE_WITH () { return "cbAutoRoundLow"; }
|
||||
static NAME = "Checkbox; clears at start of round";
|
||||
|
||||
static _AUTO_VALUE = false;
|
||||
}
|
||||
|
||||
class InitiativeTrackerStatColumn_CheckboxAutoRoundHigh extends _InitiativeTrackerStatColumnCheckboxRoundBase {
|
||||
static get POPULATE_WITH () { return "cbAutoRoundHigh"; }
|
||||
static NAME = "Checkbox; ticks at start of round";
|
||||
|
||||
static _AUTO_VALUE = true;
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) { return {value: true}; }
|
||||
}
|
||||
|
||||
export class InitiativeTrackerStatColumn_Custom extends _InitiativeTrackerStatColumnBase {
|
||||
static get POPULATE_WITH () { return ""; }
|
||||
static GROUP = GROUP_CUSTOM;
|
||||
static NAME = "(Custom)";
|
||||
|
||||
_getInitialCellObj ({mon, fluff}) { return {value: ""}; }
|
||||
}
|
||||
|
||||
export class InitiativeTrackerStatColumnFactory {
|
||||
static _COL_CLS_LOOKUP = {};
|
||||
|
||||
static _initLookup () {
|
||||
[
|
||||
InitiativeTrackerStatColumn_HpFormula,
|
||||
InitiativeTrackerStatColumn_ArmorClass,
|
||||
InitiativeTrackerStatColumn_PassivePerception,
|
||||
InitiativeTrackerStatColumn_Speed,
|
||||
InitiativeTrackerStatColumn_SpellDc,
|
||||
InitiativeTrackerStatColumn_Initiative,
|
||||
InitiativeTrackerStatColumn_LegendaryActions,
|
||||
InitiativeTrackerStatColumn_Image,
|
||||
].forEach(Cls => this._initLookup_addCls(Cls));
|
||||
|
||||
Parser.ABIL_ABVS
|
||||
.forEach(abv => {
|
||||
this._initLookup_addCls(class extends InitiativeTrackerStatColumn_Save { static _ATT = abv; });
|
||||
});
|
||||
|
||||
Parser.ABIL_ABVS
|
||||
.forEach(abv => {
|
||||
this._initLookup_addCls(class extends InitiativeTrackerStatColumn_AbilityBonus { static _ATT = abv; });
|
||||
});
|
||||
|
||||
Parser.ABIL_ABVS
|
||||
.forEach(abv => {
|
||||
this._initLookup_addCls(class extends InitiativeTrackerStatColumn_AbilityScore { static _ATT = abv; });
|
||||
});
|
||||
|
||||
Object.keys(Parser.SKILL_TO_ATB_ABV)
|
||||
.sort(SortUtil.ascSort)
|
||||
.forEach(skill => {
|
||||
this._initLookup_addCls(class extends InitiativeTrackerStatColumn_Skill { static _SKILL = skill; });
|
||||
});
|
||||
|
||||
[
|
||||
InitiativeTrackerStatColumn_Checkbox,
|
||||
InitiativeTrackerStatColumn_CheckboxAutoTurnLow,
|
||||
InitiativeTrackerStatColumn_CheckboxAutoTurnHigh,
|
||||
InitiativeTrackerStatColumn_CheckboxAutoRoundLow,
|
||||
InitiativeTrackerStatColumn_CheckboxAutoRoundHigh,
|
||||
].forEach(Cls => this._initLookup_addCls(Cls));
|
||||
}
|
||||
|
||||
static _initLookup_addCls (Cls) { this._COL_CLS_LOOKUP[Cls.POPULATE_WITH] = Cls; }
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static getGroupedByUi () {
|
||||
const out = [
|
||||
[InitiativeTrackerStatColumn_Custom],
|
||||
];
|
||||
|
||||
let groupPrev = GROUP_CUSTOM;
|
||||
Object.values(this._COL_CLS_LOOKUP)
|
||||
.forEach(Cls => {
|
||||
if (groupPrev !== Cls.GROUP) out.push([]);
|
||||
out.last().push(Cls);
|
||||
groupPrev = Cls.GROUP;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @param dataSerial
|
||||
* @param data
|
||||
* @return {_InitiativeTrackerStatColumnBase}
|
||||
*/
|
||||
static fromStateData ({dataSerial, data}) {
|
||||
if (dataSerial && data) throw new Error(`Only one of "dataSerial" and "data" may be provided!`);
|
||||
|
||||
data = data ?? InitiativeTrackerStatColumnDataSerializer.fromSerial(dataSerial);
|
||||
|
||||
const Cls = this._COL_CLS_LOOKUP[data.populateWith] ?? InitiativeTrackerStatColumn_Custom;
|
||||
return new Cls(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param colName
|
||||
* @return {_InitiativeTrackerStatColumnBase}
|
||||
*/
|
||||
static fromEncounterAdvancedColName ({colName}) {
|
||||
colName = colName.toLowerCase().trim();
|
||||
const Cls = Object.values(this._COL_CLS_LOOKUP)
|
||||
.find(Cls => Cls.ABV_DEFAULT.toLowerCase() === colName)
|
||||
|| InitiativeTrackerStatColumn_Custom;
|
||||
|
||||
return new Cls({
|
||||
id: CryptUtil.uid(),
|
||||
isEditable: true,
|
||||
isPlayerVisible: IS_PLAYER_VISIBLE_PLAYER_UNITS_ONLY,
|
||||
abbreviation: colName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param populateWith
|
||||
* @return {_InitiativeTrackerStatColumnBase}
|
||||
*/
|
||||
static fromPopulateWith ({populateWith}) {
|
||||
const Cls = this._COL_CLS_LOOKUP[populateWith] ?? InitiativeTrackerStatColumn_Custom;
|
||||
return new Cls({});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data
|
||||
* @return {_InitiativeTrackerStatColumnBase}
|
||||
*/
|
||||
static fromCollectionRowStateData ({data}) {
|
||||
const flat = {id: data.id, ...data.entity};
|
||||
return this.fromStateData({data: flat});
|
||||
}
|
||||
}
|
||||
|
||||
InitiativeTrackerStatColumnFactory._initLookup();
|
||||
@@ -0,0 +1,25 @@
|
||||
export class InitiativeTrackerUi {
|
||||
static $getBtnPlayerVisible (isVisible, fnOnClick, isTriState, ...additionalClasses) {
|
||||
let isVisNum = Number(isVisible || false);
|
||||
|
||||
const getTitle = () => isVisNum === 0 ? `Hidden in player view` : isVisNum === 1 ? `Shown in player view` : `Shown in player view on player characters, hidden in player view on monsters`;
|
||||
const getClasses = () => `${isVisNum === 0 ? `btn-default` : isVisNum === 1 ? `btn-primary` : `btn-primary btn-primary--half`} btn btn-xs ${additionalClasses.join(" ")}`;
|
||||
const getIconClasses = () => isVisNum === 0 ? `glyphicon glyphicon-eye-close` : `glyphicon glyphicon-eye-open`;
|
||||
|
||||
const $dispIcon = $(`<span class="glyphicon ${getIconClasses()}"></span>`);
|
||||
const $btnVisible = $$`<button class="${getClasses()}" title="${getTitle()}" tabindex="-1">${$dispIcon}</button>`
|
||||
.on("click", () => {
|
||||
if (isVisNum === 0) isVisNum++;
|
||||
else if (isVisNum === 1) isVisNum = isTriState ? 2 : 0;
|
||||
else if (isVisNum === 2) isVisNum = 0;
|
||||
|
||||
$btnVisible.title(getTitle());
|
||||
$btnVisible.attr("class", getClasses());
|
||||
$dispIcon.attr("class", getIconClasses());
|
||||
|
||||
fnOnClick();
|
||||
});
|
||||
|
||||
return $btnVisible;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export class InitiativeTrackerDataSerializerBase {
|
||||
static _FIELD_MAPPINGS = {};
|
||||
|
||||
static _getFieldMappings () {
|
||||
return Object.entries(this._FIELD_MAPPINGS)
|
||||
.map(([kFull, kSerial]) => {
|
||||
const kFullParts = kFull.split(".");
|
||||
return {
|
||||
kFullParts,
|
||||
kSerial,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static registerMapping ({kFull, kSerial, isAllowDuplicates = false}) {
|
||||
if (!isAllowDuplicates) {
|
||||
if (this._FIELD_MAPPINGS[kFull]) throw new Error(`Serializer key "${kFull}" was already registered!`);
|
||||
}
|
||||
|
||||
if (!isAllowDuplicates || this._FIELD_MAPPINGS[kFull] == null) {
|
||||
if (Object.values(this._FIELD_MAPPINGS).some(k => k === kSerial)) throw new Error(`Serializer value "${kFull}" was already registered!`);
|
||||
}
|
||||
|
||||
this._FIELD_MAPPINGS[kFull] = kSerial;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static fromSerial (dataSerial) {
|
||||
const out = {};
|
||||
|
||||
this._getFieldMappings()
|
||||
.filter(({kSerial}) => dataSerial[kSerial] != null)
|
||||
.forEach(({kFullParts, kSerial}) => MiscUtil.set(out, ...kFullParts, dataSerial[kSerial]));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static toSerial (data) {
|
||||
return this._getFieldMappings()
|
||||
.filter(({kFullParts}) => MiscUtil.get(data, ...kFullParts) != null)
|
||||
.mergeMap(({kFullParts, kSerial}) => ({[kSerial]: MiscUtil.get(data, ...kFullParts)}));
|
||||
}
|
||||
}
|
||||
661
js/dmscreen/initiativetracker/dmscreen-initiativetracker.js
Normal file
661
js/dmscreen/initiativetracker/dmscreen-initiativetracker.js
Normal file
@@ -0,0 +1,661 @@
|
||||
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 = $(`<div class="dm-init dm__panel-bg dm__data-anchor"></div>`);
|
||||
|
||||
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 = $(`<button title="Sort Alphabetically" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>`)
|
||||
.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 = $(`<button title="Sort Numerically" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-sort-by-order"></span></button>`)
|
||||
.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 $$`<div class="btn-group ve-flex">
|
||||
${$btnSortAlpha}
|
||||
${$btnSortNumber}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_render_$getWrpFooter ({doUpdateExternalStates}) {
|
||||
const $btnAdd = $(`<button class="btn btn-primary btn-xs dm-init-lockable" title="Add Player"><span class="glyphicon glyphicon-plus"></span></button>`)
|
||||
.on("click", async () => {
|
||||
if (this._state.isLocked) return;
|
||||
this._state.rows = [
|
||||
...this._state.rows,
|
||||
await this._rowStateBuilderActive.pGetNewRowState({
|
||||
isPlayerVisible: true,
|
||||
}),
|
||||
]
|
||||
.filter(Boolean);
|
||||
});
|
||||
|
||||
const $btnAddMonster = $(`<button class="btn btn-success btn-xs dm-init-lockable mr-2" title="Add Monster"><span class="glyphicon glyphicon-print"></span></button>`)
|
||||
.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 = $(`<button class="btn btn-default btn-xs" title="Previous Turn"><span class="glyphicon glyphicon-step-backward"></span></button>`)
|
||||
.click(() => this._viewRowsActive.pDoShiftActiveRow({direction: InitiativeTrackerConst.DIR_BACKWARDS}));
|
||||
const $btnSetNextActive = $(`<button class="btn btn-default btn-xs mr-2" title="Next Turn"><span class="glyphicon glyphicon-step-forward"></span></button>`)
|
||||
.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 = $(`<button class="btn btn-primary btn-xs mr-2" title="Player View (SHIFT to Open "Standard" View)"><span class="glyphicon glyphicon-user"></span></button>`)
|
||||
.click(evt => {
|
||||
if (evt.shiftKey) return this._networking.handleClick_playerWindowV1({doUpdateExternalStates});
|
||||
return ContextUtil.pOpenMenu(evt, menuPlayerWindow);
|
||||
});
|
||||
|
||||
const $btnLock = $(`<button class="btn btn-danger btn-xs" title="Lock Tracker"><span class="glyphicon glyphicon-lock"></span></button>`)
|
||||
.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 = $(`<button class="btn btn-default btn-xs mr-2" title="Configure (SHIFT to Open "Settings")"><span class="glyphicon glyphicon-cog"></span></button>`)
|
||||
.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 = $(`<button title="Import an encounter from the Bestiary" class="btn btn-success btn-xs dm-init-lockable"><span class="glyphicon glyphicon-upload"></span></button>`)
|
||||
.click((evt) => {
|
||||
if (this._state.isLocked) return;
|
||||
ContextUtil.pOpenMenu(evt, menuImport);
|
||||
});
|
||||
const $btnReset = $(`<button title="Reset Tracker" class="btn btn-danger btn-xs dm-init-lockable"><span class="glyphicon glyphicon-trash"></span></button>`)
|
||||
.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 $$`<div class="dm-init__wrp-controls">
|
||||
<div class="ve-flex">
|
||||
<div class="btn-group ve-flex">
|
||||
${$btnAdd}
|
||||
${$btnAddMonster}
|
||||
</div>
|
||||
<div class="btn-group">${$btnSetPrevActive}${$btnSetNextActive}</div>
|
||||
${$iptRound}
|
||||
</div>
|
||||
|
||||
${this._render_$getWrpButtonsSort()}
|
||||
|
||||
<div class="ve-flex">
|
||||
${$btnNetworking}
|
||||
|
||||
<div class="btn-group ve-flex-v-center">
|
||||
${$btnLock}
|
||||
${$btnConfigure}
|
||||
</div>
|
||||
|
||||
<div class="btn-group ve-flex-v-center">
|
||||
${$btnLoad}
|
||||
${$btnReset}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_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 = {
|
||||
// region TODO(DMS) not ideal--merge columns instead? Note that this also clobbers default party info
|
||||
isStatsAddColumns: nxtState.isStatsAddColumns,
|
||||
statsCols: nxtState.statsCols
|
||||
.map(it => it.getAsStateData()),
|
||||
// endregion
|
||||
|
||||
rows: this._state.importIsAppend
|
||||
? [
|
||||
...rowsPrevNonDefaultParty,
|
||||
...rowsFromDefaultParty,
|
||||
...nxtState.rows,
|
||||
]
|
||||
: [
|
||||
...rowsFromDefaultParty,
|
||||
...nxtState.rows,
|
||||
],
|
||||
};
|
||||
|
||||
if (!this._state.importIsAppend) {
|
||||
const defaultState = this._getDefaultState();
|
||||
["round", "sort", "dir"]
|
||||
.forEach(prop => stateNxt[prop] = defaultState[prop]);
|
||||
}
|
||||
|
||||
this._proxyAssignSimple("state", stateNxt);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user