|
import { getWallBounds,getSceneSettings,migrateData,getLevelsBounds,getAdvancedLighting,migrateTokenHeight } from "./utils.js";
|
|
|
|
const MODULE_ID = "wall-height";
|
|
|
|
class WallHeightUtils{
|
|
constructor(){
|
|
this._advancedVision = null;
|
|
this._currentTokenElevation = null;
|
|
this.isLevels = game.modules.get("levels")?.active ?? false;
|
|
this._isLevelsAutoCover = game.modules.get("levelsautocover")?.active ?? false;
|
|
this._autoLosHeight = false;
|
|
this._defaultTokenHeight = 6;
|
|
}
|
|
|
|
cacheSettings(){
|
|
this._autoLosHeight = game.settings.get(MODULE_ID, 'autoLOSHeight');
|
|
this._defaultTokenHeight = game.settings.get(MODULE_ID, 'defaultLosHeight');
|
|
this._blockSightMovement = game.settings.get(MODULE_ID, "blockSightMovement");
|
|
this._enableWallText = game.settings.get(MODULE_ID, "enableWallText");
|
|
this.schedulePerceptionUpdate();
|
|
}
|
|
|
|
get tokenElevation(){
|
|
return this._token?.document?.elevation ?? this.currentTokenElevation
|
|
}
|
|
|
|
set currentTokenElevation(elevation) {
|
|
let update = false;
|
|
const { advancedVision } = getSceneSettings(canvas.scene);
|
|
if (this._currentTokenElevation !== elevation) {
|
|
this._currentTokenElevation = elevation;
|
|
if (advancedVision) {
|
|
update = true;
|
|
}
|
|
}
|
|
if (this._advancedVision !== !!advancedVision) {
|
|
this._advancedVision = !!advancedVision;
|
|
update = true;
|
|
}
|
|
if (update) {
|
|
this.schedulePerceptionUpdate();
|
|
}
|
|
}
|
|
|
|
get currentTokenElevation(){
|
|
return this._currentTokenElevation;
|
|
}
|
|
|
|
schedulePerceptionUpdate(){
|
|
if (!canvas.ready) return;
|
|
canvas.perception.update({
|
|
|
|
initializeLighting: true,
|
|
initializeSounds: true,
|
|
initializeVision: true,
|
|
refreshLighting: true,
|
|
refreshSounds: true,
|
|
refreshTiles: true,
|
|
refreshVision: true,
|
|
}, true);
|
|
}
|
|
|
|
updateCurrentTokenElevation() {
|
|
const token = canvas.tokens.controlled.find(t => t.document.sight.enabled) ?? canvas.tokens.controlled[0];
|
|
if (!token && game.user.isGM) {
|
|
this.currentTokenElevation = null;
|
|
this._token = null;
|
|
} else if (token) {
|
|
this.currentTokenElevation = token.losHeight
|
|
this._token = token;
|
|
}
|
|
}
|
|
|
|
async setSourceElevationTop(document, value) {
|
|
if (document instanceof TokenDocument) return;
|
|
return await document.update({ "flags.levels.rangeTop": value });
|
|
}
|
|
|
|
getSourceElevationTop(document) {
|
|
if (document instanceof TokenDocument) return document.object.losHeight
|
|
return document.document.flags?.levels?.rangeTop ?? +Infinity;
|
|
}
|
|
|
|
async setSourceElevationBottom(document, value) {
|
|
if (document instanceof TokenDocument) return await document.update({ "elevation": bottom });
|
|
return await document.update({ "flags.levels.rangeBottom": value });
|
|
}
|
|
|
|
getSourceElevationBottom(document) {
|
|
if (document instanceof TokenDocument) return document.document.elevation;
|
|
return document.document.flags?.levels?.rangeBottom ?? -Infinity;
|
|
}
|
|
|
|
async setSourceElevationBounds(document, bottom, top) {
|
|
if (document instanceof TokenDocument) return await document.update({ "elevation": bottom });
|
|
return await document.update({ "flags.levels.rangeBottom": bottom, "flags.levels.rangeTop": top });
|
|
}
|
|
|
|
getSourceElevationBounds(document) {
|
|
if (document instanceof TokenDocument) {
|
|
const bottom = document.document.elevation;
|
|
const top = document.object
|
|
? document.object.losHeight
|
|
: bottom;
|
|
return { bottom, top };
|
|
}
|
|
return getLevelsBounds(document);
|
|
}
|
|
|
|
async setSourceElevationBounds(document, bottom, top) {
|
|
if (document instanceof TokenDocument) return await document.update({ "elevation": bottom });
|
|
return await document.update({ "flags.levels.rangeBottom": bottom, "flags.levels.rangeTop": top });
|
|
}
|
|
|
|
getSourceElevationBounds(document) {
|
|
if (document instanceof TokenDocument) {
|
|
const bottom = document.elevation;
|
|
const top = document.object
|
|
? document.object.losHeight
|
|
: bottom;
|
|
return { bottom, top };
|
|
}
|
|
return getLevelsBounds(document);
|
|
}
|
|
|
|
async removeOneToWalls(scene){
|
|
if(!scene) scene = canvas.scene;
|
|
const walls = Array.from(scene.walls);
|
|
const updates = [];
|
|
for(let wall of walls){
|
|
const oldTop = wall.document.flags?.["wall-height"]?.top;
|
|
if(oldTop != null && oldTop != undefined){
|
|
const newTop = oldTop - 1;
|
|
updates.push({_id: wall.id, "flags.wall-height.top": newTop});
|
|
}
|
|
}
|
|
if(updates.length <= 0) return false;
|
|
await scene.updateEmbeddedDocuments("Wall", updates);
|
|
ui.notifications.notify("Wall Height - Added +1 to " + updates.length + " walls in scene " + scene.name);
|
|
return true;
|
|
}
|
|
|
|
async migrateTokenHeight(){
|
|
return await migrateTokenHeight();
|
|
}
|
|
|
|
async migrateData(scene){
|
|
return await migrateData(scene);
|
|
}
|
|
|
|
async migrateCompendiums (){
|
|
let migratedScenes = 0;
|
|
const compendiums = Array.from(game.packs).filter(p => p.documentName === 'Scene');
|
|
for (const compendium of compendiums) {
|
|
const scenes = await compendium.getDocuments();
|
|
for(const scene of scenes){
|
|
const migrated = await migrateData(scene);
|
|
if(migrated) migratedScenes++;
|
|
}
|
|
}
|
|
if(migratedScenes > 0){
|
|
ui.notifications.notify(`Wall Height - Migrated ${migratedScenes} scenes to new Wall Height data structure.`);
|
|
console.log(`Wall Height - Migrated ${migratedScenes} scenes to new Wall Height data structure.`);
|
|
}else{
|
|
ui.notifications.notify(`Wall Height - No scenes to migrate.`);
|
|
console.log(`Wall Height - No scenes to migrate.`);
|
|
}
|
|
return migratedScenes;
|
|
}
|
|
|
|
async migrateScenes (){
|
|
const scenes = Array.from(game.scenes);
|
|
let migratedScenes = 0;
|
|
ui.notifications.warn("Wall Height - Migrating all scenes, do not refresh the page!");
|
|
for(const scene of scenes){
|
|
const migrated = await migrateData(scene);
|
|
if(migrated) migratedScenes++;
|
|
}
|
|
if(migratedScenes > 0){
|
|
ui.notifications.notify(`Wall Height - Migrated ${migratedScenes} scenes to new Wall Height data structure.`);
|
|
console.log(`Wall Height - Migrated ${migratedScenes} scenes to new Wall Height data structure.`);
|
|
}else{
|
|
ui.notifications.notify(`Wall Height - No scenes to migrate.`);
|
|
console.log(`Wall Height - No scenes to migrate.`);
|
|
}
|
|
return migratedScenes;
|
|
}
|
|
|
|
async migrateAll(){
|
|
ui.notifications.error(`Wall Height - WARNING: The new data structure requires Better Roofs, Levels and 3D Canvas and Token Attacher to be updated!`);
|
|
await WallHeight.migrateScenes();
|
|
await WallHeight.migrateCompendiums();
|
|
ui.notifications.notify(`Wall Height - Migration Complete.`);
|
|
await game.settings.set(MODULE_ID, 'migrateOnStartup', false);
|
|
}
|
|
|
|
async setWallBounds(bottom, top, walls){
|
|
if(!walls) walls = canvas.walls.controlled.length ? canvas.walls.controlled : canvas.walls.placeables;
|
|
walls instanceof Array || (walls = [walls]);
|
|
const updates = [];
|
|
for(let wall of walls){
|
|
updates.push({_id: wall.id, "flags.wall-height.top": top, "flags.wall-height.bottom": bottom});
|
|
}
|
|
return await canvas.scene.updateEmbeddedDocuments("Wall", updates);
|
|
}
|
|
|
|
getWallBounds(wall){
|
|
return getWallBounds(wall);
|
|
}
|
|
|
|
addBoundsToRays(rays, token) {
|
|
if (token) {
|
|
const bottom = token.document.elevation;
|
|
const top = WallHeight._blockSightMovement ? token.losHeight : token.document.elevation;
|
|
for (const ray of rays) {
|
|
ray.A.b = bottom;
|
|
ray.A.t = top;
|
|
}
|
|
}
|
|
return rays;
|
|
}
|
|
|
|
}
|
|
|
|
export function registerWrappers() {
|
|
globalThis.WallHeight = new WallHeightUtils();
|
|
|
|
if(!globalThis.WallHeight.isLevels){
|
|
Object.defineProperty(AmbientLightDocument.prototype, "elevation", {
|
|
get: function () {
|
|
return this.flags?.levels?.rangeBottom ?? canvas.primary.background.elevation;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function tokenOnUpdate(wrapped, ...args) {
|
|
wrapped(...args);
|
|
|
|
updateTokenSourceBounds(this);
|
|
}
|
|
|
|
function updateTokenSourceBounds(token) {
|
|
const { advancedVision } = getSceneSettings(token.scene);
|
|
const losHeight = token.losHeight;
|
|
const sourceId = token.sourceId;
|
|
if (!advancedVision) {
|
|
if (canvas.effects.visionSources.has(sourceId)) {
|
|
token.vision.los.origin.b = token.vision.los.origin.t = losHeight;
|
|
}
|
|
if (canvas.effects.lightSources.has(sourceId)) {
|
|
token.light.los.origin.b = token.light.los.origin.t = losHeight;
|
|
}
|
|
} else if (canvas.effects.visionSources.has(sourceId) && (token.vision.los.origin.b !== losHeight || token.vision.los.origin.t !== losHeight)
|
|
|| canvas.effects.lightSources.has(sourceId) && (token.light.los.origin.b !== losHeight || token.light.los.origin.t !== losHeight)) {
|
|
token.updateSource({ defer: true });
|
|
canvas.perception.update({
|
|
initializeLighting: true,
|
|
initializeSounds: true,
|
|
initializeVision: true,
|
|
refreshLighting: true,
|
|
refreshSounds: true,
|
|
refreshTiles: true,
|
|
refreshVision: true,
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
function testWallInclusion(wrapped, ...args) {
|
|
if (!wrapped(...args)) return false;
|
|
const wall = args[0]
|
|
const { advancedVision } = getSceneSettings(wall.scene);
|
|
if (!advancedVision) return true;
|
|
const { top, bottom } = getWallBounds(wall);
|
|
const b = this.config?.source?.object?.b ?? this.origin?.object?.b ?? -Infinity;
|
|
const t = this.config?.source?.object?.t ?? this.origin?.object?.t ?? +Infinity;
|
|
return b >= bottom && t <= top;
|
|
}
|
|
|
|
function isDoorVisible(wrapped, ...args) {
|
|
const wall = this.wall;
|
|
const { advancedVision } = getSceneSettings(wall.scene);
|
|
const isUI = CONFIG.Levels?.UI?.rangeEnabled && !canvas?.tokens?.controlled[0];
|
|
const elevation = WallHeight.currentTokenElevation//isUI && !canvas.tokens.controlled[0] ? WallHeight.currentTokenElevation : WallHeight._token?.document?.elevation;
|
|
if (elevation == null || !advancedVision) return wrapped(...args);
|
|
const {top, bottom} = getWallBounds(wall);
|
|
let inRange = elevation >= bottom && elevation <= top;
|
|
if (isUI) inRange = elevation >= bottom && elevation < top;
|
|
//if (elevation < bottom || elevation > top) return false;
|
|
return wrapped(...args) && inRange;
|
|
}
|
|
|
|
function setSourceElevation(wrapped, origin, config = {}, ...args) {
|
|
|
|
let bottom = -Infinity;
|
|
let top = +Infinity;
|
|
const object = config.source?.object ?? origin.object;
|
|
if (origin.b == undefined && origin.t == undefined) {
|
|
if (object instanceof Token) {
|
|
if (config.type !== "move") {
|
|
bottom = top = object.losHeight;
|
|
} else {
|
|
bottom = object.document.elevation;
|
|
top = WallHeight._blockSightMovement ? object.losHeight : bottom;
|
|
}
|
|
} else if (object instanceof AmbientLight || object instanceof AmbientSound) {
|
|
if (getAdvancedLighting(object.document)) {
|
|
const bounds = getLevelsBounds(object.document)//WallHeight.getElevation(object.document);
|
|
bottom = bounds.bottom;
|
|
top = bounds.top;
|
|
} else {
|
|
bottom = WallHeight.currentTokenElevation;
|
|
if (bottom == null) {
|
|
bottom = -Infinity;
|
|
top = +Infinity;
|
|
} else {
|
|
top = bottom;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(object){
|
|
object.b = origin.b ?? config.b ?? bottom;
|
|
object.t = origin.t ?? config.t ?? top;
|
|
}
|
|
return wrapped(origin, config, ...args);
|
|
}
|
|
|
|
function pointSourceInitialize(wrapped, ...args) {
|
|
if(this.object) args[0].elevation = this.object.losHeight ?? this.object.document.elevation;
|
|
return wrapped(...args);
|
|
}
|
|
|
|
function drawWallRange(wrapped, ...args) {
|
|
const { advancedVision } = getSceneSettings(canvas.scene);
|
|
const bounds = getWallBounds(this);
|
|
if(!this.line && !WallHeight._enableWallText || !advancedVision || (bounds.top == Infinity && bounds.bottom == -Infinity)) {
|
|
if(this.line) this.line.children = this.line.children.filter(c => c.name !== "wall-height-text");
|
|
return wrapped(...args);
|
|
|
|
}
|
|
wrapped(...args);
|
|
const style = CONFIG.canvasTextStyle.clone();
|
|
style.fontSize /= 1.5;
|
|
style.fill = this._getWallColor();
|
|
if(bounds.top == Infinity) bounds.top = "Inf";
|
|
if(bounds.bottom == -Infinity) bounds.bottom = "-Inf";
|
|
const range = `${bounds.top} / ${bounds.bottom}`;
|
|
const oldText = this.line.children.find(c => c.name === "wall-height-text");
|
|
const text = oldText ?? new PreciseText(range, style);
|
|
text.text = range;
|
|
text.name = "wall-height-text";
|
|
text.interactiveChildren = false;
|
|
let angle = (Math.atan2( this.coords[3] - this.coords[1], this.coords[2] - this.coords[0] ) * ( 180 / Math.PI ));
|
|
angle = (angle+90)%180 - 90;
|
|
text.position.set(this.center.x, this.center.y);
|
|
text.anchor.set(0.5, 0.5);
|
|
text.angle = angle;
|
|
if(!oldText) this.line.addChild(text)//this.addChild(text);
|
|
}
|
|
|
|
Hooks.on("updateToken", () => {
|
|
WallHeight.updateCurrentTokenElevation();
|
|
});
|
|
|
|
Hooks.on("controlToken", () => {
|
|
WallHeight.updateCurrentTokenElevation();
|
|
});
|
|
|
|
Hooks.on("updateScene", (doc, change) => {
|
|
WallHeight.updateCurrentTokenElevation();
|
|
});
|
|
|
|
Hooks.on("canvasInit", () => {
|
|
WallHeight._advancedVision = null;
|
|
WallHeight._currentTokenElevation = null;
|
|
WallHeight._token = null;
|
|
});
|
|
|
|
Hooks.on("canvasReady", () => {
|
|
WallHeight.updateCurrentTokenElevation();
|
|
});
|
|
|
|
Hooks.on("updateWall", (wall, updates) => {
|
|
if(updates.flags && updates.flags[MODULE_ID]) {
|
|
WallHeight.schedulePerceptionUpdate();
|
|
}
|
|
if(canvas.walls.active) wall.object.refresh();
|
|
})
|
|
|
|
Hooks.on("activateWallsLayer", () => {
|
|
canvas.walls.placeables.forEach(w => w.refresh());
|
|
});
|
|
|
|
libWrapper.register(MODULE_ID, "DoorControl.prototype.isVisible", isDoorVisible, "MIXED");
|
|
|
|
libWrapper.register(MODULE_ID, "CONFIG.Token.objectClass.prototype._onUpdate", tokenOnUpdate, "WRAPPER");
|
|
|
|
libWrapper.register(MODULE_ID, "ClockwiseSweepPolygon.prototype._testWallInclusion", testWallInclusion, "WRAPPER", { perf_mode: "FAST" });
|
|
|
|
libWrapper.register(MODULE_ID, "ClockwiseSweepPolygon.prototype.initialize", setSourceElevation, "WRAPPER");
|
|
|
|
libWrapper.register(MODULE_ID, "RenderedPointSource.prototype._initialize", pointSourceInitialize, "WRAPPER");
|
|
|
|
libWrapper.register(MODULE_ID, "Wall.prototype.refresh", drawWallRange, "WRAPPER");
|
|
|
|
libWrapper.register(
|
|
MODULE_ID,
|
|
"VisionSource.prototype.elevation",
|
|
function () {
|
|
return this.object?.losHeight ?? canvas.primary.background.elevation;
|
|
},
|
|
libWrapper.OVERRIDE,
|
|
{ perf_mode: libWrapper.PERF_FAST }
|
|
);
|
|
|
|
libWrapper.register(
|
|
MODULE_ID,
|
|
"LightSource.prototype.elevation",
|
|
function () {
|
|
return this.object instanceof Token
|
|
? this.object.losHeight
|
|
: this.object?.document?.elevation ?? canvas.primary.background.elevation;
|
|
},
|
|
libWrapper.OVERRIDE,
|
|
{ perf_mode: libWrapper.PERF_FAST }
|
|
);
|
|
}
|