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:
@@ -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); }
|
||||
}
|
||||
Reference in New Issue
Block a user