code:
var docHead = document.querySelector("head");

DollHouse = function (context) {
    context["DoLHouse"] = this;
    currentlyLoadingMap = {};
    this.dirContentMap = {};
    this.reificationRequired = true;
    this.isBlocked = false;
    this.blockedQueue = [];
    this.loadCount = 0;

    this.getTemplate = async (filePath, dollName, whenFinished, infoDiv) => {
        if (infoDiv != null)
            this.infoDiv = infoDiv

        //await this.whenReady(defer());
        var deferred = this.whenReady(async () => {
            var result = await this.loadFile(filePath);
            if (this.reificationRequired) {
                this.preify();
                this.postReify();
            }
            whenFinished(
                {
                    basePath: result.basePath,
                    fileName: result.fileName,
                    content: result.content[dollName]
                });
        });
    }

    this.unblock = () => {
        this.isBlocked = false;
        this.runblockedQueue();
    }

    this.runblockedQueue = () => {
        for (var k of this.blockedQueue) {
            var awaiting = this.blockedQueue.shift();
            awaiting();
        }
    }

    this.whenReady = async (callback) => {
        if (this.isBlocked) {
            //var waiter = defer();
            this.blockedQueue.push(callback);
            //var result = await callback;
            //return result;
        }
        else
            callback();
    }

    this.updateLoadStatus = () => {
        if (this.infoDiv != null) {
            var loadString = "";
            for (var k of Object.keys(this.dirContentMap)) {
                var statusString = "";
                if (this.dirContentMap[k].status == "loaded") {
                    statusString = `<span style="background: green">` + this.dirContentMap[k].status + `</span>`;
                }
                else {
                    statusString = `<span style="background: yellow">` + this.dirContentMap[k].status + `</span>`;
                }
                loadString += k + ` : ` + statusString + `<br/>`;
            }
            this.infoDiv.innerHTML = loadString;
        }
    }

    this.updateBuildStatus = (line) => {
        if (this.infoDiv != null) {
            var textInfo = document.createElement("div");
            textInfo.innerText = line;
            this.infoDiv.appendChild(textInfo);
            this.infoDiv.scrollTop = this.infoDiv.scrollHeight;
        }
    }

    this.loadFile = async (filePath, skipTraversal, d) => {
        if (this.dirContentMap[filePath] != null) {
            if (this.dirContentMap[filePath].status == "loaded") {
                var dirInfo = splitPath(filePath);
                return {
                    basePath: dirInfo.directory,
                    fileName: dirInfo.fileName,
                    content: this.dirContentMap[filePath].content
                };
            }
            else {
                alert("Something confusing and shameful happened.");
                return null;
            }
        }

        //if(currentlyLoading == null) {
        this.reificationRequired = true;
        var dirInfo = splitPath(filePath);
        var awaiter = defer();
        this.dirContentMap[filePath] = {
            status: "in progress",
            content: {},
            promise: awaiter
        };
        this.loadCount++;
        var scriptElem = document.createElement("script");
        scriptElem.id = "script_" + this.loadCount
        currentlyLoadingMap["script_" + this.loadCount] = {
            filePath: dirInfo.directory,
            fileName: dirInfo.fileName,
            fullPath: filePath,
            promise: awaiter
        }
        this.updateLoadStatus();
        scriptElem.setAttribute("src", filePath);
        this.isBlocked = true;
        docHead.appendChild(scriptElem);
        var result = await awaiter;

        if (!skipTraversal) {
            var d = selfOr(d, "");
            await this.buildMap(result, dirInfo.directory, filePath, d);
        }

        return {
            basePath: dirInfo.directory,
            fileName: selfOr(dirInfo.fileName, ""),
            content: this.dirContentMap[filePath].content
        };
    }

    this.add = (dolls) => {

        var script = document.currentScript;
        var loader = currentlyLoadingMap[script.id];
        var awaiter = loader.promise;
        console.log("add called");
        this.dirContentMap[loader.fullPath].content = dolls;
        this.dirContentMap[loader.fullPath].status = "loaded";
        awaiter.resolve(dolls);

        delete currentlyLoadingMap[script.id];
        this.updateLoadStatus();

    }
    this.unloadedReferences = {};
    this.discoveredValues = {};

    /**
     * Logic: We import via two passes. First, we load the contents of any js files specified in any import
     * statements of the given object into a directory->variable map.
     *
     * Then we go through the elements of the completed map, and replace the contents of any import statements
     * with a referemce to the object which was imported.
     */
    this.buildMap = async (obj, basePath, inFile) => {
        var currentPath = basePath;
        var fullPathName = inFile;
        for (var k of Object.keys(obj)) {
            if (k == "import") {
                if (obj.import.filepath != null) {
                    fullPathName = basePath + obj.import.filepath;
                    currentPath = splitPath(fullPathName).directory;
                    var pathAndObj = await this.loadFile(fullPathName, true);
                    var subObjects = pathAndObj.content;
                    await this.buildMap(subObjects, currentPath, fullPathName);
                }
            } else if (isObj(obj[k])) {
                await this.buildMap(obj[k], basePath, inFile);
            }
        }
    }

    /**goes through all elements of the dir content map and replaces the contents of any import keys with
     * the object requested for import.
     * once all requested things are placed in the import value, the contents of the object are pulled keywise
     * into the parent object and the import key is deleted*/

    this.preify = () => {
        for (var k of Object.keys(this.dirContentMap)) {
            var reified = this.dirContentMap[k].reified;
            var cont = this.dirContentMap[k].content;
            if (reified != true) {
                for (var ek of Object.keys(cont)) {
                    //cont[ek].basePath = splitPath(k).directory;
                    this.reify(cont[ek], k);
                }
                this.dirContentMap[k].reified = true;
            }
        }
    }

    this.reify = (fromObj, currentfile) => {
        for (var ok of Object.keys(fromObj)) {
            if (ok == "import") {
                var importInfo = fromObj.import;
                if (!isObj(importInfo.fDollTempContent)) {
                    if (importInfo.filepath == null) {
                        importInfo.filepath = currentfile;
                    } else {
                        var dirInfo = splitPath(currentfile);
                        importInfo.filepath = dirInfo.directory + importInfo.filepath;
                    }
                    fromObj.import.fDollTempContent = this.dirContentMap[importInfo.filepath].content[importInfo.variable];
                    this.updateBuildStatus("reifying : " + importInfo.variable + " in " + fromObj.import.filepath);
                    this.reify(fromObj.import.fDollTempContent, fromObj.import.filepath);
                }
            } else {
                var elem = fromObj[ok];
                if (isObj(elem)) {
                    this.reify(elem, currentfile);
                }
            }
        }
        if (fromObj.variants != null || fromObj.accessories != null) {
            fromObj.basePath = splitPath(currentfile).directory;
        }

    }

    this.postReify = () => {
        for (var k of Object.keys(this.dirContentMap)) {
            var cont = this.dirContentMap[k].content;
            for (var ek of Object.keys(cont)) {
                this.pullUp(cont[ek]);
            }
        }
        this.reificationRequired = false;
        if (Object.keys(currentlyLoadingMap).length == 0) {
            this.unblock();
        }
    }

    /**pulls up the contents of any import objects recursively (depth first)*/
    this.pullUp = (fromObj) => {
        if (fromObj.import != null) {
            if (fromObj.import.fDollTempContent != null) {
                for (var k of Object.keys(fromObj.import.fDollTempContent)) {
                    fromObj[k] = fromObj.import.fDollTempContent[k];
                }
            }
            delete fromObj.import;
        }
        for (var k of Object.keys(fromObj)) {
            var recurse = fromObj[k];
            if (isObj(recurse)) {
                this.pullUp(recurse);
            }
        }
    }

};

/**returns true if the input is a javascript object and not an array */
isObj = (elem) => {
    return elem != null && Array.isArray(elem) == false && typeof elem == "object";
}

/**
* splits the input string into a directory component and a file component.
* @param {String} pathString
*/
splitPath = (pathString) => {
    var jsSplit = pathString.split(".js");
    if (jsSplit.length > 1) {
        var dir = jsSplit[0].split("/");
        var filename = dir[dir.length - 1] + ".js";
        var dirString = "";
        for (var i = 0; i < dir.length - 1; i++)
            dirString += dir[i] + "/"
        return { directory: dirString, fileName: filename };
    } else {
        return { directory: pathString, fileName: null };
    }
}

/**Convenience function to resume code until after completing a request to load some file**/
defer = () => {
    var res, rej;
    var promise = new Promise((resolve, reject) => {
        res = resolve;
        rej = reject;
    });
    promise.resolve = res;
    promise.reject = rej;
    return promise;
}


code:
const DoLHouse = new DollHouse(window);
var loadedDollCount = 0;
var idMap = {};

class FDoll {

    blockedQueue = [];

    /**
     * @param {String}  dollFile  a path to the .js file declaring the doll template, or otherwise an javascript object
     * of the template
     * @param {*} dollName name of the doll specified in the given js file. Alternatively, the doll object itself.
     */
    constructor(filePath, dollName, anAwaiter, parentFDoll, infoDiv, dollObj) {
        //ancestors looking for anchors
        //the passthrough doesn't get passed along if this
        //accessory attaches to it.
        this.attachPassthrough = {
            /**anchorname : ancestorreference */
        };
        this.loopSpeed = parentFDoll == null ? 500 : parentFDoll.loopSpeed;
        loadedDollCount += 1;
        this.isBlocked = true;
        this.nameAs(dollName);
        var getDoll = async (result) => {
            this.parent = parentFDoll;
            if (result.scale != null) {
                console.log("strange");
            }
            var hold = await this.constructFrom(result);
            if (anAwaiter != null) {
                if (anAwaiter instanceof Promise)
                    this.whenReady(anAwaiter);
                else
                    anAwaiter();
            }
        };
        if (dollObj == null) {
            DoLHouse.getTemplate(filePath, dollName, getDoll, infoDiv);
        } else {
            //var clean1 = splitPath(selfOr(dollName.specifier,"")).directory;
            //var clean2 = splitPath(selfOr(dollName.data.specifier,"")).directory;
            this.attach = dollObj.attach;
            this.to = dollObj.to;
            /*var tempscale = this.copyObj(selfOr(dollName.scale, { x: 1, y: 1 }));
            var temptranslate = this.copyObj(selfOr(dollName.translate, { x: 0, y: 0, depth: 0 }));

            var thisTranslate = { x: 0, y: 0, depth: 0 };
            thisTranslate.x *= tempscale.x;
            thisTranslate.y *= tempscale.y;
            thisTranslate.x += temptranslate.x;
            thisTranslate.y += temptranslate.y;
            thisTranslate.depth += temptranslate.depth;
            this.translate = thisTranslate;
            this.scale = tempscale;*/
            getDoll(
                {
                    basePath: dollObj.basePath == null ? filePath : dollObj.basePath,
                    content: dollObj
                });
        }
    }
    unblock = () => {
        this.isBlocked = false;
        this.runblockedQueue();
    }

