Prodigy Tree Viewer

View gameobject trees within Prodigy.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Prodigy Tree Viewer
// @namespace    https://hipposgrumm.dev/
// @version      1.0.0
// @description  View gameobject trees within Prodigy.
// @author       Hipposgrumm
// @license      gpl-3.0
// @match        *://*.prodigygame.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=prodigygame.com
// @grant        none
// @run-at       document-end
// ==/UserScript==

/*jshint esversion: 11 */
(function() {
    let gametype = 0;
    if (window.Boot) gametype = 1; // Math
    else if (window.GameFramework?.FrameworkApp) gametype = 2; // English
    else return; // Can't do anything since there's no game world.

    let gameaccess;

    let css = `
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,300,400,500,600,700" rel="stylesheet" type="text/css" async>
<style>
/* https://www.w3schools.com/howto/howto_js_treeview.asp */
.tree ul {
    font-size: 14px;
    list-style-type: none;
    padding-inline-start: 20px;
    user-select: none; /* Prevent text selection */
}
.treename {
    cursor: pointer;
    display: inline-block;
    min-width: 10em;
    padding: 3px;
    border-radius: 5px;
}
.treename.hasgameobj {
    color: #DB0;
}
.treename.onlygameobj {
    color: #B90;
}
.treename:hover {
    background-color: #8884;
}
.treename:active {
    background-color: #8882;
}
.selected > .treename {
    background-color: #8888;
}
.caret {
    cursor: pointer;
}
.caret::before {
    content: "\u25B6";
    display: inline-block;
    margin-right: 6px;
}
.makeinvisible::before {
    color: transparent;
    cursor: auto;
}
.caret-down::before {
    transform: rotate(90deg);
}
</style>
<style>
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-thumb {
  background: #5F606A;
}
::-webkit-scrollbar-thumb:hover {
  background: #4F505A;
  cursor: pointer;
}
body {
    display: flex;
    font-family: "Open Sans";
    color: #DDD;
    background-color: #1A1A1E;
}
input {
    font-family: "Open Sans";
    color: #E5E5E5;
    background-color: #363638;
    border-color: #444;
    border-radius: 5px;
}
.hidden {
  display: none;
}
div.tree {
    height: calc(100vh - 40px);
    background-color: #242429;
    overflow: auto;
    white-space: nowrap;
    padding: 10px;
    border-radius: 10px;
}
div.splitter {
    width: 10px;
    cursor: col-resize;
}
div.dataview {
    background-color: #202023;
    flex: 1;
    padding: 10px;
    border-radius: 10px;
}
div.dataitem {
    display: grid;
    grid-template-columns: 250px 1fr;
    padding: 2px 10px;
    height: 25px;
}
.dataitem span {
    vertical-align: bottom;
    user-select: none;
}
.dataitem > span {
    vertical-align: middle;
    width: 250px;
    min-width: 50px;
}
.dataview div.sub {
    background-color: #242427;
    padding: 10px;
    border-radius: 10px;
}
button.degtoggle {
    background-color: #363638;
    border-color: #444;
    aspect-ratio: 1 / 1;
    height: 25px;
}
button.degtoggle.rad {
    background-image: url("data:image/svg+xml;base64,`+btoa(`<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" viewBox="0 0 500 500">
	<path d="M 71.235 164.76 L 55.596 164.695 C 55.581 164.696 68.346 113.835 89.175 88.947 C 106.336 68.108 131.312 61.924 149.8 61.7 L 444.154 61.724 L 444.2 114.6 L 354.67 114.631 C 354.669 120.296 343.483 203.929 339.545 264.172 C 336.436 311.736 334.283 367.164 380.158 371.445 C 418.882 373.364 427.109 335.185 427.683 322.619 L 442.8 322.6 C 439.3 365.492 419.567 433.28 357.153 432.362 C 308.314 429.455 293.292 398.763 287.689 363.431 C 278.473 329.546 306.818 121.136 304.62 114.733 L 206.6 114.7 C 207.422 115.05 193.43 261.761 187.564 293.505 C 183.956 324.61 174.05 370.806 161.744 395.17 C 145.189 429.984 121.953 435.333 105.175 428.222 C 77.642 414.754 81.181 388.386 90.763 374.381 C 96.317 367.136 123.771 328.393 131.818 309.928 C 142.793 288.981 151.542 249.678 153.494 236.773 C 159.835 216 167.064 115.489 166.297 115.387 L 130.092 115.44 C 111.64 115.885 97.442 126.385 88.196 139.247 C 87.33 138.9 71.158 164.683 71.2 164.7 L 71.235 164.76 Z" />
</svg>`)+`")
}
button.degtoggle.deg {
    background-image: url("data:image/svg+xml;base64,`+btoa(`<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" viewBox="0 0 500 500">
	<path d="M 159.139 277.589 C 210.158 294.834 247.443 340.673 243.385 399.989 L 288.465 398.242 C 291.281 358.175 261.569 271.201 184.684 242.627 L 159.139 277.589 Z" />
	<path d="M 250.213 80.675 L 60.809 408.222 L 60.829 419.278 L 439.19 419.325 L 439.1 377.9 L 127.222 377.894 L 290.819 106.63 L 250.213 80.675 Z" />
</svg>`)+`")
}
</style>
`;
    let DEG2RAD = Math.PI / 180;
    let RAD2DEG = 180 / Math.PI;
    /// An abstract container that can be used to modify objects that ignores the descrepancies between GameObject types
    class ManipulatableObject {
        /// Creates inner classes for this class
        static createInnerClasses() {
            /// Wrapper class for XY coordinates
            ManipulatableObject.Coord = class {
                constructor(data) {
                    this.data = data;
                }

                get x() { return this.data.x; }
                set x(val) { this.data.x = val; }
                get y() { return this.data.y; }
                set y(val) { this.data.y = val; }
            };
            /// Contains all data for this object's management in the DOM.
            ManipulatableObject.ElementData = class {
                constructor(obj, list) {
                    this._obj = obj;
                    this._list = list;
                    this._item = document.createElement('li');

                    this._caret = document.createElement('span');
                    this._caret.classList.add("caret");
                    this._caret.addEventListener("click", () => this.doToggle());
                    this._caret.classList.add("makeinvisible");
                    this._caretHidden = true;
                    this._item.appendChild(this._caret);

                    if (obj.active != null) {
                        this._checkbox = document.createElement('input');
                        this._checkbox.type = "checkbox";
                        this._checkbox.addEventListener("change", () => {
                            obj.active = this._checkbox.checked;
                        });
                        this._checkbox.checked = obj.active;
                        this._item.appendChild(this._checkbox);
                    }

                    this._nametext = " ";
                    this._txt = document.createElement('span');
                    this._txt.classList.add("treename");
                    if (!obj.treeObject) this._txt.classList.add("onlygameobj");
                    else if (obj.pixiObject) this._txt.classList.add("hasgameobj");
                    this._txt.addEventListener("click", () => openObject(obj));
                    this._item.appendChild(this._txt);

                    this._childList = document.createElement('ul');
                    this._childList.classList.add("hidden");
                    this._item.appendChild(this._childList);
                    this._hideChildren = true;
                }

                /// Expands/collapses this item's child list.
                doToggle() {
                    if (!this._childList) return;
                    this._hideChildren = !this._hideChildren;
                    if (this._hideChildren) {
                        this._childList.classList.add("hidden");
                        this._caret.classList.remove("caret-down");
                        this.updateChildren();
                    } else {
                        this._childList.classList.remove("hidden");
                        this._caret.classList.add("caret-down");
                    }
                }

                updateChildren(addition=null) {
                    if (this._caretHidden) {
                        if (this._obj.node.children.length > 0) {
                            this._caret.classList.remove("makeinvisible");
                            this._caretHidden = false;
                            return;
                        }
                    } else if (this._obj.node.children.length == 0) {
                        this._caret.classList.add("makeinvisible");
                        this._caretHidden = true;
                    }
                    if (!this.isChildrenVisible()) return; // if the children are hidden we don't need to worry about them right now
                    this._obj.node.sortChildren(addition);
                    if (!this._obj.node.sortingDirtyAccept()) return;
                    // https://stackoverflow.com/q/34685316
                    var elements = document.createDocumentFragment();
                    this._obj.node.children.forEach(child => {
                        elements.appendChild(child.object.element._item);
                    });
                    this._childList.innerHTML = null;
                    this._childList.appendChild(elements);
                }

                /// @returns {bool} Whether this item's list of children is collapsed or not
                get childrenHidden() { return this._hideChildren; }

                /// @returns {Object} List Element containing this object
                get list() { return this._list; }

                /// @returns {Object} ListItem Element representing this object
                get item() { return this._item; }

                /// @returns {Object} List Element for this object's children
                get childList() { return this._childList; }

                /// @returns {Object} Checkbox that determines if the GameObject is visible/active
                get checkbox() { return this._checkbox; }

                get txt() { return this._txt; }
                get txt_inner() { return this._nametext; }
                set txt_inner(val) {
                    this._nametext = val;
                    if (!val?.trim()) this._txt.innerHTML = "\u00A0"; // This prevents it from becoming flat.
                    else this._txt.innerHTML = val;
                }

                isChildrenVisible() {
                    let par = this._obj.node;
                    while (par) {
                        if (par.object.element._hideChildren) return false;
                        par = par.parent;
                    }
                    return true;
                }
            };
            delete ManipulatableObject.createInnerClasses;
        }

        constructor(data, list) {
            if (!data) throw new Error("Value passed to 'data' parameter of ManipulatableObject.new() is null or empty.");
            if (data instanceof ManipulatableObject) {
                this._treeobject = data._treeobject;
                this._pixiobject = data._pixiobject;
                this._data = data._data;
                this._typename = data._typename;
                this._pixiTransform = data._pixiTransform;
            } else {
                if (data.hasOwnProperty("_legacyTransform")) { // PIXI GameObject
                    this._treeobject = data.legacyTransform; // potential reference to Tree GameObject
                    this._pixiobject = data;
                } else if (data.transform) { // Tree GameObject
                    this._treeobject = data;
                    this._pixiobject = data.gameObjectRef; // potential reference to PIXI GameObject
                } else {
                    throw new Error("Object passed to 'data' parameter of ManipulatableObject.new() doesn't appear to be a GameObject.");
                }
                this._data = data; // Useful for referencing names common to both GameObject types
                this._typename = null;
                this._pixiTransform = this._treeobject ? this._treeobject.transform : this._pixiobject.transform?.pixiTransform;
            }
            if (this._pixiTransform) {
                this._transPos = new ManipulatableObject.Coord(this._pixiTransform.position);
                this._transScl = new ManipulatableObject.Coord(this._pixiTransform.scale);
                this._transSkw = new ManipulatableObject.Coord(this._pixiTransform.skew);
                this._transPiv = new ManipulatableObject.Coord(this._pixiTransform.pivot);
            }
            this._element = new ManipulatableObject.ElementData(this, list);
        }

        compare(other) {
            return this._treeobject == other._treeobject &&
                this._pixiobject == other._pixiobject;
        }

        get exists() {
            if (this._pixiobject) return !this._pixiobject.isDestroyed;
            else if (this._treeobject) return this._treeobject.exists;
            else return false;
        }
        get treeObject() { return this._treeobject; }
        get pixiObject() { return this._pixiobject; }
        get typename() { return this._typename; }
        get name() {
            if (!this._pixiobject && !this._data.hasOwnProperty("name")) return null;
            if (this._data.name) return this._data.name;
            else return "";
        }
        get active() {
            if (this._pixiobject) return this._pixiobject.active;
            else if (this._treeobject?.hasOwnProperty("visible")) {
                return this._treeobject.visible;
            } else return null;
        }
        set active(val) {
            if (this._pixiobject) this._pixiobject.active = val;
            else if (this._treeobject?.hasOwnProperty("visible")) {
                this._treeobject.visible = val;
            }
        }
        get pos() { return this._transPos; }
        get rot() { // Rotation in radians.
            return this._pixiTransform ? this._pixiTransform.rotation : null;
        }
        set rot(val) {
            if (this._pixiTransform) this._pixiTransform.rotation = val;
        }
        get scl() { return this._transScl; }
        get skw() { return this._transSkw; } // Skew in radians.
        get piv() { return this._transPiv; }

        get alpha() {
            if (!this._treeobject) return null;
            return this._treeobject.alpha;
        }
        set alpha(val) {
            if (this._treeobject) this._treeobject.alpha = val;
        }

        get element() { return this._element; }
        get node() { return this._node; }
    } ManipulatableObject.createInnerClasses();
    class ObjectHeirarchy {
        static createInnerClasses() {
            ObjectHeirarchy.Node = class {
                constructor(obj, heirarchy, parent) {
                    obj._node = this;
                    this._obj = obj;
                    this._heirarchy = heirarchy;
                    this._parent = parent;
                    this._siblingUp = null;
                    this._siblingDown = null;
                    this._children = [];
                    this._sortingDirty = true;
                    this._listTreeObjsLast = [];
                    this._listPixiObjsLast = [];
                }

                get object() { return this._obj; }
                get parent() { return this._parent; }
                get siblingUp() { return this._siblingUp; }
                get siblingDown() { return this._siblingDown; }
                get children() { return this._children; }
                sortingDirtyAccept() {
                    if (!this._sortingDirty) return false;
                    this._sortingDirty = false;
                    return true;
                }

                sortChildren(addition=null) {
                    let listTreeObjs = this._obj.treeObject?.children ?? [];
                    let listPixiObjs = this._obj.pixiObject?.children ?? [];
                    if (addition) {
                        if (addition._obj.treeObject) this._listTreeObjsLast.push(addition._obj.treeObject);
                        if (addition._obj.pixiObject) this._listPixiObjsLast.push(addition._obj.pixiObject);
                        this._obj.element.childList.appendChild(addition._obj.element.item);
                    }
                    if (
                        this._listTreeObjsLast.length == listTreeObjs.length &&
                        this._listTreeObjsLast.every((e,i) => e == listTreeObjs[i]) &&
                        this._listPixiObjsLast.length == listPixiObjs.length &&
                        this._listPixiObjsLast.every((e,i) => e == listPixiObjs[i])
                    ) return;
                    this._listTreeObjsLast = Array.from(listTreeObjs);
                    this._listPixiObjsLast = Array.from(listPixiObjs);

                    let items = [], extras = [];
                    this._children.forEach(e => {
                        let added = false;
                        e._tmp_treeInd = e._obj.treeObject ? listTreeObjs.indexOf(e._obj.treeObject) : -1;
                        if (e._tmp_treeInd >= 0) {
                            items.push(e);
                            added = true;
                        }
                        e._tmp_pixiInd = e._obj.pixiObject ? listPixiObjs.indexOf(e._obj.pixiObject) : -1;
                        if (!added) {
                            if (e._tmp_pixiInd >= 0) {
                                items.push(e);
                            } else extras.push(e);
                        }
                    });

                    items.sort((a, b) => {
                        let ind1 = a._tmp_pixiInd;
                        let ind2 = b._tmp_pixiInd;
                        if (ind1 < 0 || ind2 < 0) return 0;
                        else return ind1-ind2;
                    });
                    items.sort((a, b) => {
                        let ind1 = a._tmp_treeInd;
                        let ind2 = b._tmp_treeInd;
                        if (ind1 < 0 || ind2 < 0) return 0;
                        else return ind1-ind2;
                    });
                    let newChildren = items.concat(extras);
                    newChildren.forEach((e, i) => {
                        e._siblingUp = (i > 0) ? newChildren[i-1] : null;
                        e._siblingDown = (i < (newChildren.length-1)) ? newChildren[i+1] : null;
                        delete e._tmp_treeInd; delete e._tmp_pixiInd;
                    });
                    let sorted = this._children.length != newChildren.length ||
                        !this._children.every((e,i) => e == newChildren[i]);

                    this._children = newChildren;
                    if (sorted) this._sortingDirty = true;
                }

                add(obj) {
                    let node = new ObjectHeirarchy.Node(obj, this._heirarchy, this);
                    if (obj.treeObject) {
                        this._heirarchy._treeObjectsKeys.push(obj.treeObject);
                        this._heirarchy._treeObjectsVals.push(node);
                    }
                    if (obj.pixiObject) {
                        this._heirarchy._pixiObjectsKeys.push(obj.pixiObject);
                        this._heirarchy._pixiObjectsVals.push(node);
                    }
                    this._heirarchy._allnodes.push(node);
                    if (this._children.length > 0) {
                        let lastChild = this._children[this._children.length-1];
                        lastChild._siblingDown = node;
                        node._siblingUp = lastChild;
                    }
                    this._children.push(node);
                    this._obj.element.updateChildren(node);
                }

                removeSelf(fromRecursion=false) {
                    this._children.forEach(c=>c.removeSelf(true));
                    let ind = this._heirarchy._allnodes.indexOf(this);
                    if (ind < 0) return;

                    if (this._siblingUp) this._siblingUp._siblingDown = this._siblingDown;
                    if (this._siblingDown) this._siblingDown._siblingUp = this._siblingUp;

                    this._heirarchy._allnodes.splice(ind, 1);
                    ind = this._heirarchy._treeObjectsVals.indexOf(this);
                    if (ind >= 0) {
                        this._heirarchy._treeObjectsKeys.splice(ind, 1);
                        this._heirarchy._treeObjectsVals.splice(ind, 1);
                    }
                    ind = this._heirarchy._pixiObjectsVals.indexOf(this);
                    if (ind >= 0) {
                        this._heirarchy._pixiObjectsKeys.splice(ind, 1);
                        this._heirarchy._pixiObjectsVals.splice(ind, 1);
                    }
                    if (!fromRecursion) {
                        this._obj.element.item.remove();
                        this._parent._children.splice(this._parent._children.indexOf(this), 1);
                    }
                }
            };
            delete ObjectHeirarchy.createInnerClasses;
        }

        constructor(obj) {
            this.add(obj, null);
        }

        add(obj, parent) {
            if (parent != null && !(parent instanceof ObjectHeirarchy.Node)) {
                if (!(parent instanceof ManipulatableObject)) throw new Error("Unusable 'parent' for adding object to heirarchy.");
                if (parent.treeObject) parent = this._treeObjectsVals[this._treeObjectsKeys.indexOf(parent.treeObject)];
                else if (parent.pixiObject) parent = this._pixiObjectsVals[this._pixiObjectsKeys.indexOf(parent.pixiObject)];
                else throw new Error("This ManipulatableObject is incapable of being a 'parent'.");
            }
            if (parent) parent.add(obj);
            else {
                this._root = new ObjectHeirarchy.Node(obj, this, null);
                if (obj.treeObject) {
                    this._treeObjectsKeys = [obj.treeObject];
                    this._treeObjectsVals = [this._root];
                } else {
                    this._treeObjectsKeys = [];
                    this._treeObjectsVals = [];
                }
                if (obj.pixiObject) {
                    this._pixiObjectsKeys = [obj.pixiObject];
                    this._pixiObjectsVals = [this._root];
                } else {
                    this._pixiObjectsKeys = [];
                    this._pixiObjectsVals = [];
                }
                this._allnodes = [this._root];
                obj.element.list.appendChild(obj.element.item);
            }
        }

        remove(obj) {
            if (!(obj instanceof ObjectHeirarchy.Node)) {
                if (obj instanceof ManipulatableObject) {
                    obj = this._allnodes.find(o => o.object == obj);
                } else throw new Error("Value of 'obj' is not a node, cannot remove it.");
            }
            obj.removeSelf();
        }
    } ObjectHeirarchy.createInnerClasses();

    let createDataitem = function(nameHTML, elementType, elementSetup, elementLayout="auto") {
        let div = document.createElement('div');
        let label = document.createElement('span');
        let vals = document.createElement('div');
        div.classList.add("dataitem");
        div.appendChild(label);
        div.appendChild(vals);
        label.innerHTML = nameHTML;
        vals.style.display = "inline-grid";
        vals.style["grid-template-columns"] = elementLayout;
        if (!Array.isArray(elementType)) {
            let val = document.createElement(elementType);
            if (!Array.isArray(elementSetup)) {
                if (elementSetup) elementSetup(val, [val]);
            } else if (elementSetup.length > 0) {
                if (elementSetup[0]) elementSetup[0](val, [val]);
            }
            vals.appendChild(val);
            return div;
        } else if (elementType.length == 0) return div;
        let elements = [];
        elementType.forEach(e => {
            let val = document.createElement(e);
            elements.push(val);
            vals.appendChild(val);
        });
        if (!Array.isArray(elementSetup)) {
            if (elementSetup) elementSetup(elements[0], Array.from(elements));
            return div;
        }
        elements.every((e, i) => {
            if (i >= elementSetup.length) return false;
            if (elementSetup[i]) elementSetup[i](e, Array.from(elements));
            return true;
        });
        return div;
    };
    let dataItemDegRad = function(dataviewData, modename, onSetDegrees, onSetRadians) {
        return (radDeg, elements) => {
            radDeg.classList.add("degtoggle");
            radDeg.addEventListener("click", () => {
                var mode = !dataviewData[modename];
                dataviewData[modename] = mode;
                if (mode) {
                    radDeg.title = "Mode: Degrees";
                    radDeg.classList.remove("rad");
                    radDeg.classList.add("deg");
                    onSetDegrees(dataviewData, elements);
                } else {
                    radDeg.title = "Mode: Radians";
                    radDeg.classList.add("rad");
                    radDeg.classList.remove("deg");
                    onSetRadians(dataviewData, elements);
                }
            });
            radDeg.title = "Mode: Degrees";
            radDeg.classList.add("deg");
            dataviewData[modename] = true;
        };
    };

    let treeDiv, dataview;
    let treeData = null, dataviewData = null;

    let selected = null;
    function openObject(obj) {
        if (obj instanceof ObjectHeirarchy.Node) obj = obj.object;

        if (selected) selected.element.item.classList.remove("selected");
        selected = obj;
        while (dataview.firstChild) dataview.firstChild.remove();
        dataviewData = {"current":{}};
        if (!selected) return;
        selected.element.item.classList.add("selected");

        {
            let par = obj.node.parent;
            while (par) {
                if (par.object.element.childrenHidden) par.object.element.doToggle();
                par = par.parent;
            }
        }

        {
            let treeRect = treeDiv.getBoundingClientRect();
            let itemRect = selected.element.txt.getBoundingClientRect();
            let scrollX = treeDiv.scrollLeft, scrollY = treeDiv.scrollTop;
            if (itemRect.left < treeRect.left) scrollX -= treeRect.left-itemRect.left;
            else if (itemRect.right > treeRect.right) scrollX += itemRect.right-treeRect.right;
            if (itemRect.top < treeRect.top) scrollY -= treeRect.top-itemRect.top;
            else if (itemRect.bottom > treeRect.bottom) scrollY += itemRect.bottom-treeRect.bottom;
            treeDiv.scroll(scrollX, scrollY);
        }

        dataview.appendChild(createDataitem("Class ID", 'input', text => {
            text.type = "text";
            text.readOnly = true;
            let clazz = Object.getPrototypeOf(obj._data);
            let path = clazz.constructor.name;
            while (true) {
                let par = Object.getPrototypeOf(clazz);
                if (!par) break;
                path = par.constructor.name+' '+path;
                clazz = par;
            }
            text.value = path;
        }));
        if (obj.name != null) {
            dataview.appendChild(createDataitem("Name", 'input', text => {
                text.type = "text";
                text.value = obj.name;
                text.readOnly = true;
            }));
        }
        if (obj.active != null) {
            dataview.appendChild(createDataitem("Is Active", 'input', check => {
                check.type = "checkbox";
                check.addEventListener("change", () => {
                    obj.element.checkbox.checked = obj.active = check.checked;
                });
                check.checked = obj.active;
                dataviewData.active = check;
            }));
        }
        if (obj._pixiTransform) {
            let transformDiv = document.createElement('div');
            transformDiv.classList.add("sub");
            transformDiv.appendChild(createDataitem("Position", ['span', 'input', 'span', 'input'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.posX = obj.pos.x = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.posX = obj.pos.x;
                    dataviewData.posX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.posY = obj.pos.y = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.posY = obj.pos.y;
                    dataviewData.posY = input;
                }
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            transformDiv.appendChild(createDataitem("Rotation", ['input', 'button'], [
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        let val = input.valueAsNumber;
                        if (dataviewData.rot_degmode) val *= DEG2RAD;
                        dataviewData.current.rot = obj.rot = val;
                    });
                    input.value = dataviewData.current.rot = obj.rot * RAD2DEG;
                    dataviewData.rot = input;
                }, dataItemDegRad(dataviewData, "rot_degmode", (dataviewData, elements) => {
                    elements[0].value = dataviewData.current.rot = obj.rot * RAD2DEG;
                    dataviewData.skwX.step = dataviewData.skwY.step = 1;
                }, (dataviewData, elements) => {
                    elements[0].value = dataviewData.current.rot = obj.rot;
                    dataviewData.skwX.step = dataviewData.skwY.step = DEG2RAD;
                })
            ], "minmax(0, 1fr) auto"));
            transformDiv.appendChild(createDataitem("Scale", ['span', 'input', 'span', 'input'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.sclX = obj.scl.x = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.sclX = obj.scl.x;
                    dataviewData.sclX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.sclY = obj.scl.y = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.sclY = obj.scl.y;
                    dataviewData.sclY = input;
                }
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            transformDiv.appendChild(createDataitem("Skew", ['span', 'input', 'span', 'input', 'button'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        let val = input.valueAsNumber;
                        if (dataviewData.skw_degmode) val *= DEG2RAD;
                        dataviewData.current.skwX = obj.skw.x = val;
                    });
                    input.step = 1;
                    input.value = dataviewData.current.skwX = obj.skw.x * RAD2DEG;
                    dataviewData.skwX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        let val = input.valueAsNumber;
                        if (dataviewData.skw_degmode) val *= DEG2RAD;
                        dataviewData.current.skwY = obj.skw.y = val;
                    });
                    input.step = 1;
                    input.value = dataviewData.current.skwY = obj.skw.y * RAD2DEG;
                    dataviewData.skwY = input;
                }, dataItemDegRad(dataviewData, "skw_degmode", (dataviewData, elements) => {
                    elements[1].value = dataviewData.current.skwX = obj.skw.x * RAD2DEG;
                    elements[3].value = dataviewData.current.skwY = obj.skw.y * RAD2DEG;
                    dataviewData.skwX.step = dataviewData.skwY.step = 1;
                }, (dataviewData, elements) => {
                    elements[1].value = dataviewData.current.skwX = obj.skw.x;
                    elements[3].value = dataviewData.current.skwY = obj.skw.y;
                    dataviewData.skwX.step = dataviewData.skwY.step = DEG2RAD;
                })
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr) auto"));
            transformDiv.appendChild(createDataitem("Pivot", ['span', 'input', 'span', 'input'], [
                label => { label.innerHTML = "X:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.pivX = obj.piv.x = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.pivX = obj.piv.x;
                    dataviewData.pivX = input;
                },
                label => { label.innerHTML = ", Y:"; },
                input => {
                    input.type = "number";
                    input.addEventListener("change", () => {
                        dataviewData.current.pivY = obj.piv.y = input.valueAsNumber;
                    });
                    input.value = dataviewData.current.pivY = obj.piv.y;
                    dataviewData.pivY = input;
                }
            ], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
            dataview.appendChild(transformDiv);
        }
        if (obj.alpha != null) {
            dataview.appendChild(createDataitem("Opacity", ['input', 'input'], [
                (slider, elements) => {
                    slider.type = "range";
                    slider.min = 0;
                    slider.max = 1;
                    slider.step = 0.001;
                    slider.value = obj.alpha;
                    slider.addEventListener("input", () => {
                        let val = slider.valueAsNumber;
                        obj.alpha = val;
                        elements[1].value = dataviewData.current.alphaslider = dataviewData.current.alpha = val;
                    });
                    dataviewData.current.alphaslider = Math.min(Math.max(obj.alpha, 0), 1);
                    slider.value = dataviewData.current.alphaslider;
                    dataviewData.alphaslider = slider;
                },
                (input, elements) => {
                    input.type = "number";
                    input.step = 0.1;
                    input.addEventListener("change", () => {
                        let val = dataviewData.current.alpha = input.valueAsNumber;
                        obj.alpha = val;
                        elements[0].value = Math.min(Math.max(val, 0), 1);
                    });
                    input.value = dataviewData.current.alpha = obj.alpha;
                    dataviewData.alpha = input;
                }
            ], "auto 75px"));
        }
    }

    function updateTreeObject(obj) {
        if (obj.name != null) {
            if (obj.element.txt_inner != obj.name) {
                obj.element.txt_inner = obj.name;
            }
        } else if (obj.element.txt_inner != null) {
            obj.element.txt_inner = null;
            obj.element.txt.innerHTML = "<i>unnamed object</i>";
        }
        let activeVal = obj.active;
        if (obj.element.checkbox && obj.element.checkbox.checked != activeVal) {
            obj.element.checkbox.checked = activeVal;
        }

        obj.element.updateChildren();

        let updatedChildren = [];
        ["treeObject", "pixiObject"].forEach(type => { // Probably a horrendous way of going about iterating this, but it lets me so it makes my brain happy.
            if (obj[type]?.children != null) {
                obj[type].children.forEach(childData => { // Finds and adds new children.
                    let child = obj.node.children.find(o => o.object[type] == childData)?.object;
                    if (!child) {
                        child = new ManipulatableObject(childData, obj.element.childList);
                        obj.node.add(child);
                    }
                    updateTreeObject(child);
                    updatedChildren.push(child);
                });
            }
        });
        obj.node.children.forEach(child => { // Removes old children.
            if (!updatedChildren.includes(child.object)) {
                child.removeSelf();
            }
        });
    }

    // needed because can't open popup until there is an interaction
    let createprodigytreeviewermodbutton = document.createElement('button');
    document.body.appendChild(createprodigytreeviewermodbutton);
    createprodigytreeviewermodbutton.innerText = "Open Tree View";
    createprodigytreeviewermodbutton.style = "position: fixed; top: 10px; left: 10px;";
    createprodigytreeviewermodbutton.onclick = () => {
        switch (gametype) {
            case 1: // Math
                gameaccess = window.Boot?.prototype.game;
                break;
            case 2: // English
                gameaccess = window.GameFramework?.FrameworkApp?.instance?.game;
                break;
        }
        if (!gameaccess) {
            createprodigytreeviewermodbutton.remove();
            console.log(`No game detected (type=${gametype}).

  ____________________
< How did we get here? >
  --------------------
         \   ^__^
          \  (oo)\_______
             (__)\       )\/\\
                 ||----w |
                 ||     ||
`); // https://github.com/cowsay-org/cowsay
            return;
        }

        // Source - https://stackoverflow.com/a/16992521
        // Posted by Vadim
        // Retrieved 2026-03-18, License - CC BY-SA 3.0
        var menu = window.open("", "_blank", "height=500,width=900,status=yes,toolbar=0,menubar=0,location=0");
        if (!menu.document.body || !menu.document.body.innerHTML) {
            menu.document.write(css);
            menu.document.write(`
<!-- https://www.w3schools.com/howto/howto_js_treeview.asp -->
<span style="display:none">load-bearing element; the script crashes without this</span>
`);
            treeDiv = document.createElement('div');
            treeDiv.classList.add("tree");
            menu.document.body.appendChild(treeDiv);
            menu.window.addEventListener("keydown", function (evt) {
                if (!selected) return;
                if (menu.document.activeElement.tagName.toUpperCase() == 'INPUT') return;
                switch (evt.key) {
                    case ' ': {
                        evt.preventDefault();
                        if (!selected.element.checkbox) return;
                        let val = !selected.active;
                        selected.active = val;
                        selected.element.checkbox.checked = val;
                        if (dataviewData.active) dataviewData.active.checked = val;
                    } break;
                    case "ArrowUp":
                        evt.preventDefault();
                        if (selected.node.siblingUp) {
                            let up = selected.node.siblingUp;
                            while (!up.object.element.childrenHidden && up.children.length > 0) up = up.children[up.children.length-1];
                            openObject(up);
                        } else if (selected.node.parent) openObject(selected.node.parent);
                        else return;
                        break;
                    case "ArrowLeft":
                        evt.preventDefault();
                        if (!selected.element.childrenHidden) selected.element.doToggle();
                        else if (selected.node.parent) openObject(selected.node.parent);
                        else return;
                        break;
                    case "ArrowDown":
                        evt.preventDefault();
                        if (!selected.element.childrenHidden && selected.node.children.length > 0) openObject(selected.node.children[0]);
                        else if (selected.node.siblingDown) openObject(selected.node.siblingDown);
                        else {
                            let par = selected.node.parent;
                            while (par && !par.siblingDown) par = par.parent;
                            if (par && par.siblingDown) openObject(par.siblingDown);
                            else return;
                        }
                        break;
                    case "ArrowRight":
                        evt.preventDefault();
                        if (selected.node.children.length == 0) return;
                        if (selected.element.childrenHidden) selected.element.doToggle();
                        else openObject(selected.node.children[0]);
                        break;
                }
            });

            let drag = false; let moveX;
            let splitter = document.createElement('div');
            splitter.classList.add("splitter");
            menu.document.body.appendChild(splitter);

            dataview = document.createElement('div');
            dataview.classList.add("dataview");
            menu.document.body.appendChild(dataview);

            treeDiv.style.width = "350px";
            let treeDivResize = x => {
                let bodyStyle = menu.window.getComputedStyle(menu.document.body);
                let treeStyle = menu.window.getComputedStyle(treeDiv);
                treeDiv.style.width = (x-32)+"px";
            };
            splitter.addEventListener("mousedown", function (evt) {
                drag = true;
                treeDivResize(evt.x);
            });
            menu.window.addEventListener("mouseup", function(evt) {
                drag = false;
            });
            menu.window.addEventListener("mousemove", function (evt) {
                if (drag) treeDivResize(evt.x);
            });

            menu.updateTree = setInterval(() => {
                if (!gameaccess) {
                    menu.document.write(`
<p style="color: #F00; font-size: 20px;">Prodigy instance lost.</p>
`);
                    clearInterval(menu.updateTree);
                    return;
                }
                if (menu.closed) {
                    clearInterval(menu.updateTree);
                    treeData = null;
                    return;
                }
                let root = gameaccess.stage;
                let pathUp = [];
                /*while (root.parent != null) {
                    pathUp.push(root.parent.children.indexOf(root));
                    root = root.parent; // The stage might not be the root object.
                }*/ // but tbf it's not much of interest anyway
                if (treeData != null && treeData._data != root) {
                    openObject(null);
                    treeData.element.list.remove();
                    treeData = null;
                }
                let newTree = !treeData;
                if (newTree) {
                    let treeBase = document.createElement('ul');
                    treeBase.style = "margin: 0; padding: 0;";
                    treeDiv.appendChild(treeBase);
                    treeData = new ManipulatableObject(root, treeBase);
                    new ObjectHeirarchy(treeData);
                }
                updateTreeObject(treeData);
                if (newTree) {
                    treeData.element.doToggle();
                    pathUp.forEach(num => treeData.node.children[num].element.doToggle());
                }
            }, 100);

            menu.updateData = setInterval(() => {
                if (!gameaccess || menu.closed) {
                    clearInterval(menu.updateData);
                    return;
                }
                if (!selected?.exists) return;

                if (dataviewData.active) {
                    let val = selected.active;
                    dataviewData.active.checked = selected.active;
                }
                if (dataviewData.posX && dataviewData.posX.value == dataviewData.current.posX) {
                    dataviewData.posX.value = dataviewData.current.posX = selected.pos.x;
                }
                if (dataviewData.posY && dataviewData.posY.value == dataviewData.current.posY) {
                    dataviewData.posY.value = dataviewData.current.posY = selected.pos.y;
                }
                if (dataviewData.rot && dataviewData.rot.value == dataviewData.current.rot) {
                    let val = selected.rot;
                    if (dataviewData.rot_degmode) val *= RAD2DEG;
                    dataviewData.rot.value = dataviewData.current.rot = val;
                }
                if (dataviewData.sclX && dataviewData.sclX.value == dataviewData.current.sclX) {
                    dataviewData.sclX.value = dataviewData.current.sclX = selected.scl.x;
                }
                if (dataviewData.sclY && dataviewData.sclY.value == dataviewData.current.sclY) {
                    dataviewData.sclY.value = dataviewData.current.sclY = selected.scl.y;
                }
                if (dataviewData.skwX && dataviewData.skwX.value == dataviewData.current.skwX) {
                    let val = selected.skw.x;
                    if (dataviewData.skw_degmode) val *= RAD2DEG;
                    dataviewData.skwX.value = dataviewData.current.skwX = val;
                }
                if (dataviewData.skwY && dataviewData.skwY.value == dataviewData.current.skwY) {
                    let val = selected.skw.y;
                    if (dataviewData.skw_degmode) val *= RAD2DEG;
                    dataviewData.skwY.value = dataviewData.current.skwY = val;
                }
                if (dataviewData.pivX && dataviewData.pivX.value == dataviewData.current.pivX) {
                    dataviewData.pivX.value = dataviewData.current.pivX = selected.piv.x;
                }
                if (dataviewData.pivY && dataviewData.pivY.value == dataviewData.current.pivY) {
                    dataviewData.pivY.value = dataviewData.current.pivY = selected.piv.y;
                }
                if (dataviewData.alpha && dataviewData.alpha.value == dataviewData.current.alpha) {
                    let val = dataviewData.current.alpha = selected.alpha;
                    dataviewData.alphaslider.value = val;
                    dataviewData.alpha.value = val;
                }
            }, 5);
        }
    };
})();