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.
 
 
 

428 lines
15 KiB

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 }
);
}