    runblockedQueue = () => {
        for (var k of this.blockedQueue) {
            var awaiting = this.blockedQueue.shift();
            awaiting.resolve();
        }
    }

    whenReady = async (callback) => {
        if (this.isBlocked) {
            var waiter = defer();
            this.blockedQueue.push(waiter);
            var result = await waiter;
            return callback.resolve();
        }
        else
            return callback.resolve();
    }

    /**
     * sets the loop speed for this doll and its descendants
    */
    setLoopSpeed(newSpeed) {
        this.loopSpeed = newSpeed;
        for(var k of Object.keys(this.accessories)) {
            this.accessories[k].setLoopSpeed(newSpeed);
        }
        if(this.selected_variant != null) {
            this.selected_variant.setLoopSpeed(newSpeed);
        }
        //this.computeGlobals()
        //this._forceRestyle(true);
    }

    /**
     * creates array of whatever length is required to create subframes for all frames in
     * this global_sequence_array to all subframes in the parent_global_subframe_array.
     * The elements of the returned array will be JSON objects specifying
     * {
     * this_play_idx: //the index of the played frame from which this subframe was created
     * par_subframe_idx: //the index of the parent played subframe to which this subframe maps
     * play_t: //the percentage of the loop at which this frame should play
     * spriteframe: //the sprite frame to which this subframe corresponds
     * }
     * the inputs should be array of floats, where each float specifies the percentage
     * of the loop at which the frame starts
     */
    setSubFrameSplit = () => {
        var result = [];
        var ftemp = [];

        var from_arr = this.global_play_sequence;
        var to_arr = this.parent == null ? [] : this.parent.global_subframe_sequence;
        for (var i = 0; i < from_arr.length; i++) {
            ftemp.push({
                type: "from",
                play_t: from_arr[i].play_t,
                idx: i
            });
        }
        for (var i = 0; i < to_arr.length; i++) {
            ftemp.push({
                type: "to",
                play_t: to_arr[i].play_t,
                idx: i
            });
        }

        var sortedPreTemp = ftemp.sort((a, b) => { return a.play_t - b.play_t });
        var sortedTemp = [];
        for (var i = 0; i < sortedPreTemp.length; i++) {
            if (i == 0) {
                sortedTemp.push(sortedPreTemp[i]);
            } else if (sortedPreTemp[i].play_t != sortedTemp[sortedTemp.length - 1].play_t) {
                sortedTemp.push(sortedPreTemp[i]);
            }
        }

        var lastT = 0;
        var lastF = 0;
        for (var i = 0; i < sortedTemp.length; i++) {
            var ob = sortedTemp[i];
            if (ob.type == "from")
                lastF = ob.idx;
            if (ob.type == "to")
                lastT = ob.idx;
            result.push({
                this_play_idx: lastF,
                par_subframe_idx: lastT,
                play_t: ob.play_t,
                spriteframe: this.global_play_sequence[lastF].spriteframe
            }
            )
        }
        this.global_subframe_sequence = result;
    }

    /**
   * creates an array of JSON objects specifiyng frame index and play_time
   * from the selected_sequence specified on this doll.
   * if a selected_sequence wasn't specified, creates a default one*/
    ensureSeq = () => {
        if (this.phase == null) this.phase = 0;
        this.globalPhase = this.phase;
        if (this.resolvedParent != null) {
            this.globalPhase += this.resolvedParent.globalPhase;
        }
        var baseSeq = this.selected_sequence;
        if (baseSeq == null) {
            baseSeq = [];
            for (var i = 0; i < this.frames; i++) {
                baseSeq.push(i);
            }
            this.selected_sequence = baseSeq;
        }
        this.global_play_sequence = [];

        for (var i = 0.0; i < this.selected_sequence.length; i++) {
            this.global_play_sequence.push({
                spriteframe: this.selected_sequence[mod(i + this.globalPhase, this.selected_sequence.length)],
                play_t: i / this.selected_sequence.length
            });
        }
    }

    ensureMagnets = () => {
        this.playSeqMagnets = {};
        this.globalPlaySeqMagnets = {};
        if (this.magnets == null || Object.keys(this.magnets).length == 0) {
            this.magnets = { origin: { x: 0, y: 0, depth: 0 } };
        }
        if (this.magnets.origin == null) {
            this.magnets.origin = { x: 0, y: 0, depth: 0 };
        }
        for (var k of Object.keys(this.magnets)) {
            if (this.magnets[k].depth == null)
                this.magnets[k].depth = 0;
            this.magnets[k] = this.redundifyMagnet(this.magnets[k]);
        }

        if (this.parent != null && this.magnets[this.parent.attachPassthrough.pending] != null) {
            this.resolvedParent = this.parent.attachPassthrough.doll;
            this.anchorName = this.parent.attachPassthrough.pending;
            this.anchorTarget = this.parent.attachPassthrough.to;
        }
        if (this.attach != null) {
            if (this.magnets[this.attach] == null) {
                this.attachPassthrough = {
                    pending: this.attach,
                    doll: this.magnets[this.to] == null ? this.parent.getFirstAncestorContainingMag(this.to) : this,
                    to: this.to
                };
            } else {
                this.resolvedParent = this.parent.getFirstAncestorContainingMag(this.to);
                this.anchorName = this.attach;
                this.anchorTarget = this.to;
            }
        } else if (
            this.parent != null &&
            this.parent.attachPassthrough.pending != null
        ) {
            this.attachPassthrough = {
                pending: this.parent.attachPassthrough.pending,
                doll: this.parent.attachPassthrough.doll,
                to: this.parent.attachPassthrough.to
            }
        }

    }

    setupMagnets = () => {
        for (var k of Object.keys(this.magnets)) {
            this.playSeqMagnets[k] = this.getPlaySeqMappedMagnetsFor(this.magnets[k]);
            if (this.playSeqMagnets[k].length == 0) this.playSeqMagnets[k] = this.copyObj(this.magnets[k]);
        }

        /*
        if (this.parent != null && this.magnets[this.parent.attachPassthrough.pending] != null) {
            this.magParent = this.parent.attachPassthrough.doll;
            this.anchorName = this.parent.attachPassthrough.pending;
            this.anchorTarget = this.parent.attachPassthrough.to;
        }
        if (this.attach != null) {
            this.attachPassthrough = {
                pending: this.attach,
                doll: this.magnets[this.to] == null ? this.parent : this,
                to: this.to
            };
        } else if (
            this.parent != null &&
            this.parent.attachPassthrough.pending != null
        ) {
            this.attachPassthrough = {
                pending: this.parent.attachPassthrough.pending,
                doll: this.parent.attachPassthrough.doll,
                to: this.parent.attachPassthrough.to
            }
        }*/
    }

    /**
     * returns an array with as many duplicate instances of the last magnet instance as required to
     * match the number of frames on the spritesheet
     * if the input magnet is not specified as an array, wraps it inside of one.
     * @param {*} m
     */
    redundifyMagnet = (m) => {
        var result = m;
        if (!Array.isArray(m)) {
            result = [m]
        }
        var count = result.length;
        for (var i = count; i < this.frames; i++) {
            result[i] = this.copyObj(result[count - 1]);
        }
        return result;
    }

    /**given an array of magnets ordered such that each magnet index corresponds to a frame on the spritesheet
     * returns an array of magnets such that each magnet corresponds to the appropriate occurrence of that frame
     * on the spritesheet in the global_subframe_sequence
     */
    getPlaySeqMappedMagnetsFor = (m) => {
        var result = [];
        for (var i = 0; i < this.global_subframe_sequence.length; i++) {
            result.push(m[this.global_subframe_sequence[i].spriteframe]);
        }
        return result;
    }

    /**
     * returns the first ancestor of this acessory (inclusive with this accessory) containing a magnet
     * with the given name.
     */
    getFirstAncestorContainingMag = (magName) => {
        if (this.magnets[magName] != null) return this;
        else if (this.parent != null) return this.parent.getFirstAncestorContainingMag(magName);
        else return null;
    }

    setPhase(phase) {
        if (phase != null)
            this.phase = phase;
        this.ensureMagnets();
        this.ensureSeq();
        this.setSubFrameSplit();
        this.setupMagnets();

        for (var k of Object.keys(this.variants)) {
            this.variants[k].setPhase();
        }
        for (var k of Object.keys(this.accessories)) {
            this.accessories[k].setPhase();
        }
    }

    /**stores some of the default parameters loaded from the file for easier resetting */
    storeDefaults() {
        this.default_params = {};
        this.default_params.scale = this.copyObj(selfOr(this.scale, { x: 1, y: 1 }));
        this.default_params.translate = this.copyObj(selfOr(this.translate, { x: 1, y: 1, depth: 0 }));
        this.default_params.filter = this.filter;
        this.default_params.phase = this.phase
        this.default_params.selected_sequence = this.selected_sequence;
    }
    inheritSequencesIfNecessary() {

        if (this.parent != null && this.parent.promulgate_sequence_info) {
            this.promulgate_sequence_info = true;
            if (!(this.inherit_sequence_info === false || this.inherit_sequence_info === "false")) {
                this.inherit_sequence_info = true;
            }
        }

        if (this.parent != null && this.inherit_sequence_info) {
            this.sequences = { ...this.parent.playSequencePassThrough.sequences, ...this.sequences };
            if (this.selected_sequence == null) {
                this.selected_sequence = this.parent.playSequencePassThrough.selected_sequence;
            }
        }
        this.playSequencePassThrough = {
            sequences: this.sequences,
            selected_sequence: this.selected_sequence
        }
        if (this.sequences != null && typeof this.selected_sequence == "string")
            this.selected_sequence = this.sequences[this.selected_sequence];
    }

