class LevelsUI extends FormApplication {
constructor() {
super();
this.range = [];
this.rangeEnabled = false;
this.isEdit = false;
this.definedLevels = [];
this.roofEnabled = false;
this.placeOverhead = false;
this.stairEnabled = true;
}
static get defaultOptions() {
return {
...super.defaultOptions,
title: game.i18n.localize("levels.ui.title"),
id: "levelsUI",
template: `modules/levels/templates/layerTool.hbs`,
resizable: true,
dragDrop: [{ dragSelector: null, dropSelector: null }],
};
}
get currentRange() {
const bgElev = canvas.primary.background.elevation;
if(!this.rangeEnabled) return { bottom: bgElev, top: bgElev };
if(!this.range?.length) return { bottom: bgElev, top: bgElev };
return {
bottom: parseFloat(this.range[0]),
top: parseFloat(this.range[1]),
}
}
getData() {
return {};
}
async activateListeners(html) {
ui.controls.control.foreground = true;
canvas.tiles._activateSubLayer(true);
this.rangeEnabled = true;
this.loadLevels();
html.on("click", ".level-item", this._onChangeLevel.bind(this));
html.on("click", ".level-item .fa-trash", this._onRemoveLevel.bind(this));
html.on(
"click",
"#levels-ui-controls .fa-trash",
this._onClearLevels.bind(this)
);
html.on(
"click",
"#levels-ui-controls .fa-plus",
this._onAddLevel.bind(this)
);
html.on(
"click",
"#levels-ui-controls .fa-edit",
this._onToggleEdit.bind(this)
);
html.on(
"click",
"#levels-ui-controls .fa-map",
this._onGetFromScene.bind(this)
);
html.on(
"click",
"#levels-ui-controls .fa-users",
this._onShowPlayerList.bind(this)
);
html.on(
"click",
"#levels-ui-controls .fa-archway",
() => {
this.roofEnabled = !this.roofEnabled;
this.setButtonStyles();
this.computeLevelsVisibility();
}
);
html.on(
"click",
"#levels-ui-controls .fa-tree",
() => {
this.placeOverhead = !this.placeOverhead;
this.setButtonStyles();
}
);
html.on(
"click",
"#levels-ui-controls .fa-sort-amount-up-alt",
() => {
this.stairEnabled = !this.stairEnabled;
this.setButtonStyles();
}
);
html.on("click", ".player-portrait", this._onControlToken.bind(this));
html.on("change", ".level-inputs input", this.saveData.bind(this));
//make list sortable
html.find("#levels-list").sortable({
axis: "y",
handle: ".fa-arrows-alt",
update: this.saveData.bind(this),
});
const index = this.definedLevels
? this.definedLevels.indexOf(
this.definedLevels.find(
(l) =>
l[0] == this.range[0] &&
l[1] == this.range[1] &&
l[2] == this.range[2]
)
)
: undefined;
if (index === undefined || index === -1) {
html.find("#levels-list li:last-child").click();
} else html.find("#levels-list li")[index].click();
this.updatePlayerList();
this.setButtonStyles();
}
setButtonStyles() {
this.element.find(".fa-archway").toggleClass("active", this.roofEnabled);
this.element.find(".fa-tree").toggleClass("active", this.placeOverhead);
this.element.find(".fa-sort-amount-up-alt").toggleClass("active", this.stairEnabled);
this.element.find(".fa-users").toggleClass("active", this.element.find(".players-on-level").hasClass("active"));
}
_onAddLevel(event) {
let $li = this.generateLi([0, 0, ""]);
$("#levels-list").append($li);
}
//toggle readonly property of inputs and toggle visibility of trash icon
_onToggleEdit(event) {
this.isEdit = !this.isEdit;
let $inputs = $(".level-inputs input");
$inputs.prop("readonly", !$inputs.prop("readonly"));
$(".level-item .fa-trash").toggleClass("hidden");
$(".level-item .fa-arrows-alt").toggleClass("hidden");
this.saveData();
}
activateForeground(){
try{
ui.controls.control.foreground = true;
ui.controls.control.foreground = true;
canvas.tiles._activateSubLayer(true);
canvas.perception.update({refreshLighting: true, refreshTiles: true}, true);
const fgControl = document.querySelector(`[data-tool="foreground"]`)
if(fgControl) fgControl.classList.add("active")
}catch(e){}
}
_onChangeLevel(event) {
this.activateForeground()
if (!$(event.target).hasClass("player-portrait"))
canvas.tokens.releaseAll();
let $target = $(event.currentTarget);
let $parent = $target.parent();
$parent.find(".fa-caret-right").removeClass("active");
$parent.find("li").removeClass("active");
$target.find(".fa-caret-right").addClass("active");
$target.addClass("active");
const top = $target.find(".level-top").val();
const bottom = $target.find(".level-bottom").val();
const name = $target.find(".level-name").val();
this.definedLevels = canvas.scene.getFlag(
CONFIG.Levels.MODULE_ID,
"sceneLevels"
);
this.range = this.definedLevels?.find((l) => l[0] == bottom && l[1] == top) ?? [parseFloat(bottom), parseFloat(top)];
if ($(event.target).hasClass("player-portrait")) return;
WallHeight.currentTokenElevation = parseFloat(bottom);
this.computeLevelsVisibility(this.range);
Hooks.callAll("levelsUiChangeLevel");
}
_onRemoveLevel(event) {
Dialog.confirm({
title: game.i18n.localize("levels.dialog.removeLevel.title"),
content: game.i18n.localize("levels.dialog.removeLevel.content"),
yes: () => {
let $target = $(event.currentTarget);
$target.remove();
this.saveData();
},
no: () => {},
defaultYes: false,
});
}
_onGetFromScene(event) {
Dialog.confirm({
title: game.i18n.localize("levels.dialog.getFromScene.title"),
content: game.i18n.localize("levels.dialog.getFromScene.content"),
yes: (html) => {
const maxRange = parseFloat(html.find("#maxelevationdifference").val());
this.getFromScene(maxRange);
},
no: () => {},
render: (html) => {
const maxRange = `
`
$(html[0]).append(maxRange);
},
defaultYes: false,
});
}
_onShowPlayerList(event) {
this.element.find(".players-on-level").toggleClass("active");
this.setButtonStyles();
}
_onControlToken(event) {
canvas.tokens.releaseAll();
const tokenId = event.currentTarget.dataset.tokenid;
const token = canvas.tokens.get(tokenId);
token.control();
}
saveData() {
let data = [];
$(this.element)
.find("li")
.each((index, element) => {
let $element = $(element);
let name = $element.find(".level-name").val();
let top = $element.find(".level-top").val();
let bottom = $element.find(".level-bottom").val();
data.push([bottom, top, name]);
});
canvas.scene.setFlag(CONFIG.Levels.MODULE_ID, "sceneLevels", data);
}
async loadLevels() {
$("#levels-list").empty();
let levelsFlag =
canvas.scene.getFlag(CONFIG.Levels.MODULE_ID, "sceneLevels") || [];
this.definedLevels = levelsFlag;
this.range = this.range ?? this.definedLevels[levelsFlag.length - 1];
if (levelsFlag) {
for (let level of levelsFlag) {
this.element.find("#levels-list").append(this.generateLi(level));
}
}
}
generateLi(data) {
//data 0 - top 1- bottom 2- name
let $li = $(`
`);
$li.find("input").prop("readonly", !this.isEdit);
$li.find(".fa-trash").toggleClass("hidden", this.isEdit);
$li.find(".fa-arrows-alt").toggleClass("hidden", this.isEdit);
return $li;
}
async _onClearLevels() {
Dialog.confirm({
title: game.i18n.localize("levels.dialog.levelsclear.title"),
content: game.i18n.localize("levels.dialog.levelsclear.content"),
yes: async () => {
await canvas.scene.setFlag(CONFIG.Levels.MODULE_ID, "sceneLevels", []);
this.loadLevels();
},
no: () => {},
defaultYes: false,
});
}
updatePlayerList() {
const playerActors = Array.from(game.users).map(
(user) => user.character?.id
);
const players = canvas.tokens.placeables.filter((token) =>
playerActors.includes(token.actor?.id)
);
$(this.element)
.find("li")
.each((index, element) => {
let $element = $(element);
const top = $element.find(".level-top").val();
const bottom = $element.find(".level-bottom").val();
const $playerList = $element.find(".players-on-level");
$playerList.empty();
players.forEach((player) => {
if (
player.losHeight >= bottom &&
player.losHeight < top &&
player.id
) {
const color = Array.from(game.users).find(
(user) => user.character?.id == player?.actor?.id
)?.border;
$playerList.append(
``
);
}
});
});
this.element.css("height", "auto");
}
close(force = false) {
if (!force) this.saveData();
this.rangeEnabled = false;
if (!force) this.computeLevelsVisibility();
CONFIG.Levels.handlers.RefreshHandler.restoreVisAll();
CONFIG.Levels.handlers.RefreshHandler.refreshAll();
WallHeight.schedulePerceptionUpdate()
super.close();
}
async getFromScene(maxRange = 9) {
let autoLevels = {};
for (let wall of canvas.walls.placeables) {
const { top, bottom } = WallHeight.getWallBounds(wall);
let entityRange = [bottom, top];
if (
entityRange[0] != -Infinity &&
entityRange[1] != Infinity &&
(entityRange[0] || entityRange[0] == 0) &&
(entityRange[1] || entityRange[1] == 0)
) {
autoLevels[`${entityRange[0]}${entityRange[1]}`] = entityRange;
}
}
for (let tile of canvas.tiles.placeables.filter(
(t) => t.document.overhead
)) {
let { rangeBottom, rangeTop } =
CONFIG.Levels.helpers.getRangeForDocument(tile);
if (
(rangeBottom || rangeBottom == 0) &&
(rangeTop || rangeTop == 0) &&
rangeTop != Infinity &&
rangeBottom != -Infinity
) {
autoLevels[`${rangeBottom}${rangeTop}`] = [rangeBottom, rangeTop];
}
}
for (let light of canvas.lighting.placeables) {
let { rangeBottom, rangeTop } =
CONFIG.Levels.helpers.getRangeForDocument(light);
if (
(rangeBottom || rangeBottom == 0) &&
(rangeTop || rangeTop == 0) &&
rangeTop != Infinity &&
rangeBottom != -Infinity
) {
autoLevels[`${rangeBottom}${rangeTop}`] = [rangeBottom, rangeTop];
}
}
for (let drawing of canvas.drawings.placeables) {
let { rangeBottom, rangeTop } =
CONFIG.Levels.helpers.getRangeForDocument(drawing);
if (
(rangeBottom || rangeBottom == 0) &&
(rangeTop || rangeTop == 0) &&
rangeTop != Infinity &&
rangeBottom != -Infinity
) {
autoLevels[`${rangeBottom}${rangeTop}`] = [rangeBottom, rangeTop];
}
}
let autoRange = Object.entries(autoLevels)
.map((x) => x[1])
.filter( x => Math.abs(x[1] - x[0]) >= maxRange)
.sort()
.reverse();
if (autoRange.length) {
await canvas.scene.setFlag(
CONFIG.Levels.MODULE_ID,
"sceneLevels",
autoRange
);
this.loadLevels();
}
}
computeLevelsVisibility() {
WallHeight.currentTokenElevation = parseFloat(this.range[0] ?? 0);
CONFIG.Levels.handlers.RefreshHandler.refreshAll();
WallHeight.schedulePerceptionUpdate();
}
computeRangeForDocument(document, range, isTile = false) {
let { rangeBottom, rangeTop } =
CONFIG.Levels.helpers.getRangeForDocument(document);
rangeBottom = rangeBottom ?? -Infinity;
rangeTop = rangeTop ?? Infinity;
range[0] = parseFloat(range[0]) ?? -Infinity;
range[1] = parseFloat(range[1]) ?? Infinity;
let entityRange = [rangeBottom, rangeTop];
if (!isTile) {
if (
(entityRange[0] >= range[0] && entityRange[0] <= range[1]) ||
(entityRange[1] >= range[0] && entityRange[1] <= range[1])
) {
return true;
} else {
return false;
}
} else {
if (entityRange[0] == range[1] + 1 && entityRange[1] == Infinity) {
return true;
} else {
if (entityRange[0] >= range[0] && entityRange[1] <= range[1]) {
return true;
} else {
return false;
}
}
}
}
getObjUpdateData(range) {
return {
flags: {
[`${CONFIG.Levels.MODULE_ID}`]: {
rangeBottom: parseFloat(range[0]),
rangeTop: parseFloat(range[1]),
},
},
};
}
async elevationDialog(tool) {
let content = `
`;
let ignoreClose = false;
let toolhtml = $("body").find(`li[data-tool="setTemplateElevation"]`);
let dialog = new Dialog({
title: game.i18n.localize("levels.dialog.elevation.title"),
content: content,
buttons: {
confirm: {
label: game.i18n.localize("levels.yesnodialog.yes"),
callback: (html) => {
CONFIG.Levels.UI.nextTemplateHeight = html.find(
`input[name="templateElevation"]`
)[0].valueAsNumber;
CONFIG.Levels.UI.nextTemplateSpecial = html.find(
`input[name="special"]`
)[0].valueAsNumber;
CONFIG.Levels.UI.templateElevation = true;
ignoreClose = true;
tool.active = true;
if (toolhtml[0])
$("body")
.find(`li[data-tool="setTemplateElevation"]`)
.addClass("active");
},
},
close: {
label: game.i18n.localize("levels.yesnodialog.no"),
callback: () => {
CONFIG.Levels.UI.nextTemplateHeight = undefined;
CONFIG.Levels.UI.nextTemplateSpecial = undefined;
CONFIG.Levels.UI.templateElevation = false;
tool.active = false;
if (toolhtml[0])
$("body")
.find(`li[data-tool="setTemplateElevation"]`)
.removeClass("active");
},
},
},
default: "confirm",
close: () => {
if (ignoreClose == true) {
ignoreClose = false;
return;
}
CONFIG.Levels.nextTemplateHeight = undefined;
CONFIG.Levels.nextTemplateSpecial = undefined;
CONFIG.Levels.templateElevation = false;
tool.active = false;
if (toolhtml[0])
$("body")
.find(`li[data-tool="setTemplateElevation"]`)
.removeClass("active");
},
});
await dialog._render(true);
}
}
$(document).on("click", `li[data-control="levels"]`, (e) => {
CONFIG.Levels.UI.render(true);
})
Hooks.on("renderSceneControls", (controls, b, c) => {
if(game.user.isGM){
$(".main-controls").append(`
`)
}
});
Hooks.on("ready", () => {
if (game.user.isGM) {
Hooks.on("activateTilesLayer", ()=>{
if(CONFIG.Levels.UI.rangeEnabled){
Hooks.once("renderSceneControls", ()=>{
CONFIG.Levels.UI.activateForeground();
})
}
})
Hooks.on("canvasInit", () => {
CONFIG.Levels.UI.close(true);
})
Hooks.on("updateToken", (token,updates)=>{
if("elevation" in updates)CONFIG.Levels.UI.updatePlayerList();
})
Hooks.on("createToken", (token,updates)=>{
CONFIG.Levels.UI.updatePlayerList();
})
Hooks.on("deleteToken", (token,updates)=>{
CONFIG.Levels.UI.updatePlayerList();
})
Hooks.on("controlToken", (token,controlled)=>{
if(CONFIG.Levels.UI.rangeEnabled && !canvas.tokens.controlled.length){
CONFIG.Levels.UI.computeLevelsVisibility();
}else if(CONFIG.Levels.UI.rangeEnabled){
CONFIG.Levels.handlers.RefreshHandler.refresh(canvas.tokens);
}
})
Hooks.on("renderLevelsUI", (app, html) => {
if(!app.positionSet){
console.log((window.innerWidth - $("#sidebar").width() - $("#levelsUI").width()))
app.setPosition({
top: 2,
left: (window.innerWidth - $("#sidebar").width() - $("#levelsUI").width()) - 10,
width: $("#levelsUI").width(),
height: Math.max(150, $("#levelsUI").height()),
});
app.positionSet = true
}
})
Hooks.on("preCreateTile", (tile, updates) => {
if (CONFIG.Levels.UI.rangeEnabled == true) {
tile.updateSource({
overhead: true,
});
if(!game.Levels3DPreview?._active){
tile.updateSource({
flags: {
"betterroofs": {
brMode: CONFIG.Levels.UI.roofEnabled,
},
[`${CONFIG.Levels.MODULE_ID}`]: {
rangeBottom: CONFIG.Levels.UI.roofEnabled
? parseFloat(CONFIG.Levels.UI.range[1])
: parseFloat(CONFIG.Levels.UI.range[0]),
rangeTop: CONFIG.Levels.UI.roofEnabled ? Infinity : parseFloat(CONFIG.Levels.UI.range[1]),
allWallBlockSight: CONFIG.Levels.UI.roofEnabled
}
},
roof: CONFIG.Levels.UI.roofEnabled,
});
}else{
tile.updateSource({
flags: {
[`${CONFIG.Levels.MODULE_ID}`]: {
rangeTop: CONFIG.Levels.UI.roofEnabled ? Infinity : CONFIG.Levels.UI.range[1],
}
}
});
}
if(CONFIG.Levels.UI.placeOverhead){
tile.updateSource({
roof: false,
flags: {
[`${CONFIG.Levels.MODULE_ID}`]: {
showIfAbove: true,
noCollision: true,
showAboveRange: parseFloat(CONFIG.Levels.UI.range[1]) - parseFloat(CONFIG.Levels.UI.range[0]),
rangeBottom: parseFloat(CONFIG.Levels.UI.range[1]),
rangeTop: parseFloat(CONFIG.Levels.UI.range[1]),
}
}
});
}
}
});
Hooks.on("preCreateAmbientLight", (light, updates) => {
if (CONFIG.Levels.UI.rangeEnabled == true && !game.Levels3DPreview?._active) {
light.updateSource(CONFIG.Levels.UI.getObjUpdateData(CONFIG.Levels.UI.range));
}
});
Hooks.on("preCreateAmbientSound", (sound, updates) => {
if (CONFIG.Levels.UI.rangeEnabled == true) {
sound.updateSource(CONFIG.Levels.UI.getObjUpdateData(CONFIG.Levels.UI.range));
}
});
Hooks.on("preCreateNote", (note, updates) => {
if (CONFIG.Levels.UI.rangeEnabled == true) {
note.updateSource(CONFIG.Levels.UI.getObjUpdateData(CONFIG.Levels.UI.range));
}
});
Hooks.on("preCreateDrawing", (drawing, updates) => {
let sortedLevels = [...CONFIG.Levels.UI.definedLevels].sort((a, b) => {
return parseFloat(b[0]) - parseFloat(a[0])
})
let aboverange = sortedLevels.find(l => CONFIG.Levels.UI.range[0] === l[0] && CONFIG.Levels.UI.range[1] === l[1])
aboverange = sortedLevels.indexOf(aboverange) === 0 ? undefined : sortedLevels[sortedLevels.indexOf(aboverange) - 1]
if (aboverange) {
let newTop = aboverange[1];
let newBot = aboverange[0];
if (CONFIG.Levels.UI.rangeEnabled == true) {
drawing.updateSource({
hidden: CONFIG.Levels.UI.stairEnabled,
text: CONFIG.Levels.UI.stairEnabled ? `Levels Stair ${CONFIG.Levels.UI.range[0]}-${newBot}` : "",
flags: {
levels: {
drawingMode: CONFIG.Levels.UI.stairEnabled ? 2 : 0,
rangeBottom: parseFloat(CONFIG.Levels.UI.range[0]),
rangeTop: newBot - 1,
},
},
});
}
} else {
if (CONFIG.Levels.UI.rangeEnabled == true) {
drawing.updateSource({
hidden: CONFIG.Levels.UI.stairEnabled,
text: CONFIG.Levels.UI.stairEnabled ? `Levels Stair ${CONFIG.Levels.UI.range[0]}-${CONFIG.Levels.UI.range[1] + 1}` : "",
flags: {
levels: {
drawingMode: CONFIG.Levels.UI.stairEnabled ? 2 : 0,
rangeBottom: parseFloat(CONFIG.Levels.UI.range[0]),
rangeTop: parseFloat(CONFIG.Levels.UI.range[1]),
},
},
});
}
}
});
Hooks.on("preCreateWall", (wall, updates) => {
if (CONFIG.Levels.UI.rangeEnabled == true) {
wall.updateSource({
flags: {
"wall-height": {
bottom: parseFloat(CONFIG.Levels.UI.range[0]),
top: parseFloat(CONFIG.Levels.UI.range[1]),
},
},
});
}
});
Hooks.on("preCreateToken", (token, updates) => {
if (CONFIG.Levels.UI.rangeEnabled == true) {
if(updates) updates.elevation = parseFloat(CONFIG.Levels.UI.range[0]);
token.updateSource({
elevation: updates?.elevation ?? parseFloat(CONFIG.Levels.UI.range[0]),
});
}
});
}
});
Hooks.on("getSceneControlButtons", (controls, b, c) => {
let templateTool = {
name: "setTemplateElevation",
title: game.i18n.localize("levels.controls.setTemplateElevation.name"),
icon: "fas fa-sort",
toggle: true,
active: CONFIG.Levels?.UI.templateElevation || false,
onClick: (toggle) => {
CONFIG.Levels.UI.templateElevation = toggle;
if (toggle) CONFIG.Levels.UI.elevationDialog(templateTool);
else CONFIG.Levels.UI.nextTemplateHeight = undefined;
},
};
CONFIG.Levels.UI._levelsTemplateTool = templateTool;
controls.find((c) => c.name == "token").tools.push(templateTool);
});
Hooks.once("canvasReady", () => {
console.log(
`%cLEVELS\n%cWelcome to the 3rd Dimension`,
"font-weight: bold;text-shadow: 10px 10px 0px rgba(0,0,0,0.8), 20px 20px 0px rgba(0,0,0,0.6), 30px 30px 0px rgba(0,0,0,0.4);font-size:100px;background: #444; color: #d43f3f; padding: 2px 28px 0 2px; display: inline-block;",
"font-weight: bold;text-shadow: 2px 2px 0px rgba(0,0,0,0.8), 4px 4px 0px rgba(0,0,0,0.6), 6px 6px 0px rgba(0,0,0,0.4);font-size:20px;background: #444; color: #d43f3f; padding: 10px 27px; display: inline-block; margin-left: -30px"
);
});