All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

630 lines
24 KiB

import { injectConfig } from "./lib/injectConfig.js";
import { TileHandler } from "./handlers/tileHandler.js";
import { RefreshHandler } from "./handlers/refreshHandler.js";
import { DrawingHandler } from "./handlers/drawingHandler.js";
import { UIHandler } from "./handlers/uiHandler.js";
import { SightHandler } from "./handlers/sightHandler.js";
import { LightHandler } from "./handlers/lightHandler.js";
import { SoundHandler } from "./handlers/soundHandler.js";
import { NoteHandler } from "./handlers/noteHandler.js";
import { TokenHandler } from "./handlers/tokenHandler.js";
import { TemplateHandler } from "./handlers/templateHandler.js";
import { FoWHandler } from "./handlers/fowHandler.js";
import { BackgroundHandler } from "./handlers/backgroundHandler.js";
import { SettingsHandler } from "./handlers/settingsHandler.js";
import { LevelsAPI } from "./API.js";
import { registerWrappers } from "./wrappers.js";
import { inRange, getRangeForDocument, cloneTileMesh, inDistance } from "./helpers.js";
import { setupWarnings } from "./warnings.js";
//warnings
Hooks.on("ready", () => {
if (!game.user.isGM) return;
setupWarnings();
const recommendedVersion = "10.291";
if (isNewerVersion(recommendedVersion, game.version)) {
ui.notifications.error(`Levels recommends Foundry VTT version ${recommendedVersion} or newer. Levels might not work as expected in the currently installed version (${game.version}).`, { permanent: true });
return;
}
});
Object.defineProperty(globalThis, "_levels", {
get: () => {
console.warn("Levels: _levels is deprecated. Use CONFIG.Levels.API instead.");
return CONFIG.Levels.API;
},
});
Object.defineProperty(TileDocument.prototype, "elevation", {
get: function () {
if (CONFIG.Levels?.UI?.rangeEnabled && !this.id) {
return parseFloat(CONFIG.Levels.UI.range[0] || 0);
}
return this.overhead ? this.flags?.levels?.rangeBottom ?? canvas.scene.foregroundElevation : canvas.primary.background.elevation;
},
});
Tile.prototype.inTriggeringRange = function (token) {
const bottom = this.document.elevation;
let top = this.document.flags?.levels?.rangeTop ?? Infinity;
if (game.Levels3DPreview?._active) {
const depth = this.document.flags?.["levels-3d-preview"]?.depth;
if (depth) top = bottom + (depth / canvas.scene.dimensions.size) * canvas.scene.dimensions.distance;
}
if (token) {
return token.document.elevation >= bottom && token.document.elevation <= top;
} else {
return { bottom, top };
}
};
Object.defineProperty(DrawingDocument.prototype, "elevation", {
get: function () {
if (CONFIG.Levels?.UI?.rangeEnabled && !this.id) {
return parseFloat(CONFIG.Levels.UI.range[0] || 0);
}
return this.flags?.levels?.rangeBottom ?? canvas.primary.background.elevation;
},
});
Object.defineProperty(NoteDocument.prototype, "elevation", {
get: function () {
return this.flags?.levels?.rangeBottom ?? canvas.primary.background.elevation;
},
});
Object.defineProperty(AmbientLightDocument.prototype, "elevation", {
get: function () {
if (CONFIG.Levels.UI.rangeEnabled && !this.id) {
return parseFloat(CONFIG.Levels.UI.range[0] || 0);
}
return this.flags?.levels?.rangeBottom ?? canvas.primary.background.elevation;
},
});
Object.defineProperty(AmbientSoundDocument.prototype, "elevation", {
get: function () {
if (CONFIG.Levels.UI.rangeEnabled && !this.id) {
return parseFloat(CONFIG.Levels.UI.range[0] || 0);
}
if (isNaN(this.flags?.levels?.rangeBottom)) return canvas.primary.background.elevation;
return (this.flags?.levels?.rangeBottom + (this.flags?.levels?.rangeTop ?? this.flags?.levels?.rangeBottom)) / 2;
},
});
Object.defineProperty(MeasuredTemplateDocument.prototype, "elevation", {
get: function () {
return this.flags?.levels?.elevation ?? canvas.primary.background.elevation;
},
});
Object.defineProperty(WeatherEffects.prototype, "elevation", {
get: function () {
return canvas?.scene?.flags?.levels?.weatherElevation ?? Infinity;
},
set: function (value) {
console.error("Cannot set elevation on WeatherEffects. Levels overrides WeatherEffects.prototype.elevation core behaviour. If you wish to set the WeatherEffects elevation, use SceneDocument.flags.levels.weatherElevation");
},
});
Hooks.on("init", () => {
const canvas3d = game.modules.get("levels-3d-preview")?.active;
CONFIG.Levels = {
MODULE_ID: "levels",
};
Object.defineProperty(CONFIG.Levels, "useCollision3D", {
get: function () {
return canvas3d && canvas.scene.flags["levels-3d-preview"]?.object3dSight;
},
});
Object.defineProperty(CONFIG.Levels, "currentToken", {
get: function () {
return this._currentToken;
},
set: function (value) {
this._currentToken = value;
Hooks.callAll("levelsPerspectiveChanged", this._currentToken);
},
});
CONFIG.Levels.handlers = {
TileHandler,
RefreshHandler,
DrawingHandler,
UIHandler,
SightHandler,
LightHandler,
SoundHandler,
NoteHandler,
TokenHandler,
TemplateHandler,
FoWHandler,
BackgroundHandler,
SettingsHandler,
};
CONFIG.Levels.helpers = {
inRange,
getRangeForDocument,
cloneTileMesh,
inDistance,
};
CONFIG.Levels.API = LevelsAPI;
CONFIG.Levels.UI = new LevelsUI();
CONFIG.Levels.settings = new SettingsHandler();
Hooks.callAll("levelsInit", CONFIG.Levels);
registerWrappers();
CONFIG.Levels.FoWHandler = new FoWHandler();
CONFIG.Levels.handlers.BackgroundHandler.setupElevation();
Hooks.callAll("levelsReady", CONFIG.Levels);
});
Hooks.once("ready", () => {
if (game.modules.get("levels-3d-preview")?.active) return;
// Module title
const MODULE_ID = CONFIG.Levels.MODULE_ID;
const MODULE_TITLE = game.modules.get(MODULE_ID).title;
const FALLBACK_MESSAGE_TITLE = MODULE_TITLE;
const FALLBACK_MESSAGE = `<large>
<p><strong>This module may be very complicated for a first timer, be sure to stop by my <a href="https://theripper93.com/">Discord</a> for help and support from the wonderful community as well as many resources</strong></p>
<p>Thanks to all the patreons supporting the development of this module making continued updates possible!</p>
<p>If you want to support the development of the module or get customized support in setting up your maps you can do so here : <a href="https://www.patreon.com/theripper93">Patreon</a> </p></large>
<p><strong>Patreons</strong> get also access to <strong>15+ premium modules</strong></p>
<p>Is Levels not enough? Go Full 3D</p>
<h1>3D Canvas</h1>
<iframe width="385" height="225" src="https://www.youtube.com/embed/hC1QGZFUhcU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<p>Check 3D Canvas and all my other <strong>15+ premium modules <a href="https://theripper93.com/">Here</a></strong></p>
<p>Special thanks to Baileywiki for the support and feedback and Blair for the amazing UI elements</p>`;
// Settings key used for the "Don't remind me again" setting
const DONT_REMIND_AGAIN_KEY = "popup-dont-remind-again-2";
// Dialog code
game.settings.register(MODULE_ID, DONT_REMIND_AGAIN_KEY, {
name: "",
default: false,
type: Boolean,
scope: "world",
config: false,
});
if (game.user.isGM && !game.settings.get(MODULE_ID, DONT_REMIND_AGAIN_KEY)) {
new Dialog({
title: FALLBACK_MESSAGE_TITLE,
content: FALLBACK_MESSAGE,
buttons: {
ok: { icon: '<i class="fas fa-check"></i>', label: "Understood" },
dont_remind: {
icon: '<i class="fas fa-times"></i>',
label: "Don't remind me again",
callback: () => game.settings.set(MODULE_ID, DONT_REMIND_AGAIN_KEY, true),
},
},
}).render(true);
}
});
Hooks.on("init", () => {
game.settings.register(CONFIG.Levels.MODULE_ID, "tokenElevScale", {
name: game.i18n.localize("levels.settings.tokenElevScale.name"),
hint: game.i18n.localize("levels.settings.tokenElevScale.hint"),
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "tokenElevScaleMultiSett", {
name: game.i18n.localize("levels.settings.tokenElevScaleMultiSett.name"),
hint: game.i18n.localize("levels.settings.tokenElevScaleMultiSett.hint"),
scope: "world",
config: true,
type: Number,
range: {
min: 0.1,
max: 2,
step: 0.1,
},
default: 1,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "fogHiding", {
name: game.i18n.localize("levels.settings.fogHiding.name"),
hint: game.i18n.localize("levels.settings.fogHiding.hint"),
scope: "world",
config: true,
type: Boolean,
default: true,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "revealTokenInFog", {
name: game.i18n.localize("levels.settings.revealTokenInFog.name"),
hint: game.i18n.localize("levels.settings.revealTokenInFog.hint"),
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "lockElevation", {
name: game.i18n.localize("levels.settings.lockElevation.name"),
hint: game.i18n.localize("levels.settings.lockElevation.hint"),
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "hideElevation", {
name: game.i18n.localize("levels.settings.hideElevation.name"),
hint: game.i18n.localize("levels.settings.hideElevation.hint"),
scope: "world",
config: true,
type: Number,
choices: {
0: game.i18n.localize("levels.settings.hideElevation.opt0"),
1: game.i18n.localize("levels.settings.hideElevation.opt1"),
2: game.i18n.localize("levels.settings.hideElevation.opt2"),
},
default: 0,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "enableTooltips", {
name: game.i18n.localize("levels.settings.enableTooltips.name"),
hint: game.i18n.localize("levels.settings.enableTooltips.hint"),
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "preciseTokenVisibility", {
name: game.i18n.localize("levels.settings.preciseTokenVisibility.name"),
hint: game.i18n.localize("levels.settings.preciseTokenVisibility.hint"),
scope: "world",
config: true,
type: Boolean,
default: true,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
game.settings.register(CONFIG.Levels.MODULE_ID, "exactTokenVisibility", {
name: game.i18n.localize("levels.settings.exactTokenVisibility.name"),
hint: game.i18n.localize("levels.settings.exactTokenVisibility.hint"),
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: () => {
CONFIG.Levels.settings.cacheSettings();
},
});
});
Hooks.on("updateTile", (tile, updates) => {
if (updates?.flags?.levels?.allWallBlockSight !== undefined) {
canvas.walls.placeables.forEach((w) => w.identifyInteriorState());
WallHeight.schedulePerceptionUpdate();
}
});
Hooks.on("renderTileConfig", (app, html, data) => {
const isInjected = html.find(`input[name="flags.${CONFIG.Levels.MODULE_ID}.rangeTop"]`).length > 0;
if (isInjected) return;
const injHtml = injectConfig.inject(app, html, {
moduleId: "levels",
tab: {
name: "levels",
label: "Levels",
icon: "fas fa-layer-group",
},
rangeTop: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeTop.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "Infinity",
step: "any",
},
rangeBottom: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeBottom.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "-Infinity",
step: "any",
},
showIfAbove: {
type: "checkbox",
label: game.i18n.localize("levels.tileconfig.showIfAbove.name"),
notes: game.i18n.localize("levels.tileconfig.showIfAbove.hint"),
},
showAboveRange: {
type: "number",
label: game.i18n.localize("levels.tileconfig.showAboveRange.name"),
notes: game.i18n.localize("levels.tileconfig.showAboveRange.hint"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "Infinity",
},
noCollision: {
type: "checkbox",
label: game.i18n.localize("levels.tileconfig.noCollision.name"),
notes: game.i18n.localize("levels.tileconfig.noCollision.hint"),
},
noFogHide: {
type: "checkbox",
label: game.i18n.localize("levels.tileconfig.noFogHide.name"),
notes: game.i18n.localize("levels.tileconfig.noFogHide.hint"),
},
isBasement: {
type: "checkbox",
label: game.i18n.localize("levels.tileconfig.isBasement.name"),
notes: game.i18n.localize("levels.tileconfig.isBasement.hint"),
},
allWallBlockSight: {
type: "checkbox",
label: game.i18n.localize("levels.tileconfig.allWallBlockSight.name"),
notes: game.i18n.localize("levels.tileconfig.allWallBlockSight.hint"),
default: true,
},
});
injHtml.find(`input[name="flags.${CONFIG.Levels.MODULE_ID}.rangeTop"]`).closest(".form-group").before(`
<p class="notes" style="color: red" id="no-overhead-warning">${game.i18n.localize("levels.tileconfig.noOverhead")}</>
<p class="notes" style="color: red" id="occlusion-none-warning">${game.i18n.localize("levels.tileconfig.occlusionNone")}</>
`);
html.on("change", "input", (e) => {
const isOverhead = html.find(`input[name="overhead"]`).is(":checked");
const occlusionMode = html.find(`select[name="occlusion.mode"]`).val();
const isShowIfAbove = injHtml.find(`input[name="flags.levels.showIfAbove"]`).is(":checked");
injHtml.find("input").prop("disabled", !isOverhead);
injHtml.find("input[name='flags.levels.showAboveRange']").closest(".form-group").toggle(isShowIfAbove);
html.find("#no-overhead-warning").toggle(!isOverhead);
html.find("#occlusion-none-warning").toggle(occlusionMode == 0);
app.setPosition({ height: "auto" });
});
html.find(`input[name="overhead"]`).trigger("change");
app.setPosition({ height: "auto" });
});
Hooks.on("renderAmbientLightConfig", (app, html, data) => {
const injHtml = injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'input[name="config.dim"]',
rangeTop: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeTop.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "Infinity",
step: "any",
},
rangeBottom: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeBottom.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "-Infinity",
step: "any",
},
});
});
Hooks.on("renderNoteConfig", (app, html, data) => {
const injHtml = injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'select[name="textAnchor"]',
rangeTop: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeTop.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "Infinity",
step: "any",
},
rangeBottom: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeBottom.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "-Infinity",
step: "any",
},
});
});
Hooks.on("renderAmbientSoundConfig", (app, html, data) => {
const injHtml = injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'input[name="radius"]',
rangeTop: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeTop.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "Infinity",
step: "any",
},
rangeBottom: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeBottom.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "-Infinity",
step: "any",
},
});
});
Hooks.on("renderDrawingConfig", (app, html, data) => {
const injHtml = injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'input[name="z"]',
drawingMode: {
type: "select",
label: game.i18n.localize("levels.drawingconfig.isHole.name"),
default: 0,
dType: "Number",
options: {
0: game.i18n.localize("levels.drawingconfig.isHole.opt0"),
2: game.i18n.localize("levels.drawingconfig.isHole.opt2"),
21: game.i18n.localize("levels.drawingconfig.isHole.opt21"),
22: game.i18n.localize("levels.drawingconfig.isHole.opt22"),
3: game.i18n.localize("levels.drawingconfig.isHole.opt3"),
},
},
elevatorFloors: {
type: "text",
label: game.i18n.localize("levels.drawingconfig.elevatorFloors.name"),
notes: game.i18n.localize("levels.drawingconfig.elevatorFloors.hint"),
},
rangeTop: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeTop.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "Infinity",
step: "any",
},
rangeBottom: {
type: "number",
label: game.i18n.localize("levels.tileconfig.rangeBottom.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: "",
placeholder: "-Infinity",
step: "any",
},
});
});
Hooks.on("renderMeasuredTemplateConfig", (app, html, data) => {
const injHtml = injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'input[name="width"]',
elevation: {
type: "text",
dType: "Number",
label: game.i18n.localize("levels.template.elevation.name"),
units: game.i18n.localize("levels.tileconfig.range.unit"),
default: Infinity,
step: "any",
},
special: {
type: "number",
label: game.i18n.localize("levels.template.depth.name"),
default: 0,
dType: "Number",
},
});
});
Hooks.on("renderDrawingHUD", (data, hud, drawData) => {
let drawing = data.object.document;
if (drawing.getFlag(CONFIG.Levels.MODULE_ID, "drawingMode")) {
let active = drawing.getFlag(CONFIG.Levels.MODULE_ID, "stairLocked") || false;
let toggleStairbtn = `<div class="control-icon${active ? " active" : ""}" id="toggleStair">
<i class="fas fa-lock" width="36" height="36" title='${game.i18n.localize("levels.drawingHud.title")}'></i>
</div>`;
const controlIcons = hud.find("div.control-icon");
controlIcons.last().after(toggleStairbtn);
$(hud.find(`div[id="toggleStair"]`)).on("click", test);
function test() {
console.log("test");
active = !active;
drawing.setFlag(CONFIG.Levels.MODULE_ID, "stairLocked", !(drawing.getFlag(CONFIG.Levels.MODULE_ID, "stairLocked") || false));
let hudbtn = hud.find(`div[id="toggleStair"]`);
if (active) hudbtn.addClass("active");
else hudbtn.removeClass("active");
}
}
});
Hooks.on("renderTokenHUD", (data, hud, drawData) => {
if (CONFIG.Levels.settings.get("lockElevation") && !game.user.isGM) {
const controlIcons = hud.find(`div[class="attribute elevation"]`);
$(controlIcons[0]).remove();
}
});
Hooks.on("preCreateMeasuredTemplate", (template) => {
const templateData = CONFIG.Levels.handlers.TemplateHandler.getTemplateData();
if (template.flags?.levels?.elevation) return;
template.updateSource({
flags: { levels: { elevation: templateData.elevation, special: templateData.special } },
});
});
Hooks.on("renderSceneConfig", (app, html, data) => {
injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'input[name="foregroundElevation"]',
backgroundElevation: {
type: "number",
label: game.i18n.localize("levels.sceneconfig.backgroundElevation.name"),
notes: game.i18n.localize("levels.sceneconfig.backgroundElevation.notes"),
default: 0,
},
});
injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'select[name="weather"]',
weatherElevation: {
type: "number",
label: game.i18n.localize("levels.sceneconfig.weatherElevation.name"),
notes: game.i18n.localize("levels.sceneconfig.weatherElevation.notes"),
default: "",
placeholder: "Infinity",
},
});
injectConfig.inject(app, html, {
moduleId: "levels",
inject: 'input[name="fogExploration"]',
lightMasking: {
type: "checkbox",
label: game.i18n.localize("levels.sceneconfig.lightMasking.name"),
notes: game.i18n.localize("levels.sceneconfig.lightMasking.notes"),
default: true,
},
});
});