    constructFrom = async (dollInfoContainer) => {
        var dollInfo = dollInfoContainer.content;
        dollInfo = dollInfo.data != null ? dollInfo.data : dollInfo;
        this.dollInfo = dollInfo;
        this.basePath = dollInfoContainer.basePath;
        this.sequences = {};
        for (var k of Object.keys(dollInfo)) {
            this[k] = this.copyObj(dollInfo[k]);
        }
        this.translate = this.copyObj(selfOr(this.translate, { x: 0, y: 0, depth: 0 }));
        this.scale = this.copyObj(selfOr(this.scale, { x: 1, y: 1 }));
        this.storeDefaults();
        this.inheritSequencesIfNecessary();
        this.accessories = {};
        this.variants = {};

        this.spritesheet = dollInfo.spritesheet;
        this.phase = selfOr(dollInfo.phase, 0);
        if (this.width == null && this.parent != null) {
            this.width = this.parent.width;
        }
        if (this.frames == null && this.parent != null) {
            this.frames = this.parent.frames;
        } else if (this.parent == null && this.frames == null) {
            this.frames = 0;
        }
        this.ensureMagnets();
        //this.resolvedParent = this.getFirstAncestorContainingMag(this.to);
        //if(this.resolvedParent == null) this.getFirstAncestorContainingMag("origin");
        //if(this.resolvedParent == this) this.resolvedParent = null;
        this.ensureSeq();
        this.setSubFrameSplit();
        this.setupMagnets();
        if (this.filter == null) this.filter = "";
        if (this.inherit_filter && this.parent != null) {
            if (this.parent.postfilter != null) {
                this.postfilter = this.parent.postfilter + " ";
            }
            this.filter = this.parent.filter + " " + this.filter;
        }

        var holdOff = await this.instantiateSubdollsOf(dollInfo);
        this.idString = "fdoll_accessory_" + loadedDollCount;
        var plus = 0;
        while (idMap[this.idString] != null) {
            plus++;
            this.idString = "fdoll_accessory_" + (loadedDollCount + plus);
        }
        idMap[this.idString] = [];
        idMap[this.idString].push(this);
        //var existingSprite = document.querySelector("#" + this.idString);
        this.ensureSpriteElems();
        if (this.scale == null) {
            this.scale = { x: 1, y: 1 };
        }
        // var complete = await anAwaiter;
        this.unblock();
    }

    ensureSpriteElems = () => {
        var filterString = "";
        if (this.filter != null) {
            filterString += this.filter;
        }
        if (this.postfilter != null) {
            filterString += this.postfilter;
        }

        if (this.spritesheet != null) {
            if (this.spriteElem == null) {
                this.spriteElem = document.createElement("div");

                this.spriteElem.id = this.idString;
                this.spriteElem.forDoll = this;
            }

            if (this.imageElem == null) {
                this.imageElem = document.createElement("img");
            }
            this.imageElem.style.filter = filterString;
            this.spriteElem.classList.add("spritesheet_container");
            if (this.keyFrameElem == null) {
                this.keyFrameElem = document.createElement("style");
                this.keyFrameElem.id = "for_" + this.spriteElem.id;
            }
            if (this.spriteFrameElems == null) {
                this.spriteFrameElem = document.createElement("style");
                this.spriteFrameElem.id = "for_outer_" + this.spriteElem.id;
            }

            //docHead.appendChild(this.keyFrameElem);
            //docHead.appendChild(this.spriteFrameElem);

            if (this.spritesheet != null) {
                this.imageElem.setAttribute("src", this.basePath + this.spritesheet);
                this.spriteElem.appendChild(this.imageElem);
            }
        }

    }

    nameAs = (name) => {
        if (this.spriteElem != null)
            this.spriteElem.setAttribute("data-name", name);
        this.named = name;
        this.amnamed = name;
        //this.activeVariantMap = this.getRootDollActiveVariantMapMapFor(this.getAccessPath());
    }

    /**
     * recursively adds any js files specified in the given dollInfo
     * if they have not been loaded already and adds
     * them to the cache to avoid reloading existing files
     */
    instantiateSubdollsOf = async (dollInfo) => {

        if (dollInfo.variants != null) {
            //for (var k of Object.keys(dollInfo.variants)) {

            //this.variants[k].hide();
            //}
            if (dollInfo.selected_variant != null) {
                this.instantiateVariant(dollInfo.selected_variant);
                this.selected_variant = this.variants[dollInfo.selected_variant];
                //this.selected_variant.show();
            } else if (this.dollInfo.variants != null && Object.keys(this.dollInfo.variants).length > 0) {
                for (var k of Object.keys(this.dollInfo.variants)) {
                    this.instantiateVariant(k);
                    this.selected_variant = this.variants[dollInfo.selected_variant];
                    break;
                }
            }
        }
        if (dollInfo.accessories != null) {
            for (var k of Object.keys(dollInfo.accessories)) {
                var acc = dollInfo.accessories[k];
                this.accessories[k] = this.copyObj(acc);
                var anAwaiter = defer();
                this.accessories[k] = new FDoll(this.basePath, k, anAwaiter, this, null, acc);

                var wait = await anAwaiter;
                //this.accessories[k].nameAs(k);
                //this.accessories[k].setParent(this, attachChild, toChild);
                //this.accessories[k].show();
            }
        }
    }

    renderTo = async (elem, requestAnimFrame) => {
        var renderOut = async () => {
            var waitResult = await this.whenReady(defer());
            await this.show();
            var waitResult = await this.computeGlobals();
            this._forceRender(elem);
        };

        if(requestAnimFrame == true) window.requestAnimationFrame(()=>{renderOut()})
        else renderOut();
    }

    _forceRender = (elem) => {
        //if(this.spriteElem.parentNode != null)
        this.ensureSpriteElems();
        if (this.spriteElem != null)
            this.spriteElem.remove();
        this._forceRestyle(false);

        for (var k of Object.keys(this.accessories)) {
            this.accessories[k]._forceRender(elem);
        }
        if (this.selected_variant != null) {
            this.selected_variant._forceRender(elem);
        }
        /*for (var k of Object.keys(this.variants)) {
            await this.variants[k]._forceRender(elem);
        }*/
        if (this.spriteElem != null
            && this.spriteElem.parentNode != elem
            && this.visible) {
            if (this.keyFrameElem.parentNode == null)
                docHead.appendChild(this.keyFrameElem);
            if (this.spriteFrameElem.parentNode == null)
                docHead.appendChild(this.spriteFrameElem);

            elem.appendChild(this.spriteElem);
        }
    }

    _forceRestyle = (recursive) => {
        if (this.visible && this.spritesheet) {
            this.ensureSpriteElems();
            this.spriteElem.style.width = this.drawInfo.width;
            this.spriteElem.style.transform = this.drawInfo.scaling;
            this.spriteElem.style.transformOrigin = this.drawInfo.transformOrigin;
            this.spriteElem.style.zIndex = this.drawInfo.depth;

            var animString = `@keyframes ` + this.spriteFrameElem.id + `outer-anim {
                `;

            for (var i = 0; i < this.global_subframe_sequence.length; i++) {
                var subframe = this.global_subframe_sequence[i];
                var percent = (subframe.spriteframe / this.frames) * 100;
                var time = subframe.play_t;
                var anchorage = "translate(" + this.drawInfo.framed_transforms[i].x + "px, " + this.drawInfo.framed_transforms[i].y + "px)";
                var scaleage = "scale(" + this.globalScale.x + ", " + this.globalScale.y + ")";
                animString += (100 * time) + `% {transform: ` + anchorage + ` ` + scaleage + `;}
                `;
            }
            this.spriteFrameElem.innerText = animString + "}";
            this.spriteElem.style.animationName = this.spriteFrameElem.id + `outer-anim`
            this.spriteElem.style.animationDuration = this.loopSpeed + "ms";
            //this.keyFrameElem.style.animationDuration = FDollGlobals.loopSpeed + "ms";
            this.spriteElem.style.animationTimingFunction = "steps(1)";
            this.spriteElem.style.animationIterationCount = "Infinite";
            this.spriteElem.style.imageRendering = "pixelated";

        }

        if (recursive) {
            for (var k of Object.keys(this.accessories)) {
                this.accessories[k]._forceRestyle(true);
            }
            if (this.selected_variant != null)
                this.selected_variant._forceRestyle(true);
        }
    }

    /**recursively computes all global space style and anchor information
     * on this FDoll and its children
     *
     * Logic:
     *  1. compute the global locations of each magnet on this accessory at each frame in its spritesheet
     *      (scale all magnets by parent global scale -> globalMagnets,
     *       scale all magnets by this scale -> globalMagnets,
     *       translate all globalMagnets by amount required to move global anchor to parent global target -> globalMagnets,
     *       )
     *  2. determine the frame correspondence between this accessory and the parent accessory
    */
    computeGlobals = () => {
        var waitResult = this.whenReady(defer());

        /*if (this.parent != null) {
            this.playSequencePassThrough = {
                sequence: this.parent.playSequencePassThrough.sequence,
                frameCount: this.playSequencePassThrough.frameCount
            }
        }*/
        this.computeGlobalTransforms();
        this.computeGlobalFrameSequence();

        //for (var k of Object.keys(this.variants)) {
        if (this.selected_variant != null)
            this.selected_variant.computeGlobals();
        //}
        for (var k of Object.keys(this.accessories)) {
            this.accessories[k].computeGlobals();
        }
        /*if(this.selected_variant != null) {
            this.selected_variant.computeGlobals();
        }*/

    }

    /**returns the transforms of all descendants that were skipped due to
     * magnet passthrough so that scales and translations will still influence
     * children
     */
    getSumOfSkippedTransforms = (upTo) => {
        var transforms = {
            scale: { x: this.scale.x, y: this.scale.y },
            translate: { x: this.translate.x, y: this.translate.y, depth: this.translate.depth }
        };
        /*if() {
            transforms = {
                scale : {x: 1, y: 1},
                translate: {x: 0, y:0, depth: 0}
            }
        }*/
        if (upTo == this.parent || this.parent == null) {

        } else if (upTo == this) {
            transforms = {
                scale: { x: 1, y: 1 },
                translate: { x: 0, y: 0, depth: 0 }
            }
        }
        else {
            var parTransforms = this.parent.getSumOfSkippedTransforms(upTo);
            transforms.scale.x *= parTransforms.scale.x;
            transforms.scale.y *= parTransforms.scale.y;
            transforms.translate.x += parTransforms.scale.x * parTransforms.translate.x
            transforms.translate.y += parTransforms.scale.y * parTransforms.translate.y
            transforms.translate.depth += parTransforms.translate.depth;

        }
        return transforms;

    }

