Greasy Fork is available in English.
View gameobject trees within Prodigy.
// ==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);
}
};
})();