    /**
     * computes the appropriate translation, scale, and z-index for this elements
     * spritesheet for global rendering,
     *
     * composition order is
     *
     * first translate, then scale.
     */
    computeGlobalTransforms = () => {
        this.globalDepth = 0;
        var resolvedAnchorTarget = this.anchorTarget == null ? "origin" : this.anchorTarget;
        var resolvedAnchorName = this.anchorName == null ? "origin" : this.anchorName;
        if (this.parent == null) {
            var parGlobalScale = { x: 1, y: 1 };
            var globalParTranslate = { x: 0, y: 0, depth: 0 };
            //this.globalScale = this.copyObj(this.scale);
            this.globalPlaySeqMagnets = this.copyObj(this.playSeqMagnets);
            this.resolvedParent = this;
            var globalParMagnet = this.copyObj(this.playSeqMagnets[resolvedAnchorTarget]);
            var parGlobalARMS = this.copyObj(this.playSeqMagnets);
            this.globalDepth = this.translate.depth;
            this.globalScale = this.copyObj(this.scale);
        } else {
            this.resolvedParent = this.resolvedParent == null ? this.parent : this.resolvedParent;
            var parGlobalScale = this.resolvedParent.globalScale;
            var globalParTranslate = this.resolvedParent.globalTranslate;
            this.globalScale = this.mult(this.scale, parGlobalScale);
            var globalParMagnet = this.resolvedParent.globalPlaySeqMagnets[resolvedAnchorTarget];
            var parGlobalARMS = this.resolvedParent.globalARMS;
            this.globalDepth = this.parent.globalDepth + this.translate.depth;
        }

        //if(this.parent != this.resolvedParent)
        this.accruedTransforms = this.getSumOfSkippedTransforms(this.resolvedParent);
        this.globalScale.x = this.accruedTransforms.scale.x * parGlobalScale.x;
        this.globalScale.y = this.accruedTransforms.scale.y * parGlobalScale.y;

        this.anchorRelativeMagnets = {};
        var anchor = this.playSeqMagnets[resolvedAnchorName];
        for (var k of Object.keys(this.playSeqMagnets)) {
            var arm = [];
            for (var frame = 0; frame < this.playSeqMagnets[k].length; frame++) {
                var anchorFrame = anchor[frame];
                var magFrame = this.playSeqMagnets[k][frame];
                var armf = {};
                armf.x = magFrame.x - anchorFrame.x;
                armf.y = magFrame.y - anchorFrame.y;
                armf.depth = magFrame.depth - anchorFrame.depth;
                arm.push(armf);
            }
            this.anchorRelativeMagnets[k] = arm;
        }

        var localArms = {};
        for (var k of Object.keys(this.anchorRelativeMagnets)) {
            var l_arm = [];
            for (var frame = 0; frame < this.anchorRelativeMagnets[k].length; frame++) {
                var armf = this.anchorRelativeMagnets[k][frame];
                var l_armf = {};
                l_armf.x = (armf.x * this.accruedTransforms.scale.x) + this.accruedTransforms.translate.x;
                l_armf.y = (armf.y * this.accruedTransforms.scale.y) + this.accruedTransforms.translate.y;
                l_armf.depth = armf.depth + this.accruedTransforms.translate.depth; //armf.depth;
                l_arm.push(l_armf);
            }
            localArms[k] = l_arm;
        }

        this.globalPlaySeqMagnets = {};

        for (var k of Object.keys(localArms)) {
            var g_arm = [];
            for (var frame = 0; frame < localArms[k].length; frame++) {
                var targetIdx = this.global_subframe_sequence[frame].par_subframe_idx;
                if (this.resolvedParent != this)
                    var targetMagnet = this.resolvedParent.globalPlaySeqMagnets[resolvedAnchorTarget][targetIdx];
                else
                    var targetMagnet = { x: 0, y: 0, depth: 0 };

                var l_armf = localArms[k][frame];
                var g_armf = {};
                g_armf.x = (l_armf.x * parGlobalScale.x) + targetMagnet.x;
                g_armf.y = (l_armf.y * parGlobalScale.y) + targetMagnet.y;
                g_armf.depth = l_armf.depth + targetMagnet.depth;
                g_arm.push(g_armf);
            }
            this.globalPlaySeqMagnets[k] = g_arm;
        }

        this.drawInfo = {
            width: (this.width / this.frames) + "px",
            depth: this.globalPlaySeqMagnets[resolvedAnchorName][0].depth,// + this.translate.depth,
            scaling: "scale(" + this.globalScale.x + ", " + this.globalScale.y + ")",
            framed_transforms: this.globalPlaySeqMagnets["origin"], //"translate(" + xDelta + "px, " + yDelta + "px) scale(" + this.globalScale.x + ", " + this.globalScale.y + ")",
            transformOrigin: "top left"
        }
    }

    /**
     * set a css filter on this accessory or variant
     * (note, filter effect does not automatically propogate to children
     * unless the children have their "inherit_filter" property set to true);
     *
    */
    setFilter(filter, exclusiveMode) {
        this.postfilter = filter;
        if (!exclusiveMode) {
            if (this.parent != null && Object.keys(this.parent.variants).length != 0) {
                this.parent.setFilter(filter, true);
                return;
            } else {
                this.setFilter(filter, true);
            }
        } else {
            this.ensureSpriteElems(); /*setting of postfilter handled by ensureSpriteElem*/

            //if (this.spritesheet != null) {
            //  this.imageElem.style.filter = this.postfilter;
            //}
            for (var k of Object.keys(this.accessories)) {
                var acc = this.accessories[k];
                if (acc.inherit_filter)
                    acc.setFilter(filter, true);
            }
            for (var k of Object.keys(this.variants)) {
                var vari = this.variants[k];
                if (vari.inherit_filter)
                    vari.setFilter(filter, true);
            }
        }
    }

    /**
     * adds the given filter to the filter stack without replacing previous filters
     * @param {String} filter
     * @param {boolean} exclusiveMode
     */
    pushFilter(filter, exclusiveMode) {

    }

    /**
     * translates the entire doll so that the magnet given magnet on this accessory aligns with
     * the given magnet on the target doll accessory. Use this if you want to (for example) make dolls hold hands.
     *
     * @param {*} thisMagnetName
     * @param {*} targetDoll
     * @param {*} targetMagnetName
     */
    dragBy(thisMagnetName, targetDoll, targetMagnetName) {
        var thisVariant = this.getCurrentVariant();
        var descendantContainer = thisVariant.magnets[thisMagnetName] == null ? null : thisVariant;
        if (descendantContainer == null)
            descendantContainer = thisVariant.findDescendantContainingMagnet(thisMagnetName);
        this.boundedMove(thisMagnetName, descendantContainer, targetDoll, targetMagnetName, this.findDollRoot());
    }

    /**
     * similar to dragBy, but affect only this accessory.
     * (meaning you can drag an arm so its hand aligns with a certain spot, without having to drag the entire doll)
     * @param {*} thisMagnetName
     * @param {*} targetDoll
     * @param {*} targetMagnetName
     */
    accessoryDragBy(thisMagnetName, targetDoll, targetMagnetName) {
        var thisVariant = this.getCurrentVariant();
        var descendantContainer = thisVariant.magnets[thisMagnetName] == null ? null : thisVariant;
        if (descendantContainer == null)
            descendantContainer = thisVariant.findDescendantContainingMagnet(thisMagnetName);
        this.boundedMove(thisMagnetName, descendantContainer, targetDoll, targetMagnetName, this);
    }

    boundedMove(thisMagnetName, thisMagnetContainer, targetDoll, targetMagnetName, dragAncestor) {

        var targetDollVariant = targetDoll.getCurrentVariant()
        var targetDollMag = targetDollVariant.findDescendantContainingMagnet(targetMagnetName);

        //for(var i=0; i<10; i++) {
        var magnetLoc = thisMagnetContainer.globalPlaySeqMagnets[thisMagnetName][0];
        var targetLoc = targetDollMag.globalPlaySeqMagnets[targetMagnetName][0];
        var magnetDelta = { x: targetLoc.x - magnetLoc.x, y: targetLoc.y - magnetLoc.y }
        console.log("moving from: (" + dragAncestor.translate.x + ", " + dragAncestor.translate.y + ")");
        console.log("moving to: (" + (dragAncestor.translate.x + magnetDelta.x) + ", " + (dragAncestor.translate.y + magnetDelta.y) + ")");
        console.log("moving by: (" + magnetDelta.x + ", " + magnetDelta.y + ")");

        dragAncestor.translate.x += magnetDelta.x;
        dragAncestor.translate.y += magnetDelta.y;
        dragAncestor.computeGlobals();
        magnetLoc = thisMagnetContainer.globalPlaySeqMagnets[thisMagnetName][0];
        console.log("resulted in: (" + magnetLoc.x + ", " + magnetLoc.y + ")");
        console.log("-----");
        //}

        /**todo, figure out why this ends up with precision issues */

        //console.log("-----");
        //console.log("-----");
        //console.log("-----");

    }

    /**returns the highest level doll */
    findDollRoot() {
        if (this.parent == null) return this;
        else if (this.rootDoll != null) return this.rootDoll;
        else return this.parent.findDollRoot();
    }

    findDescendantContainingMagnet(magnetName) {
        if (this.magnets[magnetName] != null) {
            return this;
        } else {
            var result = null;
            for (var k of Object.keys(this.accessories)) {
                result = this.accessories[k].findDescendantContainingMagnet(magnetName);
                if (result != null) return result;
            }
            if (this.selected_variant != null) {
                return this.selected_variant.findDescendantContainingMagnet(magnetName);
            } else {
                for (var k of Object.keys(this.variants)) {
                    result = this.variants[k].findDescendantContainingMagnet(magnetName);
                    if (result != null) return result;
                }
            }
            return null;
        }
    }

    /**
     * (advanced users only) allows you to specify a custom css attribute and value to the
     * accessory / its variants. The result will only be applied to the immediate accessories/variants
     * that have a spritesheet.
     *
     * The css attribute must be provided in camelcase.
     * so something like "background-color" should be given as "backgroundColor"
     * and any values should be given as strings.
     *
     * @param {String} ruleName the name of the attribute
     * @param {String} value the value to apply
     * @param {*} exclusiveMode IGNORE THIS AND DON'T PROVIDE A THIRD ARGUMENT
     */
    customCSSRule(ruleName, value, exclusiveMode) {
        if (!exclusiveMode) {
            if (this.parent.variants.length != 0) {
                this.parent.setFilter(ruleName, value, true);
                return;
            }
        } else {
            if (this.spritesheet == null) {
                for (var k of Object.keys(this.accessories)) {
                    this.accessories[k].setFilter(ruleName, value, true);
                }
                for (var k of Object.keys(this.variants)) {
                    this.variants[k].setFilter(ruleName, value, true);
                }
            } else {
                this.imageElem.style[ruleName] = value;
            }
        }
    }

    /**
     * returns the active variant of the specified accessory on this doll or accessory.
     * (if the accessory is multiple accessories deep, this function will return the first active
     * instance of that accessory it finds)
     * if the accessory has no variants, returns the accessory itself.
     * if the accessory has variants, returns the accessory itself.
     * @param {String} accessory
     */
    getAccessory = (accessory) => {
        var searched = this.findFirstAccessory(accessory);
        var result = null;
        if (searched != null)
            result = searched.getCurrentVariant();
        if (result != null) return result;
        else if (searched != null) {
            var accessoryPath = searched != null ? searched.generateDebugString() : this.generateDebugString();
            alert(
                `Error: No selected_variant specified for:
    `+ accessoryPath + `

in directory:
    ` + this.basePath + `.`);
            return null;
        } else {
            console.warn("requested accessory: '" + accessory + '" not found on this armature');
        }

        /*var accessoryOfVariant = currentVariant.accessories[accessory];
        var variantOfAccessoryOfVariant = accessoryOfVariant.getCurrentVariant();
        return variantOfAccessoryOfVariant;
        if(Object.keys(this.variants).length == 0) {
            return this.accessories[accessory].getCurrentVariant();
        } else {
            return this.selected_variant;
        }*/
    }

    generateDebugString = () => {
        var path = "";
        if (this.parent != null) {
            path += this.parent.generateDebugString() + ".";
        }
        path += "" + (this.named == undefined ? "_" : this.named);
        return path;
    }

    /**finds the first instance of the specified accessory
     * which is a descendant of this variant or accessory.
     * (will only search through the chain of selected_variants)
     */
    findFirstAccessory = (accessory) => {

        if (Object.keys(this.accessories).length > 0) {
            if (this.accessories[accessory] != null) {
                return this.accessories[accessory];
            } else {
                for (var k of Object.keys(this.accessories)) {
                    var result = this.accessories[k].findFirstAccessory(accessory)
                    if (result != null)
                        return result;
                }
            }
        } else if (this.selected_variant != null) {
            return this.selected_variant.findFirstAccessory(accessory);
        }
    }

    /**
     * returns the currently active variant of this accessory.
     * if this accessory has no variants, returns this accessory.
     * if this accessory has variants but none are selected, returns null;
     */
    getCurrentVariant = () => {
        if (this.dollInfo.variants == null || Object.keys(this.dollInfo.variants).length == 0) {
            return this;
        } else {
            return this.selected_variant;
        }
    }

    /**
     * computes the appropriate css animation for cycling through this spritesheet
     */
    computeGlobalFrameSequence = () => {
        //var waitResult = await this.whenReady(defer());

        if (this.visible && this.spritesheet != null) {
            var frameWidth = this.width / this.frames;
            var animString = `@keyframes ` + this.keyFrameElem.id + `-anim {
                `;
            for (var i = 0; i < this.global_subframe_sequence.length; i++) {
                var subframe = this.global_subframe_sequence[i];
                var percent = (subframe.spriteframe / this.frames) * 100;
                var time = subframe.play_t;
                //var anchorage = "translate(" + this.drawInfo.framed_transforms[i].x + "px, " + this.drawInfo.framed_transforms[i].y + "px)";
                //var scaleage = "scale(" + this.globalScale.x + ", " + this.globalScale.y + ")";
                animString += (100 * time) + `% {transform: translateX(-` + percent + `%) ;}
                `;
            }
            this.keyFrameElem.innerText = animString + "}";
            this.imageElem.style.animationName = this.keyFrameElem.id + `-anim`
            this.imageElem.style.animationDuration = this.loopSpeed + "ms";
            this.imageElem.style.animationTimingFunction = "steps(1)";
            this.imageElem.style.animationIterationCount = "Infinite";
            this.imageElem.style.imageRendering = "pixelated";
        }
    }

    addPhase(interp, shiftBy) {
        var result = [];
        for (var i = 0; i < interp.length; i++) {
            var modded = mod((i - shiftBy), interp.length);
            result.push(interp[modded]);
        }
        return result;
    }

    /**
     * Given a play sequence for this spritesheet, and a play sequence of an ancestor
     * spritesheet
     * @param {*} thisSequence
     * @param {*} parentSequence
     * @param {*} progress
     */
    getInterpoSnappedSequence = (thisSequence, thisFrameTotal, parentSequence, parFrameTotal) => {
        var sortedThis = thisSequence.sort((a, b) => a - b);
        var sortedParent = parentSequence != null ? parentSequence.sort((a, b) => a - b) : null;
        var grouped = [];
        var result = [];
        for (var i = 0; i < thisSequence.length; i++) {
            grouped.push({ frame: thisSequence[i] / thisFrameTotal, parent: "me" });
        }
        if (parentSequence == null) {
            for (var i = 0; i < thisSequence.length; i++) {
                result.push(grouped[i].frame);
            }
            return result;
        }
        for (var i = 0; i < parentSequence.length; i++) {
            grouped.push({ frame: parentSequence[i] / parFrameTotal, parent: "parent" });
        }
        groupedAll.sort((a, b) => a.frame - b.frame);

        function pushUntil(val, of, from, direction) {
            var ct = 0;
            for (var i = from; ct < groupedAll.length; i += direction) {
                if (groupedAll[i].frame == val && groupedAll[i].parent == of) {
                    break;
                } else {
                    result.push(groupedAll[i].val);
                }
                ct++;
            }
        }
        var lastVal = parentSequence[0];
        var lastIndex = 0;
        for (var i = 1; i < parentSequence.length; i++) {
            var targetVal = parentSequence[i];
            var direction = targetVal - lastVal > 0 ? 1 : 0;
            pushUntil(targetVal, "parent", lastIndex, direction);
            lastIndex = i;
            lastVal = targetVal;
        }
        return result;
    }

    hide = async () => {
        var waitResult = await this.whenReady(defer());
        this.visible = false;

        //this.spriteElem.classList.add("hidden");
        if (this.spriteElem != null)
            this.spriteElem.remove();
        this.spriteElem = null;

        if (this.spriteFrameElem != null)
            this.spriteFrameElem.remove();
        this.spriteFrameElem = null;

        if (this.imageElem != null)
            this.imageElem.remove();
        this.imageElem = null;

        if (this.keyFrameElem != null)
            this.keyFrameElem.remove();
        this.keyFrameElem = null;

        if (this.selected_variant != null) {
            this.selected_variant.hide();
        }
        for (var k of Object.keys(this.accessories)) {
            this.accessories[k].hide();
        }

    }

    show = async () => {
        var waitResult = await this.whenReady(defer());
        this.visible = true;
        if (this.spritesheet != null) {
            this.ensureSpriteElems();
            this.spriteElem.classList.remove("hidden");
            //this.spriteElem.appendChild(this.imageElem);
        }
        if (this.selected_variant != null) {
            await this.selected_variant.show();
        }

        if (this.variants != null) {
            for (var k of Object.keys(this.variants)) {
                if (this.variants[k] != this.selected_variant)
                    this.variants[k].hide();
            }
        }
        for (var k of Object.keys(this.accessories)) {
            await this.accessories[k].show();
        }
    }

    /**
     * displays the given variant of this FDoll as the active one.
     * Unless any descendant accessories wll also have their selected variant
     * set to the given variant
     * @param {String} variantName the nae of the variant to set on the doll.
     * note that by default any descendant accessories that contain a variant of the
     * same name will also have that varant selected. To prevent this, set "noRecurse"
     * to true
     * @param {Boolean} noRecurse if set to true, will not attempt to set the variant on
     * any qualifiying descendants
     */
    setVariant = async (variantName, noRecurse, isRecursed) => {
        //var waitResult = await this.whenReady(defer());

        if (variantName == null) {
            this.hide();
        }
        else if (!isRecursed && this.parent != null && Object.keys(this.parent.variants).length > 0) {
            await this.parent.setVariant(variantName, noRecurse, true);
        } else {
            //this.updateRequestedVariantMap(variantName);
            await this.ensureVariant(variantName);
            this.updateRequestedVariantMap(variantName);

            for (var k of Object.keys(this.variants)) {
                if (!noRecurse) {
                    await this.variants[k].setVariant(variantName, noRecurse, true);
                }
                if (this.variants[variantName] != null)
                    this.variants[k].hide();
            }
            if (!noRecurse) {
                for (var k of Object.keys(this.accessories)) {
                    await this.accessories[k].setVariant(variantName, noRecurse, true);
                }
            }

        }
        var chosenVariant = this.variants[variantName];
        if (this.variants[variantName] != null) {
            this.selected_variant = this.variants[variantName];
            this.a_selected_variant_name = variantName;
            if (this.visible)
                this.selected_variant.show();
        }

        /*if (!isRecursed) {
            var waitResult = this.computeGlobals();
            this._forceRestyle(true);
        }*/
    }

    getAccessPath = () => {
        if (this.parent == null) {
            return [];
        } else if (this.parent.selected_variant == this) {
            return this.parent.getAccessPath();
        } else if (this.parent != null && Object.keys(this.parent.accessories).length > 0
            && this.parent.accessories[this.named] != null) {
            var parAccPath = this.parent.getAccessPath();
            parAccPath.push(this.named);
            return parAccPath;
        } else {
            return [];
        }
    }

    updateRequestedVariantMap = (variantName) => {

        //if (this.named != null) {
            if (this.activeVariantMap == null) {
                this.activeVariantMap = {};//this.getRootDollActiveVariantMapMapFor(this.getAccessPath());
            }
        //}
        if (this.dollInfo.variants != null
            && this.dollInfo.variants[variantName] != null) {
            if (this.selected_variant != null)
                delete this.activeVariantMap[this.selected_variant.named];
        }
        this.activeVariantMap[variantName] = true;
    }

    /*getRootDollActiveVariantMapMapFor = (objectPath) => {
        var rootDoll = this.findDollRoot();
        this.rootDoll = rootDoll;
        if (rootDoll.activeVariantMap == null) rootDoll.activeVariantMap = { __a: {}, active: {}};
        var rootMap = rootDoll.activeVariantMap;
        var currentMap = rootMap;
        for (var i = 0; i < objectPath.length; i++) {
            if (currentMap["__a"][objectPath[i]] == null) currentMap["__a"][objectPath[i]] = { __a: {}, active: {} };
            currentMap = currentMap["__a"][objectPath[i]];
        }
        return currentMap;
    }*/

    /**
     * lazy loading optimization. Variants will not be loaded until required.
     * This checks whether or not the requested variant is both specified and instantiated
     * on this accessory. If it is specified but not instantiated, it instantiates it.
     *
     */
    ensureVariant = async (variantName) => {
        if (this.dollInfo.variants != null &&
            this.dollInfo.variants[variantName] != null
            && this.variants[variantName] == null) {
            await this.instantiateVariant(variantName);
        }
    }

    instantiateVariant = async (variantName) => {
        var variant = this.dollInfo.variants[variantName];
        var anAwaiter = defer();
        this.variants[variantName] = new FDoll(this.basePath, variantName, anAwaiter, this, null, variant);
        //this.variants[k].setParent(this, attachChild,  toChild);
        var wait = await anAwaiter;
        //this.variants[variantName].nameAs(variantName);
        this.updateRequestedVariantMap(variantName);

        for (var k of Object.keys(this.activeVariantMap)) {
            await this.variants[variantName].setVariant(k);
        }
    }

    copyObj(obj) {
        if (typeof obj != "function") {
            return JSON.parse(JSON.stringify(obj));
        } else {
            return obj;
        }
        /*if(typeof obj == "object" || typeof obj == "array") {
            var copy = {...{}, ...obj};
            return copy;
        } else {
            var copy =  obj;
            return copy;
        }*/
    }

    /**
     * multiplies the values of the keys in set 1
     * by the values of the same keys in set 2 and returns
     * the resulting set of values in a set with the same key names.
     * @param {*} set1
     * @param {*} set2
     */
    mult(set1, set2) {
        var result = {};
        for (var k of Object.keys(set1)) {
            if (set2[k] != null) {
                result[k] = set1[k] * set2[k];
            }
        }
        return result;
    }

    /**
    * adds the values of the keys in set 1
    * to the values of the same keys in set 2 and returns
    * the resulting set of values in a set with the same key names.
    * @param {*} set1
    * @param {*} set2
    */
    add(set1, set2) {
        var result = {};
        for (var k of Object.keys(set1)) {
            if (set2[k] != null) {
                result[k] = set1[k] + set2[k];
            }
        }
        return result;
    }
}

function mod(num, by) {
    return ((num % by) + by) % by;
};

/*convenience functon. returns val if val !=null, otherwise returns ifnull*/
function selfOr(vall, ifnull) {
    return vall == null ? ifnull : vall;
}

var loadBlockers = {};

var baseStyles = document.createElement("style");
baseStyles.innerText =
    `
 .spritesheet_container {
    overflow-x: hidden;
    overflow-y: hidden;
    position: absolute;`+
    //border-style: solid;
    //border-color: red;
    //border-width: 2px;
    `}

 .hidden {
     display: none;
 }
 `;

docHead.appendChild(baseStyles);

repo_name: Area/degrees

license: mit


code:
nameValMaps = {
    npchairlengthrear : {
        "buzz" : 1,
        "short" : 2,
        "trimmed" : 3,
        "flowing" : 4,
        "long" : 5,
        "luxurious" : 6,
        "uncompromising" : 7,
        "repunzotic" : 8,
        "endless" : 9
    },
    npcbodysize : {
        "tiny_body" : 1,
        "small_body" : 2,
        "petite_body" : 3,
        "medium_body" : 4,
        "large_body" : 5,
        "huge_body" : 6,
        "giant_body" : 7,
        "joke_body" : 11
    },
    npcbreastsize : {
        "flat" : 0,
        "budding" : 1,
        "tiny" : 2,
        "small" : 3,
        "pert" : 4,
        "modest": 5,
        "full": 6,
        "large": 7,
        "ample": 8,
        "massive": 9,
        "huge": 10,
        "gigantic": 11,
        "enormous": 12
    },
    npcpenissize: {
        "none" : 0,
        "tiny" : 1,
        "small" : 2,
        "medium" : 3,
        "large" : 4,
        "huge" : 5
    },
    playerbreastsize : {
        "flat" : 0,
        "tiny" : 1,
        "small" : 2,
        "large" : 3,
        "huge" : 4
    }
}

valNameMaps = {};

function getValueFromName(map, key) {
    return nameValMaps[map][key];
}

function getNameFromValue(map, key) {
    return valNameMaps[map][key];
}

function generateReverseMaps() {
    reversal(nameValMaps, valNameMaps);
    reversal(valNameMaps, nameValMaps);
}

function reversal(source, target) {
    for(var mapName of Object.keys(source)) {
        for(var k of Object.keys(source[mapName])) {
            var value = source[mapName][k];
            if(target[mapName] == null) {
                target[mapName] = {};
            }
            if(target[mapName][value] == null) {
                target[mapName][value] = k
            }
        }
    }
}

generateReverseMaps();


code:
var NPC1 =
{
  penis: "anus",
  vagina: "none",
  bodysize: 4,
  chest: 0,
  lactation: 0,
  lefthand: 0,
  righthand: "throat",
  anus: 0,
  mouth: 0,
  gender: "m",
  description: "toned",
  fullDescription: "toned woman",
  insecurity: "ethics",
  pronoun: "f",
  pronouns: {
    he: 0,
    his: 0
  },
  penissize: 4,
  breastsize: 7,
  bottomsize: 0,
  ballssize: 0,
  penisdesc: "humongous cock",
  hairlengthrear: 3,
  hairstylerear: "femme",
  hairstylefront: "bangs",
  health: 250,
  skincolour: "white",
  teen: 0,
  adult: 1,
  intro: 0,
  speechpenisescape: 0,
  speechvaginaescape: 0,
  speechanusescape: 0,
  type: 0,
  stance: 0,
  monster: 0,
  hasPriority: false
}
var NPC2 =
{
  penis: "mouthimminent",
  vagina: "none",
  bodysize: 2,
  chest: 0,
  lactation: 1,
  lefthand: 0,
  righthand: "pen_right_cheek",
  anus: 0,
  mouth: 0,
  gender: "m",
  description: "lithe",
  fullDescription: "lithe woman",
  insecurity: "ethics",
  pronoun: "f",
  pronouns: {
    he: 0,
    his: 0
  },
  penissize: 4,
  breastsize: 7,
  bottomsize: 0,
  ballssize: 0,
  penisdesc: "massive cock",
  hairlengthrear: 6,
  hairstylerear: "pigtails",
  hairstylefront: "swept",
  health: 175,
  skincolour: "white",
  teen: 0,
  adult: 1,
  intro: 0,
  speechpenisescape: 0,
  speechvaginaescape: 0,
  speechanusescape: 0,
  type: 0,
  stance: 0,
  monster: 0,
  hasPriority: false
};
var NPC3 =
{
  penis: "none",
  vagina: "none",
  breastsdesc: 0,
  chest: "none",
  lactation: 0,
  lefthand: "none",
  righthand: "none",
  hairlengthrear: 3,
  hairstylerear: "ponytail",
  hairstylefront: "bangs",
  bodysize: 3,
  anus: 0,
  mouth: "none",
  gender: 0,
  description: 0,
  fullDescription: "slight woman",
  insecurity: 0,
  pronoun: 0,
  pronouns: {
    he: 0,
    his: 0
  },
  penissize: 0,
  breastsize: 0,
  bottomsize: 0,
  ballssize: 0,
  penisdesc: 0,
  breastdesc: 0,
  health: 0,
  skincolour: "white",
  teen: 0,
  adult: 1,
  intro: 0,
  speechpenisescape: 0,
  speechvaginaescape: 0,
  speechanusescape: 0,
  type: 0,
  stance: 0,
  monster: 0
};
var NPC4 =
{
  penis: "none",
  vagina: "none",
  breastsdesc: 0,
  chest: "none",
  lactation: 0,
  lefthand: "none",
  hairstylefront: "bangs",
  hairstylerear: "ponytail",
  bodysize: 4,
  righthand: "none",
  anus: 0,
  mouth: "none",
  gender: 0,
  description: 0,
  fullDescription: "slender woman",
  insecurity: 0,
  pronoun: 0,
  pronouns: {
    he: 0,
    his: 0
  },
  penissize: 0,
  breastsize: 0,
  bottomsize: 0,
  ballssize: 0,
  penisdesc: 0,
  breastdesc: 0,
  health: 0,
  skincolour: "white",
  teen: 1,
  adult: 0,
  intro: 0,
  speechpenisescape: 0,
  speechvaginaescape: 0,
  speechanusescape: 0,
  type: 0,
  stance: 0,
  monster: 0
}
var NPC5 =
{
  penis: "none",
  vagina: "none",
  breastsdesc: 0,
  chest: "none",
  hairstylefront: "bangs",
  hairstylerear: "ponytail",
  bodysize: "medium",
  lactation: 3,
  lefthand: "none",
  righthand: "none",
  anus: 0,
  mouth: "none",
  gender: 0,
  description: 0,
  fullDescription: "graceful woman",
  insecurity: 0,
  pronoun: 0,
  pronouns: {
    he: 0,
    his: 0
  },
  penissize: 0,
  breastsize: 0,
  bottomsize: 0,
  ballssize: 0,
  penisdesc: 0,
  breastdesc: 0,
  health: 0,
  skincolour: "white",
  teen: 1,
  adult: 0,
  intro: 0,
  speechpenisescape: 0,
  speechvaginaescape: 0,
  speechanusescape: 0,
  type: 0,
  stance: 0,
  monster: 0
};
var NPC6 =
{
  penis: "none",
  vagina: "none",
  breastsdesc: 0,
  chest: "none",
  hairstylefront: "swept",
  hairstylerear: "ponytail",
  bodysize: 2,
  lactation: 0,
  lefthand: "none",
  righthand: "none",
  anus: 0,
  mouth: "none",
  gender: 0,
  description: 0,
  fullDescription: 0,
  insecurity: 0,
  pronoun: 0,
  pronouns: {
    he: 0,
    his: 0
  },
  penissize: 0,
  breastsize: 0,
  bottomsize: 0,
  ballssize: 0,
  penisdesc: 0,
  breastdesc: 0,
  health: 0,
  skincolour: 0,
  teen: 0,
  adult: 0,
  intro: 0,
  speechpenisescape: 0,
  speechvaginaescape: 0,
  speechanusescape: 0,
  type: 0,
  stance: 0,
  monster: 0
};

var player = {
  bodysize: 1,
  gender: "m",
  sex: "m",
  penis: 0,
  vagina: 0,
  penissize: 0,
  breastsize: 0,
  bottomsize: 0,
  ballssize: 0,
  pronoun: 0,
  pronouns: {
    he: 0,
    his: 0
  },
  virginity: {
    anal: false,
    oral: false,
    penile: true,
    vaginal: true,
    temple: true
  },
  gender_appearance: "m",
  gender_appearance_without_overwear: "m",
  penisExist: true,
  vaginaExist: false,
  ballsExist: true,
  gender_appearance_factors: [
    {
      femininity: -100000,
      factor: "Penis visible"
    },
    {
      femininity: -50,
      factor: "Exposed breasts"
    },
    {
      femininity: 19,
      factor: "Hair length"
    },
    {
      femininity: 50,
      factor: "Bottom size (100% visible)"
    },
    {
      femininity: -200,
      factor: "Natural features"
    },
    {
      femininity: -119,
      factor: "Toned muscles"
    },
    {
      femininity: 0,
      factor: "Posture (x1 effectiveness due to English skill)"
    },
    {
      femininity: 50,
      factor: "Visible skin markings"
    }
  ],
  gender_appearance_without_overwear_factors: [
    {
      femininity: -100000,
      factor: "Penis visible"
    },
    {
      femininity: -50,
      factor: "Exposed breasts"
    },
    {
      femininity: 19,
      factor: "Hair length"
    },
    {
      femininity: 50,
      factor: "Bottom size (100% visible)"
    },
    {
      femininity: -200,
      factor: "Natural features"
    },
    {
      femininity: -119,
      factor: "Toned muscles"
    },
    {
      femininity: 0,
      factor: "Posture (x1 effectiveness due to English skill)"
    },
    {
      femininity: 50,
      factor: "Visible skin markings"
    }
  ],
  gender_posture: "n",
  femininity: -100250,
  gender_body: "m",
  femininity_without_overwear: -100250
}

var skinColor = {
  tanLoc: ["body", "breasts", "penis", "swimshorts", "swimsuitTop", "swimsuitBottom", "bikiniTop", "bikiniBottom", "tshirt"],
  natural: "light",
  init: true,
  tanningEnabled: true,
  tanImgEnabled: "t",
  range: 17,
  current: {
    test: "",
    body: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    breasts: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    penis: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    swimshorts: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    swimsuitTop: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    swimsuitBottom: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    bikiniTop: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    bikiniBottom: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    tshirt: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)",
    mouth: "hue-rotate(30.187deg) saturate(0.15) brightness(4.29)"
  },
  tanValues: [1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1],
  overwriteEnable: false,
  sunBlock: false,
  overwrite: { hStart: 30, hEnd: 47, sStart: 0.15, sEnd: 0.3, bStart: 4.3, bEnd: 3.4 },
  overwriteValues: { hStart: 45, hEnd: 45, sStart: 0.2, sEnd: 0.4, bStart: 4.5, bEnd: 0.7 }
}

var worn = {
  upper:{
     index:14,
     name:"large towel",
     name_cap:"Large towel",
     variable:"towellarge",
     integrity:10,
     integrity_max:10,
     fabric_strength:20,
     reveal:800,
     word:"a",
     one_piece:1,
     strap:0,
     open:1,
     state:"waist",
     state_base:"waist",
     state_top:"chest",
     state_top_base:"chest",
     plural:0,
     colour:"tangerine",
     colour_options:[
        "black",
        "blue",
        "brown",
        "green",
        "pink",
        "purple",
        "red",
        "tangerine",
        "teal",
        "white",
        "yellow"
     ],
     exposed:0,
     exposed_base:0,
     type:[
        "normal"
     ],
     set:"towellarge",
     gender:"n",
     cost:0,
     description:"Not very secure.",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     sleeve_img:0,
     breast_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     exposedcarry:0,
     outfitPrimary:{
        lower:"large towel bottom"
     }
  },
  lower:{
     index:15,
     name:"large towel bottom",
     name_cap:"Large towel bottom",
     variable:"gymbloomers",
     integrity:10,
     integrity_max:10,
     fabric_strength:30,
     reveal:800,
     word:"a",
     one_piece:1,
     skirt:1,
     skirt_down:1,
     state:"hips",
     state_base:"hips",
     plural:0,
     colour:"tangerine",
     colour_options:[
        "black",
        "blue",
        "brown",
        "green",
        "pink",
        "purple",
        "red",
        "tangerine",
        "teal",
        "white",
        "yellow"
     ],
     exposed:0,
     exposed_base:0,
     vagina_exposed:1,
     vagina_exposed_base:1,
     anus_exposed:1,
     anus_exposed_base:1,
     type:[
        "normal"
     ],
     set:"towellarge",
     gender:"n",
     cost:0,
     description:"Not very secure.",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     high_img:0,
     back_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     exposedcarry:0,
     outfitSecondary:[
        "upper",
        "large towel"
     ]
  },
  under_upper:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:0,
     integrity_max:0,
     fabric_strength:0,
     reveal:1000,
     word:"n",
     one_piece:0,
     strap:0,
     open:0,
     state:0,
     state_base:0,
     state_top:0,
     state_top_base:0,
     plural:0,
     colour:0,
     colour_options:[
        "black",
        "blue",
        "brown",
        "green",
        "pink",
        "purple",
        "red",
        "tangerine",
        "teal",
        "white",
        "yellow",
        "custom"
     ],
     exposed:1,
     exposed_base:1,
     type:[
        "naked"
     ],
     set:"under_upper",
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     sleeve_img:0,
     breast_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     exposedcarry:1,
     state_stop:0,
     mainImage:0
  },
  under_lower:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:-10,
     integrity_max:0,
     fabric_strength:0,
     reveal:1000,
     word:"n",
     one_piece:0,
     state:0,
     state_base:0,
     plural:0,
     colour:0,
     colour_options:[
        "black",
        "blue",
        "brown",
        "green",
        "pink",
        "purple",
        "red",
        "tangerine",
        "teal",
        "white",
        "yellow",
        "custom"
     ],
     exposed:1,
     exposed_base:1,
     vagina_exposed:1,
     vagina_exposed_base:1,
     anus_exposed:1,
     anus_exposed_base:1,
     type:[
        "naked"
     ],
     anal_shield:0,
     set:"under_lower",
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     penis_img:0,
     high_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     exposedcarry:1,
     mainImage:0
  },
  genitals:{
     index:2,
     name:"chastity cage",
     name_cap:"Chastity cage",
     variable:"chastitycage",
     integrity:1540,
     integrity_max:2000,
     fabric_strength:15,
     reveal:1000,
     word:"a",
     one_piece:0,
     state:"waist",
     state_base:"waist",
     plural:1,
     colour:0,
     colour_options:[

     ],
     exposed:1,
     exposed_base:1,
     vagina_exposed:0,
     vagina_exposed_base:0,
     anus_exposed:1,
     anus_exposed_base:1,
     type:[
        "chastity",
        "cage"
     ],
     anal_shield:null,
     set:"genitals",
     gender:"m",
     cost:0,
     description:"Restrictive.",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     penis_img:0,
     high_img:0,
     cursed:1,
     location:0,
     hideUnderLower:[
        "plain panties",
        "bikini bottoms",
        "lace panties",
        "briefs",
        "school swimsuit bottom",
        "school swim shorts",
        "leotard bottom",
        "full body leotard bottom",
        "skimpy leotard bottom",
        "foreign school swimsuit bottom",
        "swimsuit bottom",
        "bunny leotard bottom",
        "boyshorts",
        "catgirl panties",
        "G-string",
        "microkini bottom",
        "speedo",
        "striped panties",
        "thong"
     ],
     iconFile:0,
     accIcon:0,
     origin:"temple"
  },
  head:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:0,
     integrity_max:0,
     fabric_strength:0,
     reveal:1,
     word:"n",
     plural:0,
     colour:0,
     colour_options:[

     ],
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     back_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     mainImage:0
  },
  face:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:10,
     integrity_max:10,
     fabric_strength:20,
     reveal:1,
     word:"a",
     plural:0,
     colour:0,
     colour_options:[

     ],
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     mainImage:0
  },
  neck:{
     index:1,
     name:"collar",
     name_cap:"Collar",
     variable:"collar",
     integrity:400,
     integrity_max:400,
     fabric_strength:20,
     reveal:1000,
     word:"n",
     plural:1,
     colour:0,
     colour_options:[

     ],
     type:[
        "fetish"
     ],
     gender:"n",
     cost:20000,
     description:"Requires a special tool to unlock.",
     shop:[

     ],
     collared:1,
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     cursed:1,
     location:0,
     iconFile:"Collar.png",
     accIcon:0
  },
  legs:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:10,
     integrity_max:10,
     fabric_strength:20,
     reveal:1,
     word:"a",
     plural:0,
     colour:0,
     colour_options:[

     ],
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     state_base:0,
     mainImage:0
  },
  feet:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:10,
     integrity_max:10,
     fabric_strength:20,
     reveal:1,
     word:"a",
     plural:0,
     colour:0,
     colour_options:[

     ],
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     mainImage:0
  },
  over_upper:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:0,
     integrity_max:0,
     fabric_strength:0,
     reveal:1000,
     word:"n",
     strap:0,
     open:0,
     zip:0,
     state:0,
     state_base:0,
     state_top:0,
     state_top_base:0,
     plural:0,
     colour:0,
     colour_options:[
        "black",
        "blue",
        "brown",
        "green",
        "pink",
        "purple",
        "red",
        "tangerine",
        "teal",
        "white",
        "yellow",
        "custom"
     ],
     exposed:2,
     exposed_base:2,
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     sleeve_img:0,
     breast_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     mainImage:0
  },
  over_lower:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:0,
     integrity_max:0,
     fabric_strength:0,
     reveal:1000,
     word:"n",
     skirt:0,
     skirt_down:0,
     state:0,
     state_base:0,
     plural:0,
     colour:0,
     colour_options:[

     ],
     exposed:2,
     exposed_base:2,
     vagina_exposed:1,
     vagina_exposed_base:1,
     anus_exposed:1,
     anus_exposed_base:1,
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     high_img:0,
     back_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     mainImage:0
  },
  over_head:{
     index:0,
     name:"naked",
     name_cap:"Naked",
     variable:"naked",
     integrity:0,
     integrity_max:0,
     fabric_strength:0,
     reveal:1,
     word:"n",
     plural:0,
     colour:0,
     colour_options:[

     ],
     type:[
        "naked"
     ],
     gender:"n",
     cost:0,
     description:"naked",
     shop:[

     ],
     accessory:0,
     accessory_colour:0,
     accessory_colour_options:[

     ],
     back_img:0,
     cursed:0,
     location:0,
     iconFile:0,
     accIcon:0,
     mainImage:0
  }
}

var SugarCube = {
  State: {
    variables: {
      enemyno: 1,
      position: "doggy",
      skinColor: skinColor,
      hairlengthstage: "shoulder",
      haircolorfilter: "hue-rotate(40deg) saturate(60%) brightness(71%) contrast(100%)",
      arousal: 0,
      arousalmax: 10000,
      trauma: 0,
      traumamax: 5000,
      pain: 0,
      NPCList: [NPC1, NPC2, NPC3, NPC4, NPC5, NPC6],
      player: player,
      worn: worn
    }
  }
};


code:
function setEnemyCount(elem) {
    enemyControlsContainer = document.getElementById("enemy-controls-container");
    var currentEnemyCount = enemyControlsContainer.querySelectorAll(".enemy-controller").length;
    SugarCube.State.variables.enemyno = elem.value;
    addOrRemoveControllers(elem.value, currentEnemyCount);
}

function addOrRemoveControllers(newCount, oldCount) {
    var count = newCount - oldCount;
    for (var i = count; i < 0; i++) {
        enemyControlsContainer.lastElementChild.remove();
    }
    var cachedController = enemyControlsCache.querySelector(".enemy-controller");
    for (var i = 0; i < count; i++) {
        var clone = cachedController.cloneNode(true);
        clone.setAttribute("data-NPC", oldCount + (i));
        clone.querySelector("h3").innerText = "NPC-" + (oldCount + (i + 1))
        enemyControlsContainer.appendChild(clone);
        updateControllerValues(clone);
    }
}

function updateClothesDropdown() {
    var bottomAcc = playerDoll.getAccessory("bottom").getAccessory("clothes");
    var underlower = bottomAcc.getAccessory("under_lower").parent;
    var playerControls = document.querySelector("#player-controls div");
    var underLowerSelect = dropdownFromVariants(underlower, "under_lower", (el) => { updateSubVariants(el); updateById(el); }, updateById);
    playerControls.appendChild(underLowerSelect);
    underLowerSelect.forPart = "under_lower";
    underLowerSelect.appendChild(makecolorInputFor(SugarCube.State.variables.worn.under_lower));

    var lower = bottomAcc.getAccessory("lower").parent;
    var lowerSelect = dropdownFromVariants(lower, "lower", (el) => { updateSubVariants(el); updateById(el); }, updateById);
    playerControls.appendChild(lowerSelect);
    lowerSelect.forPart = "lower";
    lowerSelect.appendChild(makecolorInputFor(SugarCube.State.variables.worn.lower));

}

function dropdownFromVariants(variantContainer, label, onchange, onsubchange) {
    var select = document.createElement("select");
    select.classList.add("variants");
    select.variantContainer = variantContainer;
    select.onsubchange = onsubchange;
    select.onchange = () => { onchange(select) };
    var variantsHolder = variantContainer.dollInfo == null ? variantContainer : variantContainer.dollInfo;
    for (var k of Object.keys(variantsHolder.variants)) {
        var opt = document.createElement("option");
        opt.value = k;
        opt.innerText = k;
        select.appendChild(opt);
    }
    var selectorDiv = document.createElement("div");
    selectorDiv.classList.add("selector");
    selectorDiv.innerHTML = label;
    selectorDiv.appendChild(select);
    return selectorDiv;
}

function updateSubVariants(elem) {
    var subvariants = elem.parentNode.querySelector(".subvariants");
    if (subvariants != null)
        subvariants.remove();
    var variantContainer = elem.variantContainer;
    var variantHolder = variantContainer.dollInfo == null? variantContainer.variants : variantContainer.dollInfo.variants;
    var result = dropdownFromVariants(variantHolder[elem.value], "", elem.onsubchange);
    result = result.querySelector("select");
    result.classList.add("subvariants");
    result.onchange = elem.onsubchange;
    elem.parentNode.appendChild(result);
    SugarCube.State.variables.worn[variantContainer.named].variable = elem.value;
    updateState(loopSpeed);
}

function updateById(elem) {
    var val = elem.value == null ? elem.target.value : elem.value;
    var targ = elem.target == null ? elem : elem.target;
    var forPart = targ.parentNode.forPart;
    SugarCube.State.variables.worn[forPart].state = val; //targ.variantContainer.setVariant(val);
    updateState(loopSpeed);
}

function makecolorInputFor(item) {
    var colorer = document.createElement("input")
    colorer.setAttribute("type", "color");
    colorer.onchange = (e) => {
        colorItem(e.target.value, item);
    }
    return colorer;
}

function colorItem(value, item) {
    var hsb = toHSB(value);
    var filterString = "hue-rotate(" + hsb.h + "deg) saturate(" + hsb.s + "%) brightness(" + hsb.b+ "%)";
    item.currentcolor = filterString;
    updateState(loopSpeed);
}

/**sets the controller values to reflect its corresponding NPC */
function updateControllerValues(controller) {
    var NPCNum = controller.getAttribute("data-NPC");
    var NPCInfo = SugarCube.State.variables.NPCList[NPCNum];
    controller.querySelector(".type").value = NPCInfo.type;
    controller.querySelector(".breastsize").value = NPCInfo.breastsize;
    controller.querySelector(".hairstylerear").value = NPCInfo.hairstylerear;
    controller.querySelector(".hairstylerear").value = NPCInfo.hairstylerear;
    controller.querySelector(".penissize").value = NPCInfo.penissize;
    controller.querySelector(".bodysize").value = NPCInfo.bodysize;
    controller.querySelector(".penis").value = NPCInfo.penis;

}

function toHSB(h) {
    r = "0x" + h[1] + h[2];
    g = "0x" + h[3] + h[4];
    b = "0x" + h[5] + h[6];
    var hsv = rgbToHsv(r, g, b);
    //compensate for CSS's horrible hue-rotate math the best we can.
    var mod = Math.abs(180-hsv.h)%180;
    if(mod == 0) mod = 180;
    var satMult = 4.5*((100-hsv.s)/100);
    var b = hsv.v + (hsv.v * (satMult*(mod/180.0)));
    return {h:hsv.h, s:hsv.s, b:b};
}
function rgbToHsv(r, g, b) {
    let rabs, gabs, babs, rr, gg, bb, h, s, v, diff, diffc, percentRoundFn;
    rabs = r / 255;
    gabs = g / 255;
    babs = b / 255;
    v = Math.max(rabs, gabs, babs),
    diff = v - Math.min(rabs, gabs, babs);
    diffc = c => (v - c) / 6 / diff + 1 / 2;
    percentRoundFn = num => Math.round(num * 100) / 100;
    if (diff == 0) {
        h = s = 0;
    } else {
        s = diff / v;
        rr = diffc(rabs);
        gg = diffc(gabs);
        bb = diffc(babs);

        if (rabs === v) {
            h = bb - gg;
        } else if (gabs === v) {
            h = (1 / 3) + rr - bb;
        } else if (babs === v) {
            h = (2 / 3) + gg - rr;
        }
        if (h < 0) {
            h += 1;
        }else if (h > 1) {
            h -= 1;
        }
    }
    return {
        h: Math.round(h * 360),
        s: percentRoundFn(s * 100),
        v: percentRoundFn(v * 100)
    };
}

function hsvToRgb(h) {
    var r, g, b, i, f, p, q, t;
    if (arguments.length === 1) {
        s = h.s/100, v = h.v/100, h = h.h/360;
    }
    i = Math.floor(h * 6);
    f = h * 6 - i;
    p = v * (1 - s);
    q = v * (1 - f * s);
    t = v * (1 - (1 - f) * s);
    switch (i % 6) {
        case 0: r = v, g = t, b = p; break;
        case 1: r = q, g = v, b = p; break;
        case 2: r = p, g = v, b = t; break;
        case 3: r = p, g = q, b = v; break;
        case 4: r = t, g = p, b = v; break;
        case 5: r = v, g = p, b = q; break;
    }
    return {
        r: Math.round(r * 255),
        g: Math.round(g * 255),
        b: Math.round(b * 255)
    };
}


code:
var playerDoll = null;
var humanCache = [];
var beastCache = [];

var humansActive = [];
var beastsActive = [];
var enemiesActive = [];
var activeNPCList = [];

var insertListener = async function(event){

	if (event.animationName == "nodeInserted") {
        updateState();
	}
}

async function updateState(msPerLoop, infoDiv) {
    //console.log("BATTLE BEGIN! ", event, event.target);
        globals = SugarCube.State.variables;
        if(playerDoll == null) {
            var anAwaiter = defer();
            playerDoll = new FDoll("dolls/player/player_sex.js", "player", anAwaiter, null, infoDiv);
            var wait = await anAwaiter;
        }
        if(msPerLoop == null) {
            msPerLoop = 500;
        }

        var canvasElem = document.querySelector("#divsex");
        canvasElem.innerHTML = '';
        cleanSlate();
        playerDoll.setLoopSpeed(msPerLoop);
        for(var i = 0; i<activeNPCList.length; i++ ) {
            activeNPCList[i].setLoopSpeed(msPerLoop);
        }
        await initNPCs(infoDiv);
        await updatePlayerDraw();
        await updateNPCs(activeNPCList);
        drawAll();
}

async function initNPCs(infoDiv) {
    var npcList = SugarCube.State.variables.NPCList;
    var enemyCount = SugarCube.State.variables.enemyno;
    var humansRequested = 0;
    activeNPCList = [];
    var beastsRequested = 0;
    orificeDibs = {};

    for(var i=0; i<enemyCount; i++) {
        var npcState = npcList[i];
        if (npcState.type != 0) {
            beastsRequested++;
            var beast = await getOrCreateBeastFromCache(beastsRequested, npcState.type, infoDiv);
            beast.NPCInfo = npcState;
            activeNPCList.push(beast);
            await updateEnemyState(beast);
            //human.renderTo(canvasElem);
        } else {
            humansRequested++;
            var human = await getOrCreateHumansFromCache(humansRequested, infoDiv);
            human.NPCInfo = npcState;
            activeNPCList.push(human);
            await updateEnemyState(human);
            //human.renderTo(canvasElem);
        }
    }
}

async function updateNPCs(activeNPCList) {
    for (var i = 0; i<activeNPCList.length; i++) {
        updateEnemyDraw((activeNPCList[i]));
    }
}

function drawAll() {
    var canvasElem = document.querySelector("#divsex");
    window.requestAnimationFrame( ()=> {
        playerDoll.renderTo(canvasElem);
        for (var i = 0; i<activeNPCList.length; i++) {
            (activeNPCList[i]).renderTo(canvasElem);
        }
    });

}

async function getOrCreateHumansFromCache(humansRequested, infoDiv) {
    if (humanCache.length >= humansRequested) {
        return humanCache[humansRequested - 1];
    } else {
        var anAwaiter = defer();
        var newHuman = new FDoll("dolls/human/human.js", "human", anAwaiter, null, infoDiv);
        var wait = await anAwaiter;
        humanCache.push(newHuman);
        return newHuman;
    }
}

async function getOrCreateBeastFromCache(beastsRequested, type, infoDiv) {
    if (beastCache.length >= beastsRequested) {
        return beastCache[beastsRequested - 1];
    } else {
        var anAwaiter = defer();
        var newBeast = new FDoll("dolls/beast/beast.js", type, anAwaiter, null, infoDiv);
        var wait = await anAwaiter;
        beastCache.push(newBeast);
        return newBeast;
    }
}

function getPlayerPartDrawInfo(partName) {
    throw error; // todo: write this
}

const propDrawRules = {
    pillory: {},
    wall: {},
    stable: {}
}

document.addEventListener("animationstart", insertListener, false); // standard + firefox
document.addEventListener("MSAnimationStart", insertListener, false); // IE
document.addEventListener("webkitAnimationStart", insertListener, false); // Chrome + Safari