code:
// Handles mostly cursor animations on tool activation
class Animation {
    constructor() {
        this.keyframes = {};
        this.interpolations = {};
        this.actions = {};
    }

    addKeyframe(t, keyframe, action) {
        for (const key in keyframe) {
            if (!(key in this.keyframes)) {
                this.keyframes[key] = {};
                this.interpolations[key] = "linear";
            }
            this.keyframes[key][t] = keyframe[key];
            if (isNaN(keyframe[key])) {
                this.interpolations[key] = "flat";
            }
        }
        if (action) {
            this.actions[t] = action;
        }
    }

    interpolate(value1, value2, t1, t2, t, method) {
        switch (method) {
            case "flat":
                return value1;
            case "linear":
                return (t-t1)/(t2-t1) * (value2-value1) + value1;
            case "rotation":
                // https://gist.github.com/shaunlebron/8832585
                let da = (value2 - value1) % (Math.PI*2);
                let diff = 2 * da % (Math.PI*2) - da;
                return value1 + diff * (t-t1)/(t2-t1);
        }
    }

    getValues(t) {
        let ret = {}
        for (const key in this.keyframes) {
            if (t in this.keyframes[key]) {
                ret[key] = this.keyframes[key][t];
            } else {
                // TODO this is crap and should be replaced with a BST
                // but the actual length of the keywords will probably remain small
                let minLargestT = Infinity;
                let maxSmallestT = 0;
                for (const tt_str in this.keyframes[key]) {
                    let tt = parseInt(tt_str); // Dict keys are provided as strings, even though they should be integers
                    if (tt > t && minLargestT > tt) {
                        minLargestT = tt;
                    }
                    if (tt < t && maxSmallestT < tt) {
                        maxSmallestT = tt;
                    }
                }
                if (minLargestT == Infinity) {
                    ret[key] = this.keyframes[key][maxSmallestT];
                } else {
                    ret[key] = this.interpolate(this.keyframes[key][maxSmallestT],
                                                this.keyframes[key][minLargestT],
                                                maxSmallestT, minLargestT,
                                                t, this.interpolations[key]);
                }
            }
        }
        return ret;
    }

    callActions(t) {
        // To prevent numbers like 3.000000006 from not registering properly
        t = Math.round(t * 100) / 100;
        if (t in this.actions) {
            this.actions[t]();
        }
    }
}

repo_name: Tomajc/torture

license: mit


code:
// Represents a body part, encompasses item and cloth management mostly
class BodyPart {

    static CONTOUR = 0;
    static SKIN = 1;
    static MEAT = 2;
    static ORGANS = 3;
    static BONE = 4;
    static MEAT_ORGANS = 5; // Drawing option which combines meat and organs

    static CLOTH_COUNT = 8;

    static MASK_COLOR = "white";

    constructor(name, chara, def, world) {
        this.name = name;
        this.world = world;
        this.def = def;
        this.children = [];
        this.character = chara;
        this.destroyed = false;
        this.joints = [];
        this.extra = false;

        this.clothes = new Array(BodyPart.CLOTH_COUNT);
        this.items = [];
    }

    copyJoints(newBodyPart1, newBodyPart2, lp) {
        for (const i in this.joints) {
            let j = this.joints[i]; // Old joint
            let newJoint;
            let otherBody;
            let newBodyPart;
            if (j.bodyA == this) {
                otherBody = j.bodyB;
                if (j.jointYA <= lp.y) {
                    newJoint = j.copyToNewBodyA(newBodyPart1);
                    newBodyPart = newBodyPart1;
                } else {
                    newJoint = j.copyToNewBodyA(newBodyPart2);
                    newBodyPart = newBodyPart2;
                }
            } else {
                otherBody = j.bodyA;
                if (j.jointYB <= lp.y) {
                    newJoint = j.copyToNewBodyB(newBodyPart1);
                    newBodyPart = newBodyPart1;
                } else {
                    newJoint = j.copyToNewBodyB(newBodyPart2);
                    newBodyPart = newBodyPart2;
                }
            }
            newBodyPart.joints.push(newJoint);
            otherBody.joints[otherBody.joints.indexOf(j)] = newJoint;
        }
    }

    addCloth(slot, image, thickness="thin") {
        this.clothes[slot] = new Cloth(this, image, thickness);
        if (this.extra) {
            this.clothes[slot].setExtra(this.extra);
        }
    }

    /**
     * Adds a new item to the body part
     * @param {*} name Either a string (name of the item) or the conctere item object. In the latter case, the rest of the params should not be defined
     * @param {*} image Displayed upon rendering
     * @param {*} lp Local position relative to body part
     * @param {*} ang Global rotation
     * @param {*} attachStrength How hard is to remove the item. If the tool has a removal strength larger than the item's attachStrength,
     *              the item will get deleted on hit
     */
    addItem(name, image, lp, ang, attachStrength) {
        let item;
        if (typeof name === 'string' || name instanceof String) {
            item = new Item(name, image, this, lp, ang, this.character.params, attachStrength);
        } else {
            item = name;
        }
        this.items.push(item);
        item.params.items.push(item);
    }

    removeItem(item) {
        let index = this.items.indexOf(item);
        this.items.splice(index, 1);
        index = item.params.items.indexOf(item);
        item.params.items.splice(index, 1);
        item.delete();
    }

    transferItemTo(part, item) {
        let index = this.items.indexOf(item);
        this.items.splice(index, 1);
        part.items.push(item);
        item.setParent(part);
    }

    renderItems(ctx) {
        for (const item of this.items) {
            item.render(ctx);
        }
    }

    setExtra(extra) {
        this.extra = extra;
        this.contourExtraCanvas = createCanvas(this.contourCanvas);
        this.skinExtraCanvas = createCanvas(this.skinCanvas);
        this.meatExtraCanvas = createCanvas(this.meatCanvas);
        if (this.hasOrganLayer) this.organsExtraCanvas = createCanvas(this.organsCanvas);
        this.boneExtraCanvas = createCanvas(this.boneCanvas);
        let extraCanvases = this.hasOrganLayer ?
            [this.contourExtraCanvas, this.skinExtraCanvas, this.meatExtraCanvas, this.organsExtraCanvas, this.boneExtraCanvas] :
            [this.contourExtraCanvas, this.skinExtraCanvas, this.meatExtraCanvas, this.boneExtraCanvas];

        for (const extraCanvas of extraCanvases) {
            let ctx = extraCanvas.getContext("2d");
            ctx.save();
            ctx.globalCompositeOperation = "destination-out";
            ctx.drawImage(extra, 0, 0);
            ctx.restore();
        }
    }

    drawOnClothes(image, lp, ang, {scale=1, gco="source-atop", alpha=1} = {}) {
        for (const cloth of this.clothes) {
            if (cloth && cloth.on) {
                cloth.draw(image, lp, ang-this.box2dBody.GetAngle(), {scale: scale, gco: gco, alpha: alpha});
            }
        }
    }

    destroy() {
        this.world.DestroyBody(this.box2dBody);
    }
}


code:
// Character base class
// Extend your custom character from this
class Character {
    constructor(params, rm, style, pose) {
        // Create a standard character definition object
        this.def = createDefaultCharacterDefs(params, rm);
        this.params = params;
        this.world = params.world;
        this.images = rm.images.characters[this.constructor.name];
        this.rm = rm;
        this.browRenderer = new BrowRenderer();
        this.eyeRenderer = new EyeRenderer();
        this.costumeID = style.costume;
        this.options = style.options;
        this.pose = pose;

        // Starting expression while full hp and no other expressions are applied by tools
        this.initialMouth = "close_sad";
        this.initialEyes = "normal";

        // HP of the character
        this.hp = 100;
        // Orgasm state of the character
        this.pleasure = 0;
        this.orgasming = false;
        this.orgasmState = 0;
        this.orgasmTimer = undefined;
        this.diedWhileOrgasming = false;

        // Timers
        this.forcedShadow1Timer = undefined;
        this.forcedShadow2Timer = undefined;
        this.forcedShadow1Flag = false;
        this.forcedShadow2Flag = false;
        this.forcedBlushTimer = undefined;
        this.forcedBlushFlag = false;
        this.forcedSweatTimer = undefined;
        this.forcedSweatStatus = 0;
        this.forcedTearsTimer = undefined;
        this.forcedTearsStatus = 0;

        this.eyeDefs = undefined;
        this.eyeTimer = undefined;
        this.mouthDefs = undefined;
        this.mouthTimer = undefined;

        this.bleedingTimer = undefined;
        this.bleedingAmount = 0;
        this.bleedingDeadly = false;
        this.pleasureDecayTimer = undefined;

        // Callbacks for UI elements
        this.hpCallback = undefined;
        this.bleedingCallback = undefined;
        this.pleasureCallback = undefined;

        this.autoSkin = false;

        // Breath control
        this.canBreathBreast = true;
        this.canBreathBelly = true;
        this.breathFrequency = randfloat(0.9, 1.1);
        this.breathSum = 0;

        // Various flags
        this.shadow1Flag = false;
        this.shadow2Flag = false;
        this.blushFlag = false;
        this.sweatStatus = 0; // 0: no sweat, 1-2: sweat
        this.tearsStatus = 0; // 0: no tears, 1-2-3: tears

        // If destroyed, open or close will be set as a fixed shape
        this.leyeDestroyed = null;
        this.reyeDestroyed = null;
        this.mouthForced = null;
        this.noseDestroyed = false; // Simple boolean

        // Particle effects
        this.spitEffect = new Spit(this.rm, this.params);
        this.peeEffect = new Pee(this.rm, this.params);
        this.squirtEffect = new Squirt(this.rm, this.params);

        // Regions indicate specific trigger areas on the body for tools
        // With respect to head
        this.leyeRegion = circleRegion(I2W(68), I2W(304), I2W(40));
        this.reyeRegion = circleRegion(I2W(-68), I2W(304), I2W(40));
        this.mouthRegion = circleRegion(I2W(0), I2W(412), I2W(35));
        this.noseRegion = circleRegion(I2W(0), I2W(370), I2W(20));
        // With respect to breast
        this.lnippleRegion = circleRegion(I2W(103), I2W(284), I2W(45));
        this.rnippleRegion = circleRegion(I2W(-103), I2W(284), I2W(45));
        // With respect to waist
        this.pussyRegion = circleRegion(I2W(0), I2W(280), I2W(45));
    }

    initClothes() {}

    initExtraParts() {
        this.extraParts = [];
    }

    init(specialDef) {
        let world = this.world;

        // Override basic settings with individual character definitions
        for (let key in specialDef) {
            this.def[key] = specialDef[key];
        }
        // Applying pose definitions
        for (let key in this.pose) {
            this.def[key] = this.pose[key];
        }
        let def = this.def;

        ////////////////////////////////////////////////////////////////////////
        // Defining bodyparts
        ////////////////////////////////////////////////////////////////////////
        this.head = new Head(this, def, world);
        this.neck = new RegularBodyPart("neck", this.head, b2Body.b2_dynamicBody, BODYPART_NECK, def, world, this);
        this.breast = new RegularBodyPart("breast", this.neck, b2Body.b2_dynamicBody, BODYPART_BREAST, def, world, this);
        this.belly = new RegularBodyPart("belly", this.breast, b2Body.b2_dynamicBody, BODYPART_NECK, def, world, this);
        this.waist = new RegularBodyPart("waist", this.belly, b2Body.b2_dynamicBody, BODYPART_2, def, world, this);
        this.lbutt = new RegularBodyPart("lbutt", this.waist, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);
        this.rbutt = new RegularBodyPart("rbutt", this.waist, b2Body.b2_dynamicBody, BODYPART_3, def, world, this);
        this.lleg = new RegularBodyPart("lleg", this.lbutt, b2Body.b2_dynamicBody, BODYPART_2, def, world, this);
        this.rleg = new RegularBodyPart("rleg", this.rbutt, b2Body.b2_dynamicBody, BODYPART_2, def, world, this);
        this.lfoot = new RegularBodyPart("lfoot", this.lleg, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);
        this.rfoot = new RegularBodyPart("rfoot", this.rleg, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);
        this.luparm = new RegularBodyPart("luparm", this.breast, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);
        this.ruparm = new RegularBodyPart("ruparm", this.breast, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);
        this.lloarm = new RegularBodyPart("lloarm", this.luparm, b2Body.b2_dynamicBody, BODYPART_2, def, world, this);
        this.rloarm = new RegularBodyPart("rloarm", this.ruparm, b2Body.b2_dynamicBody, BODYPART_2, def, world, this);
        this.lhand = new RegularBodyPart("lhand", this.lloarm, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);
        this.rhand = new RegularBodyPart("rhand", this.rloarm, b2Body.b2_dynamicBody, BODYPART_1, def, world, this);

        this.initExtraParts();

        this.bodyParts = [
            this.neck, this.belly,
            this.lfoot, this.rfoot,
            this.lleg, this.rleg,
            this.breast,
            this.lbutt, this.rbutt,
            this.waist,
            this.luparm, this.ruparm,
            this.lloarm, this.rloarm,
            this.lhand, this.rhand,
            this.head,
        ];

        this.initClothes();
        this.eyeRenderer.initMorphs();
        this.browRenderer.initMorphs();

        // Set a standard face
        this.setDefaultEyes();
        this.setDefaultMouth();
    }

    ////////////////////////////////////////////////////////////////////////
    // Render
    ////////////////////////////////////////////////////////////////////////

    renderBackHair(ctx) {
        this.head.renderBackHair(ctx);
    }

    renderBody(ctx) {
        let renderOrder = this.bodyParts.slice(0, -1); // Exclude head

        for (const part of renderOrder) {
            part.render(ctx, BodyPart.BONE);
        }
        for (const part of this.extraParts) {
            part.render(ctx, BodyPart.BONE, true);
        }

        for (const part of renderOrder) {
            part.render(ctx, BodyPart.ORGANS);
        }
        for (const part of this.extraParts) {
            part.render(ctx, BodyPart.ORGANS, true);
        }

        for (const part of renderOrder) {
            part.render(ctx, BodyPart.MEAT);
        }
        for (const part of this.extraParts) {
            part.render(ctx, BodyPart.MEAT, true);
        }

        if (!this.autoSkin) {
            for (const part of renderOrder) {
                part.render(ctx, BodyPart.SKIN);
                part.render(ctx, BodyPart.CONTOUR);
            }
            for (const part of this.extraParts) {
                part.render(ctx, BodyPart.SKIN, true);
                part.render(ctx, BodyPart.CONTOUR, true);
            }
        }

        for (let i = 0; i < BodyPart.CLOTH_COUNT; i++) {
            for (const part of renderOrder) {
                if (part.clothes[i]) {
                    part.clothes[i].render(ctx);
                }
            }
            for (const part of this.extraParts) {
                if (part.clothes[i]) {
                    part.clothes[i].render(ctx, true);
                }
            }
        }

        for (const part of renderOrder) {
            part.renderItems(ctx);
        }
    }

    renderHead(ctx) {
        this.head.render(ctx);
        for (let i = 0; i < BodyPart.CLOTH_COUNT; i++) {
            if (this.head.clothes[i]) {
                this.head.clothes[i].render(ctx);
            }
        }
        this.head.renderItems(ctx);
    }

    ////////////////////////////////////////////////////////////////////////
    // Update
    ////////////////////////////////////////////////////////////////////////
    breath() {
        if (this.dead()) return;
        let dy = Math.cos(this.params.tick/100*2*Math.PI * this.breathFrequency)/SCALE/6;
        let prevDy = Math.cos((this.params.tick-1)/100*2*Math.PI * this.breathFrequency)/SCALE/6;
        this.breathSum += dy;
        if (dy > 0 && prevDy <= 0) {
            dy -= this.breathSum;
            this.breathSum = 0; // Attempts to fix drifting due to numerical inaccuracies
        }
        // This code accesses local members of Box2D, which should be avoided (TODO)
        // But not sure if this is possible within the realm of this lib
        if (this.canBreathBreast)   this.breast.parentJoint.joint.m_localAnchor1.y += dy * 0.15;
        if (this.canBreathBreast)   this.belly.parentJoint.joint.m_localAnchor1.y += dy * 0.55;
        if (this.canBreathBelly)    this.waist.parentJoint.joint.m_localAnchor1.y -= dy * 0.35;
    }

    updateHeadPosition() {
        // Tries to keep the head and neck straight
        if (this.alive()) {
            let desiredNeckHeadAngle = 0;
            let neckHeadAngle = this.neck.parentJoint.joint.GetJointAngle()/Math.PI*180;
            let tmpHeadAngularSpeed = (desiredNeckHeadAngle-neckHeadAngle)*-0.75;
            this.head.box2dBody.SetAngularVelocity(this.head.box2dBody.GetAngularVelocity() + tmpHeadAngularSpeed);
            let desiredBreastNeckAngle = 0;
            let breastNeckAngle = this.breast.parentJoint.joint.GetJointAngle()/Math.PI*180;
            let tmpNeckAngularSpeed = (desiredBreastNeckAngle-breastNeckAngle)*-0.75;
            this.neck.box2dBody.SetAngularVelocity(this.neck.box2dBody.GetAngularVelocity() + tmpNeckAngularSpeed);
        }
    }

    updateHeadCanvas() {
        this.head.updateCanvas();
    }

    /**
     * Removes a part from the body parts list and inserts two in its place
     * Used when body parts are sliced into two
     * @param {*} oldPart The part to remove
     * @param {*} newParts The new parts to insert
     */
    updateBodyParts(oldPart, newParts) {
        for (let i = 0; i < this.bodyParts.length; i++) {
            if (this.bodyParts[i] === oldPart) {
                let firstHalf = this.bodyParts.slice(0, i);
                let secondHalf = this.bodyParts.slice(i+1);
                this.bodyParts = Array.prototype.concat(firstHalf, newParts, secondHalf);
                break;
            }
        }
        for (let i = 0; i < this.extraParts.length; i++) {
            if (this.extraParts[i] === oldPart) {
                let firstHalf = this.extraParts.slice(0, i);
                let secondHalf = this.extraParts.slice(i+1);
                this.extraParts = Array.prototype.concat(firstHalf, newParts, secondHalf);
                return;
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////
    // Facial expression
    ////////////////////////////////////////////////////////////////////////

    // Temporarily forces various effects for a given duration, then reverts them to the initial state
    // TODO less redundancy
    forceShadow1(duration) {
        clearTimeout(this.forcedShadow1Timer);
        this.forcedShadow1Flag = true;
        this.forcedShadow1Timer = setTimeout(() => this.forcedShadow1Flag = false, TIMESTEP * duration);
    }
    forceShadow2(duration) {
        clearTimeout(this.forcedShadow2Timer);
        this.forcedShadow2Flag = true;
        this.forcedShadow2Timer = setTimeout(() => this.forcedShadow2Flag = false, TIMESTEP * duration);
    }
    forceBlush(duration) {
        clearTimeout(this.forcedBlushTimer);
        this.forcedBlushFlag = true;
        this.forcedBlushTimer = setTimeout(() => this.forcedBlushFlag = false, TIMESTEP * duration);
    }
    forceSweat(duration, status) {
        clearTimeout(this.forcedSweatTimer);
        this.forcedSweatStatus = status;
        this.forcedSweatTimer = setTimeout(() => this.forcedSweatStatus = 0, TIMESTEP * duration);
    }
    forceTears(duration, status) {
        clearTimeout(this.forcedTearsTimer);
        this.forcedTearsStatus = status;
        this.forcedTearsTimer = setTimeout(() => this.forcedTearsStatus = 0, TIMESTEP * duration);
    }

    // Set a standard facial expression
    setDefaultEyes() {
        if (this.alive()) {
            if (this.hp > 95 && this.pleasure < 10) {
                this.setEyes([
                    [this.initialEyes, randint(70, 120)], // Random blinking interval
                    [this.initialEyes + "_blinking1", 4],
                    [this.initialEyes + "_blinking2", 3]
                ]);
            } else {
                this.setEyes([
                    ["normal", randint(70, 120)],
                    ["normal_blinking1", 4],
                    ["normal_blinking2", 3]
                ]);
            }
        } else {
            this.setEyes([["normal", 10000]]); // The actualy expression is irrelevant here
        }
    }

    setDefaultMouth() {
        // Matrix of ranges of hp and pleasure
        if (this.hp > 95) {
            if (this.pleasure < 10) {
                this.setMouth([[this.initialMouth, 100]]);
            } else if (this.pleasure < 40) {
                this.setMouth([["happy_small", 100]]);
            } else if (this.pleasure < 70) {
                this.setMouth([["happy_medium", 100]]);
            } else {
                this.setMouth([["happy_large", 100]]);
            }
        } else if (this.hp > 50) {
            if (this.pleasure < 10) {
                this.setMouth([["clench_weak", 100]]);
            } else if (this.pleasure < 40) {
                this.setMouth([["close_smile", 100]]);
            } else if (this.pleasure < 70) {
                this.setMouth([["happy_small", 100]]);
            } else {
                this.setMouth([["happy_medium", 100]]);
            }
        } else {
            if (this.pleasure < 10) {
                this.setMouth([["open_small", 100]]);
            } else if (this.pleasure < 40) {
                this.setMouth([["clench_weak", 100]]);
            } else if (this.pleasure < 70) {
                this.setMouth([["close_smile", 100]]);
            } else {
                this.setMouth([["happy_small", 100]]);
            }
        }
    }

    updateEyes(eyesExpression, eyesDir) {
        let eyeCtx = this.head.eyeCanvas.getContext("2d");
        let browCtx = this.head.browCanvas.getContext("2d");

        this.browRenderer.resetStates();
        this.eyeRenderer.resetStates();

        if (this.dead()) {
            // Expression on death is handled separately
            this.eyeRenderer.setState("irisMoveY", I2W(-28));
            this.eyeRenderer.setState("closed", 0.5);
            this.browRenderer.setState("closed", 0.3);
        } else {
            switch (eyesExpression) {
            case "normal": break; // TODO fix this redunancy about blinking
            case "normal_blinking1":
                this.browRenderer.setState("closed", 1);
                this.eyeRenderer.setState("closed", 1);
                break;
            case "normal_blinking2":
                this.browRenderer.setState("closed", 0.5);
                this.eyeRenderer.setState("closed", 0.5);
                break;
            case "angry":
                this.browRenderer.setState("flip", 0.5);
                this.browRenderer.setState("leanInwards", 0.5);
                this.eyeRenderer.setState("closed", 0.2);
                this.eyeRenderer.setState("leanInwards", 0.4);
                break;
            case "angry_blinking1":
                this.browRenderer.setState("flip", 0.5);
                this.browRenderer.setState("leanInwards", 0.5);
                this.browRenderer.setState("closed", 1);
                this.eyeRenderer.setState("closed", 1);
                this.eyeRenderer.setState("leanInwards", 0.4);
                break;
            case "angry_blinking2":
                this.browRenderer.setState("flip", 0.5);
                this.browRenderer.setState("leanInwards", 0.5);
                this.browRenderer.setState("closed", 0.5);
                this.eyeRenderer.setState("closed", 0.6);
                this.eyeRenderer.setState("leanInwards", 0.4);
                break;
            case "sad":
                this.browRenderer.setState("leanOutwards", 0.6);
                this.eyeRenderer.setState("closed", 0.2);
                this.eyeRenderer.setState("leanOutwards", 0.2);
                break;
            case "sad_blinking1":
                this.browRenderer.setState("leanOutwards", 0.6);
                this.browRenderer.setState("closed", 1);
                this.eyeRenderer.setState("closed", 1);
                this.eyeRenderer.setState("leanOutwards", 0.2);
                break;
            case "sad_blinking2":
                this.browRenderer.setState("leanOutwards", 0.6);
                this.browRenderer.setState("closed", 0.5);
                this.eyeRenderer.setState("closed", 0.55);
                this.eyeRenderer.setState("leanOutwards", 0.2);
                break;
            case "ashamed_strong":
                if (eyesDir == "center" || eyesDir == "left") {
                    this.eyeRenderer.setStateL("closed", 1);
                    this.eyeRenderer.setStateL("leanOutwards", 0.2);
                    this.browRenderer.setStateL("leanInwards", 0.7);
                    this.browRenderer.setStateL("flip", 1);
                } else {
                    this.eyeRenderer.setStateL("closed", 0.2);
                    this.eyeRenderer.setStateL("irisMoveX", I2W(-2));
                    this.eyeRenderer.setStateL("irisMoveY", I2W(5));
                    this.eyeRenderer.setStateL("leanOutwards", 0.2);
                    this.browRenderer.setStateL("leanInwards", 0.3);
                    this.browRenderer.setStateL("flip", 1);
                }
                if (eyesDir == "center" || eyesDir == "right") {
                    this.eyeRenderer.setStateR("closed", 1);
                    this.eyeRenderer.setStateR("leanOutwards", 0.2);
                    this.browRenderer.setStateR("leanInwards", 0.7);
                    this.browRenderer.setStateR("flip", 1);
                } else {
                    this.eyeRenderer.setStateR("closed", 0.2);
                    this.eyeRenderer.setStateR("irisMoveX", I2W(-2));
                    this.eyeRenderer.setStateR("irisMoveY", I2W(5));
                    this.eyeRenderer.setStateR("leanOutwards", 0.2);
                    this.browRenderer.setStateR("leanInwards", 0.3);
                    this.browRenderer.setStateR("flip", 1);
                }
                break;
            case "ashamed_weak":
                if (eyesDir == "center" || eyesDir == "left") {
                    this.eyeRenderer.setStateL("closed", 0.2);
                    this.eyeRenderer.setStateL("irisMoveX", I2W(-2));
                    this.eyeRenderer.setStateL("irisMoveY", I2W(5));
                    this.eyeRenderer.setStateL("leanOutwards", 0.2);
                    this.browRenderer.setStateL("leanInwards", 0.3);
                    this.browRenderer.setStateL("flip", 1);
                }
                if (eyesDir == "center" || eyesDir == "right") {
                    this.eyeRenderer.setStateR("closed", 0.2);
                    this.eyeRenderer.setStateR("irisMoveX", I2W(-2));
                    this.eyeRenderer.setStateR("irisMoveY", I2W(5));
                    this.eyeRenderer.setStateR("leanOutwards", 0.2);
                    this.browRenderer.setStateR("leanInwards", 0.3);
                    this.browRenderer.setStateR("flip", 1);
                }
                break;
            case "happy_strong":
                if (eyesDir == "center" || eyesDir == "left") {
                    this.eyeRenderer.setStateL("closed", 1);
                    this.eyeRenderer.setStateL("leanOutwards", 0.4);
                    this.browRenderer.setStateL("leanOutwards", 0.7);
                } else {
                    this.eyeRenderer.setStateL("closed", 0.2);
                    this.eyeRenderer.setStateL("leanOutwards", 0.2);
                    this.browRenderer.setStateL("leanOutwards", 0.3);
                }
                if (eyesDir == "center" || eyesDir == "right") {
                    this.eyeRenderer.setStateR("closed", 1);
                    this.eyeRenderer.setStateR("leanOutwards", 0.4);
                    this.browRenderer.setStateR("leanOutwards", 0.7);
                } else {
                    this.eyeRenderer.setStateR("closed", 0.2);
                    this.eyeRenderer.setStateR("leanOutwards", 0.2);
                    this.browRenderer.setStateR("leanOutwards", 0.3);
                }
                break;
            case "happy_weak":
                if (eyesDir == "center" || eyesDir == "left") {
                    this.eyeRenderer.setStateL("closed", 0.2);
                    this.eyeRenderer.setStateL("leanOutwards", 0.2);
                    this.browRenderer.setStateL("leanOutwards", 0.3);
                }
                if (eyesDir == "center" || eyesDir == "right") {
                    this.eyeRenderer.setStateR("closed", 0.2);
                    this.eyeRenderer.setStateR("leanOutwards", 0.2);
                    this.browRenderer.setStateR("leanOutwards", 0.3);
                }
                break;
            case "pain_strong":
                if (eyesDir == "center" || eyesDir == "left") {
                    this.eyeRenderer.setStateL("closed", 1);
                    this.eyeRenderer.setStateL("leanInwards", 0.2);
                    this.browRenderer.setStateL("leanInwards", 0.7);
                    this.browRenderer.setStateL("flip", 1);
                } else {
                    this.eyeRenderer.setStateL("closed", 0.2);
                    this.eyeRenderer.setStateL("irisMoveY", I2W(-5));
                    this.eyeRenderer.setStateL("leanInwards", 0.2);
                    this.browRenderer.setStateL("leanInwards", 0.3);
                    this.browRenderer.setStateL("flip", 1);
                }
                if (eyesDir == "center" || eyesDir == "right") {
                    this.eyeRenderer.setStateR("closed", 1);
                    this.eyeRenderer.setStateR("leanInwards", 0.2);
                    this.browRenderer.setStateR("leanInwards", 0.7);
                    this.browRenderer.setStateR("flip", 1);
                } else {
                    this.eyeRenderer.setStateR("closed", 0.2);
                    this.eyeRenderer.setStateR("irisMoveY", I2W(-5));
                    this.eyeRenderer.setStateR("leanInwards", 0.2);
                    this.browRenderer.setStateR("leanInwards", 0.3);
                    this.browRenderer.setStateR("flip", 1);
                }
                break;
            case "pain_weak":
                if (eyesDir == "center" || eyesDir == "left") {
                    this.eyeRenderer.setStateL("closed", 0.2);
                    this.eyeRenderer.setStateL("irisMoveY", I2W(-5));
                    this.eyeRenderer.setStateL("leanInwards", 0.2);
                    this.browRenderer.setStateL("leanInwards", 0.3);
                    this.browRenderer.setStateL("flip", 1);
                }
                if (eyesDir == "center" || eyesDir == "right") {
                    this.eyeRenderer.setStateR("closed", 0.2);
                    this.eyeRenderer.setStateR("irisMoveY", I2W(-5));
                    this.eyeRenderer.setStateR("leanInwards", 0.2);
                    this.browRenderer.setStateR("leanInwards", 0.3);
                    this.browRenderer.setStateR("flip", 1);
                }
                break;
            case "surprised_strong":
                this.eyeRenderer.setState("irisScaleX", 0.6);
                this.eyeRenderer.setState("irisScaleY", 0.6);
                this.eyeRenderer.setState("irisMoveX", I2W(-2));
                this.eyeRenderer.setState("irisMoveY", I2W(-6));
                this.browRenderer.setState("leanInwards", 0.7);
                break;
            case "surprised_weak":
                this.eyeRenderer.setState("irisScaleX", 0.55);
                this.eyeRenderer.setState("irisScaleY", 0.55);
                this.eyeRenderer.setState("irisMoveX", I2W(-2));
                this.browRenderer.setState("leanInwards", 0.3);
                break;
            }

            // Adding morphs on the top of existing expressions based on character hp
            // The less hp a character has, the more painful expression will be shown
            this.eyeRenderer.addState("closed", (1-this.hp/100)/2);
            this.browRenderer.addState("closed", (1-this.hp/100)/2);
            this.browRenderer.addState("leanOutwards", (1-this.hp/100));

            // Additional states on the top of existing expressions based on character pleasure
            this.eyeRenderer.mulState("irisScaleX", (3-this.pleasure/100)/3);
            this.eyeRenderer.mulState("irisScaleY", (3-this.pleasure/100)/3);
            this.eyeRenderer.addState("irisMoveX", I2W(-4) * this.pleasure/100);
            this.eyeRenderer.addState("irisMoveY", I2W(-4) * this.pleasure/100);
            this.browRenderer.addState("leanOutwards", this.pleasure/100/3);
        }

        // Removing specular, when hp is less than a threshold to indicate damage
        if (this.hp > 70) {
            this.eyeRenderer.setState("specularCount", 3);
        } else if (this.hp > 50) {
            this.eyeRenderer.setState("specularCount", 2);
        } else if (this.hp > 30) {
            this.eyeRenderer.setState("specularCount", 1);
        } else {
            this.eyeRenderer.setState("specularCount", 0);
        }

        // Adding heart-shaped pupil overlays when pleasure is greater than a threshold
        if (this.pleasure > 60) {
            this.eyeRenderer.setState("renderHeart", true);
        }

        if (this.leyeDestroyed == "open") { // No iris
            this.eyeRenderer.setStateL("destroyed", true);
        } else if (this.leyeDestroyed == "close") { // Force closed
            this.eyeRenderer.setStateL("closed", 1);
        }

        if (this.reyeDestroyed == "open") {
            this.eyeRenderer.setStateR("destroyed", true);
        } else if (this.reyeDestroyed == "close") {
            this.eyeRenderer.setStateR("closed", 1);
        }

        this.browRenderer.update();
        clearCtx(browCtx);
        this.browRenderer.render(browCtx);

        this.eyeRenderer.update();
        clearCtx(eyeCtx);
        this.eyeRenderer.render(eyeCtx);
    }

    updateMouth(mouthExpression) {
        let def = this.def;
        let mouthCtx = this.head.mouthCanvas.getContext("2d");
        let mouthImage = def["mouth_" + mouthExpression];

        if (this.mouthForced) { // Mouth is forcibly closed (by staples for example)
            mouthImage = def["mouth_" + this.mouthForced];
        } else if (this.dead()) {
            if (this.diedWhileOrgasming) {
                mouthImage = def["mouth_orgasm_1"];
            } else {
                mouthImage = def["mouth_dead"];
            }
        }

        clearCtx(mouthCtx);
        mouthCtx.drawImage(mouthImage, 0, 0);
    }

    setEyes(eyeDefs) {
        if (this.orgasming) return;
        clearTimeout(this.eyeTimer);
        if (!Array.isArray(eyeDefs[0])) eyeDefs = [eyeDefs,]; // To support syntactic sugar when specifying only one expression instead of a sequence
        this.eyeDefs = eyeDefs;
        this.eyesLoop();
    }

    eyesLoop() {
        if (!this.eyeDefs || Object.keys(this.eyeDefs).length == 0) {
            this.setDefaultEyes();
            return;
        }

        let eyeDef = this.eyeDefs.shift();
        let eyesExpression = eyeDef[0];
        let duration = TIMESTEP * eyeDef[1];
        let eyesDir = eyeDef.length >= 3 ? eyeDef[2] : "center";

        this.updateEyes(eyesExpression, eyesDir);
        this.eyeTimer = setTimeout(this.eyesLoop.bind(this), duration);
    }

    setMouth(mouthDefs) {
        if (this.orgasming) return;
        clearTimeout(this.mouthTimer);
        if (!Array.isArray(mouthDefs[0])) mouthDefs = [mouthDefs,];
        this.mouthDefs = mouthDefs;
        this.mouthLoop();
    }

    mouthLoop() {
        if (!this.mouthDefs || Object.keys(this.mouthDefs).length == 0) {
            this.setDefaultMouth();
            return;
        }

        let mouthDef = this.mouthDefs.shift();
        let mouthExpression = mouthDef[0];
        let duration = TIMESTEP * mouthDef[1];

        this.updateMouth(mouthExpression);
        this.mouthTimer = setTimeout(this.mouthLoop.bind(this), duration);
    }

    // Destroy left eye
    destroyLeye(shape) {
        if (this.leyeDestroyed)
            return;
        this.leyeDestroyed = shape;
    }
    // Destroy right eye
    destroyReye(shape) {
        if (this.reyeDestroyed)
            return;
        this.reyeDestroyed = shape;
    }
    // Force mouth into a shape, can be unforced
    // If mouth is already in a forced state, won't do anything
    forceMouth(shape) {
        if (this.mouthForced)
            return false;
        this.mouthForced = shape;
        this.setDefaultMouth();
        return true;
    }
    unforceMouth() {
        this.mouthForced = null;
    }
    // Destroy nose
    destroyNose() {
        this.noseDestroyed = true;
    }

    ////////////////////////////////////////////////////////////////////////
    // Animated effects
    ////////////////////////////////////////////////////////////////////////
    spit() {
        let mouthPos = this.head.box2dBody.GetWorldPoint(this.mouthRegion.GetLocalPosition());
        this.spitEffect.apply(mouthPos.x * SCALE, mouthPos.y * SCALE);
    }

    pee() {
        let pussyPos = this.waist.box2dBody.GetWorldPoint(this.pussyRegion.GetLocalPosition());
        this.peeEffect.apply(pussyPos.x * SCALE, pussyPos.y * SCALE);
    }

    squirt() {
        let pussyPos = this.waist.box2dBody.GetWorldPoint(this.pussyRegion.GetLocalPosition());
        this.squirtEffect.apply(pussyPos.x * SCALE, pussyPos.y * SCALE);
    }

    ////////////////////////////////////////////////////////////////////////
    // Other
    ////////////////////////////////////////////////////////////////////////
    stopBreath(name) {
        switch (name) {
            default:;
            case "breast": this.canBreathBreast = false;
            case "belly": this.canBreathBelly = false;
        }
    }

    // Change of HP by either damage or heal
    changeHP(val, isDeadly=false) {
        let origHP = this.hp;
        this.hp += val;
        if (this.hp <= 0) {
            if (isDeadly || origHP <= 0) {
                this.hp = 0;
            } else { // If it is not a lethal attack, HP is set to one == target lives
                this.hp = 1;
            }
        }
        if (this.hp > 100)
            this.hp = 100;

        // Specific triggers for the moment of death / resurrection
        if (this.hp == 0 && origHP > 0)
            this.kill();
        if (origHP == 0 && this.hp > 0)
            this.resurrect();

        // HP-based change processing
        this.sweatStatus = (this.hp >= 90) ? 0 : (this.hp >= 60 ? 1 : 2);
        this.shadow1Flag = this.hp < 30;
        this.shadow2Flag = this.dead();

        if (this.hp > 40) {
            this.tearsStatus = 0;
        } else if (this.hp > 30) {
            this.tearsStatus = 1;
        } else if (this.hp > 20) {
            this.tearsStatus = 2;
        } else {
            this.tearsStatus = 3;
        }

        if (this.hpCallback) {
            this.hpCallback(this.hp);
        }
    }

    applyBleeding(amount, isDeadly=false) {
        this.bleedingAmount += amount;
        this.bleedingDeadly ||= isDeadly; // If the previously applied bleeding was deadly OR the new bleeding is

        if (this.bleedingCallback) {
            this.bleedingCallback(this.bleedingAmount, this.bleedingDeadly);
        }

        clearTimeout(this.bleedingTimer);
        this.bleedingTimer = setTimeout(this.bleeding.bind(this), TIMESTEP*10);
    }

    clearBleeding() {
        this.bleedingAmount = 0;
        this.bleedingDeadly = false;

        if (this.bleedingCallback) {
            this.bleedingCallback(this.bleedingAmount, this.bleedingDeadly);
        }

        clearTimeout(this.bleedingTimer);
    }

    bleeding() {
        if (this.alive()) {
            this.changeHP(-this.bleedingAmount/20, this.bleedingDeadly);
        }
        this.bleedingTimer = setTimeout(this.bleeding.bind(this), TIMESTEP*10);
    }

    changePleasure(val) {
        if (this.dead() && val > 0) return; // Only allow pleasure decay when dead
        this.pleasure += val;
        this.pleasure = Math.min(Math.max(this.pleasure, 0), 100);
        this.blushFlag = this.pleasure > 50;

        if (this.pleasureCallback) {
            this.pleasureCallback(this.pleasure);
        }

        if (this.pleasure == 100 && !this.orgasming) {
            this.orgasm();
        }

        clearTimeout(this.pleasureDecayTimer);
        this.pleasureDecayTimer = setTimeout(this.decayPleasure.bind(this), TIMESTEP*10);
    }

    decayPleasure() {
        if (!this.orgasming) {
            this.changePleasure(-0.5);
        }
        this.pleasureDecayTimer = setTimeout(this.decayPleasure.bind(this), TIMESTEP*10);
    }

    orgasm() {
        if (this.orgasmState == 0) {
            // Setting Eyes and mouth
            this.setMouth([
                ["clench_strong", 20],
                ["orgasm_1", 40],
                ["orgasm_2", 30],
                ["orgasm_1", 40],
                ["happy_small", 70],
            ]);
            this.setEyes([
                ["ashamed_strong", 20],
                ["surprised_strong", 40],
                ["happy_weak", 30],
                ["surprised_strong", 40],
                ["ashamed_weak", 70],
            ]);
            this.orgasming = true;
        } else if (this.orgasmState == 35 || this.orgasmState == 70 || this.orgasmState == 100) {
            this.squirt();
        } else if (this.orgasmState >= 30 && this.orgasmState <= 180 && this.orgasmState % 5 == 0) {
            // Body convulsions
            let mass = this.waist.box2dBody.GetMass();
            let dir = this.orgasmState % 2 == 0 ? 1 : -1;
            let scale = this.orgasmState > 110 ? 0.5 : 1;
            this.waist.box2dBody.ApplyImpulse(new b2Vec2(0, randfloat(2, 3) * dir * mass * scale), this.pussyRegion.GetLocalPosition());
            this.breast.box2dBody.ApplyImpulse(new b2Vec2(0, randfloat(0.5, 1.5) * dir * -1 * mass * scale),
                    new b2Vec2(0, this.lnippleRegion.GetLocalPosition().y));
        } else if (this.orgasmState == 200) {
            // Finish
            this.orgasming = false;
            this.pleasure = 0;
            this.orgasmState = 0;
            clearTimeout(this.orgasmTimer);
            this.setDefaultEyes();
            this.setDefaultMouth();
            return;
        }
        this.orgasmState++;
        this.orgasmTimer = setTimeout(this.orgasm.bind(this), TIMESTEP);
    }

    dead() {
        return this.hp == 0;
    }

    alive() {
        return this.hp != 0;
    }

    kill() {
        if (this.orgasming) {
            // Sets a special face if the character was killed during her orgasm
            this.diedWhileOrgasming = true;
        }
    }

    resurrect() {
        this.diedWhileOrgasming = false;
    }

    delete() {
        // Delete timers to prevent memory leaks
        clearTimeout(this.eyeTimer);
        clearTimeout(this.mouthTimer);
        clearTimeout(this.forcedBlushTimer);
        clearTimeout(this.forcedSweatTimer);
        clearTimeout(this.forcedShadow1Timer);
        clearTimeout(this.forcedShadow2Timer);
        clearTimeout(this.forcedTearsTimer);
        clearTimeout(this.bleedingTimer);
        clearTimeout(this.pleasureDecayTimer);
        clearTimeout(this.orgasmTimer);

        this.hpCallback = undefined;
        this.bleedingCallback = undefined;
        this.pleasureCallback = undefined;
    }
}


code:
class ParticleEffect {
    constructor(rm, params) {
        this.rm = rm;
        this.params = params;
        this.emitter = new Proton.Emitter();
        this.active = false;
        this.emissionCount = 1;
    }

    apply(x, y) {
        this.emitter.p.x = x;
        this.emitter.p.y = y;
        this.emitter.emit(this.emissionCount);
    }

    make() {
        this.params.particleManager.addEmitter(this.emitter);
    }
}

class Pee extends ParticleEffect {
    constructor(rm, params) {
        super(rm, params);

        this.emitter.rate = new Proton.Rate(10, 0.1);

        // initial parameters
        this.emitter.addInitialize(new Proton.Body([
            "system/images/base/pee1.png",
            "system/images/base/pee2.png",
            "system/images/base/pee3.png",
        ] , I2W(400), I2W(400)));
        this.emitter.addInitialize(new Proton.Life(1, 3));
        this.emitter.addInitialize(new Proton.Mass(1));
        this.emitter.addInitialize(new Proton.Velocity(new Proton.Span(3, 5), new Proton.Span(160, 200), "polar"));

        // behaviours
        this.emitter.addBehaviour(new Proton.Rotate());
        this.emitter.addBehaviour(new Proton.Gravity(3));
        this.emitter.addBehaviour(new Proton.Scale(new Proton.Span(1, 0.5), 0));
        this.emitter.addBehaviour(new Proton.Alpha(1, 0));
        this.emissionCount = 0.5;

        this.make();
    }
}

class Spit extends ParticleEffect {
    constructor(rm, params) {
        super(rm, params);

        this.emitter.rate = new Proton.Rate(3, 0.05);

        // initial parameters
        this.emitter.addInitialize(new Proton.Body("system/images/base/spit.png", I2W(400), I2W(400)));
        this.emitter.addInitialize(new Proton.Life(1, 2));
        this.emitter.addInitialize(new Proton.Mass(1));
        this.emitter.addInitialize(new Proton.Velocity(new Proton.Span(3, 5), new Proton.Span(90, 270), "polar"));

        // behaviours
        this.emitter.addBehaviour(new Proton.Rotate());
        this.emitter.addBehaviour(new Proton.Gravity(4));
        this.emitter.addBehaviour(new Proton.Scale(new Proton.Span(1, 0.5), 0));
        this.emitter.addBehaviour(new Proton.Alpha(1, 0));
        this.emissionCount = 0.1;

        this.make();
    }
}

class Squirt extends ParticleEffect {
    constructor(rm, params) {
        super(rm, params);

        this.emitter.rate = new Proton.Rate(5, 0.1);

        // initial parameters
        this.emitter.addInitialize(new Proton.Body([
            "system/images/base/squirt1.png",
            "system/images/base/squirt2.png",
            "system/images/base/squirt3.png",
        ] , I2W(400), I2W(400)));
        this.emitter.addInitialize(new Proton.Life(1, 3));
        this.emitter.addInitialize(new Proton.Mass(1));
        this.emitter.addInitialize(new Proton.Velocity(new Proton.Span(3, 5), new Proton.Span(165, 195), "polar"));

        // behaviours
        this.emitter.addBehaviour(new Proton.Rotate());
        this.emitter.addBehaviour(new Proton.Gravity(7));
        this.emitter.addBehaviour(new Proton.Scale(new Proton.Span(1, 0.5), 0));
        this.emitter.addBehaviour(new Proton.Alpha(1, 0));
        this.emissionCount = 0.5;

        this.make();
    }
}


code:
class Cloth {
    constructor(bodyPart, image, thickness) {
        this.bodyPart = bodyPart;
        this.canvas = createCanvas(image);
        this.ctx = this.canvas.getContext("2d");
        this.thickness = thickness;
        this.on = true;
        this.extra = false;

        this.collision = undefined;

        this.height = this.bodyPart.height;
        this.top = this.bodyPart.top;
        this.bottom = this.bodyPart.bottom;
    }

    draw(image, lp, ang, {scale=1, gco="source-atop", alpha=1} = {}) {
        this.ctx.save();
        this.ctx.globalCompositeOperation = gco;
        this.ctx.globalAlpha = alpha;
        this.ctx.translate(
            lp.x*SCALE + this.canvas.width/2,
            lp.y*SCALE + (this.canvas.height-this.height)/2
        );
        this.ctx.rotate(ang);
        this.ctx.drawImage(image,
            0, 0, image.width, image.height,
            -image.width/2 * scale, -image.height/2 * scale,
            image.width * scale, image.height * scale);
        this.ctx.restore();

        if (this.extra) {
            let extraCtx = this.extraCanvas.getContext("2d");
            extraCtx.save();
            extraCtx.globalCompositeOperation = gco;
            extraCtx.globalAlpha = alpha;
            extraCtx.translate(
                lp.x*SCALE + this.canvas.width/2,
                lp.y*SCALE + (this.canvas.height-this.height)/2
            );
            extraCtx.rotate(ang);
            extraCtx.drawImage(image,
                0, 0, image.width, image.height,
                -image.width/2 * scale, -image.height/2 * scale,
                image.width * scale, image.height * scale);
            extraCtx.restore();
        }
    }

    render(ctx, extra=false) {
        if (this.on) {
            let b = this.bodyPart.box2dBody;
            let bx = b.GetPosition().x * SCALE;
            let by = b.GetPosition().y * SCALE;
            let bang = b.GetAngle();

            let canvas = this.canvas;
            if (extra) canvas = this.extraCanvas;

            if (canvas) {
                ctx.save();
                ctx.translate(bx, by);
                ctx.rotate(bang);
                ctx.drawImage(
                    canvas,                                  // src
                    0, (canvas.height-this.height)/2+this.top,   // sx, sy
                    canvas.width, this.bottom-this.top,          // sw, sh
                    -canvas.width/2, this.top,                 // dx, dy
                    canvas.width, this.bottom-this.top           // dw, dh
                );
                ctx.restore();
            }
        }
    }

    setExtra(extra) {
        this.extra = extra;
        this.extraCanvas = createCanvas(this.canvas);
        let ctx = this.extraCanvas.getContext("2d");
        ctx.save();
        ctx.globalCompositeOperation = "destination-out";
        ctx.drawImage(extra, 0, 0);
        ctx.restore();
    }
}


code:
// Definition of resources commonly used in the game
let systemImageDefs = {
    // User interface images
    ui: [
        "background_tile", "title_logo", "start_button",
    ],
    // Common parts of characters which can be overridden
    base: [
        "dummy", // Empty image

        "mouth_close_smile", "mouth_close_neutral", "mouth_close_sad", "mouth_close_squiggly", "mouth_close_pouting",
        "mouth_open_small", "mouth_open_medium", "mouth_open_large", "mouth_open_squiggly",
        "mouth_happy_small", "mouth_happy_medium", "mouth_happy_large",
        "mouth_small_o", "mouth_small_triangle", "mouth_orgasm_1", "mouth_orgasm_2",
        "mouth_dead",
        "mouth_clench_weak", "mouth_clench_medium", "mouth_clench_strong", "mouth_clench_squiggly",
        "sweat1", "sweat2", "shadow1", "shadow2", "blush",
        "tears1", "tears2", "tears3", "nose", "nose_bloody",

        "headSkin", "headCont", "headMeat", "headBone",
        "llegSkin", "llegCont", "llegMeat", "llegBone", "rlegSkin", "rlegCont", "rlegMeat", "rlegBone",
        "lfootSkin", "lfootCont", "lfootMeat", "lfootBone", "rfootSkin", "rfootCont", "rfootMeat", "rfootBone",
        "luparmSkin", "luparmCont", "luparmMeat", "luparmBone", "ruparmSkin", "ruparmCont", "ruparmMeat", "ruparmBone",
        "lloarmSkin", "lloarmCont", "lloarmMeat", "lloarmBone", "rloarmSkin", "rloarmCont", "rloarmMeat", "rloarmBone",
        "lhandSkin", "lhandCont", "lhandMeat", "lhandBone", "rhandSkin", "rhandCont", "rhandMeat", "rhandBone",

        "bellySkin", "bellyCont", "bellyMeat", "bellyOrgans", "bellyBone",
        "breastSkin", "breastCont", "breastMeat", "breastOrgans", "breastBone",
        "neckSkin", "neckCont", "neckMeat", "neckOrgans", "neckBone",
        "waistSkin", "waistCont", "waistMeat", "waistOrgans", "waistBone",
        "lbuttSkin", "lbuttCont", "lbuttMeat", "lbuttBone",
        "rbuttSkin", "rbuttCont", "rbuttMeat", "rbuttBone",
    ],
};

let systemSoundDefs = {
    ui: [], // Currently there is no sound to call other than the tools
};

// Has to be recreated with each new game, so it's a function instead of a dict object
function createDefaultCharacterDefs(params, rm) {
    "use strict";
    let baseImages = rm.images.base;

    return {
        // Angle limits, do not change
        // changing them would break both the character appearances and the map poses
        HEAD_NECK_MIN_ANGLE: D2R(-10),
        HEAD_NECK_MAX_ANGLE: D2R(10),
        NECK_BREAST_MIN_ANGLE: D2R(-10),
        NECK_BREAST_MAX_ANGLE: D2R(10),
        BREAST_BELLY_MIN_ANGLE: D2R(-2),
        BREAST_BELLY_MAX_ANGLE: D2R(2),
        BELLY_WAIST_MIN_ANGLE: D2R(-2),
        BELLY_WAIST_MAX_ANGLE: D2R(2),

        WAIST_LBUTT_MIN_ANGLE: D2R(-60),
        WAIST_LBUTT_MAX_ANGLE: D2R(10),
        WAIST_RBUTT_MIN_ANGLE: D2R(-10),
        WAIST_RBUTT_MAX_ANGLE: D2R(60),
        LBUTT_LLEG_MIN_ANGLE: D2R(-10),
        LBUTT_LLEG_MAX_ANGLE: D2R(60),
        RBUTT_RLEG_MIN_ANGLE: D2R(-60),
        RBUTT_RLEG_MAX_ANGLE: D2R(10),
        LLEG_LFOOT_MIN_ANGLE: D2R(-30),
        LLEG_LFOOT_MAX_ANGLE: D2R(10),
        RLEG_RFOOT_MIN_ANGLE: D2R(-10),
        RLEG_RFOOT_MAX_ANGLE: D2R(30),

        BREAST_LUPARM_MIN_ANGLE: D2R(-180),
        BREAST_LUPARM_MAX_ANGLE: D2R(-20),
        BREAST_RUPARM_MIN_ANGLE: D2R(20),
        BREAST_RUPARM_MAX_ANGLE: D2R(180),
        LUPARM_LLOARM_MIN_ANGLE: D2R(-90),
        LUPARM_LLOARM_MAX_ANGLE: D2R(90),
        RUPARM_RLOARM_MIN_ANGLE: D2R(-90),
        RUPARM_RLOARM_MAX_ANGLE: D2R(90),
        LLOARM_LHAND_MIN_ANGLE: D2R(-40),
        LLOARM_LHAND_MAX_ANGLE: D2R(20),
        RLOARM_RHAND_MIN_ANGLE: D2R(-20),
        RLOARM_RHAND_MAX_ANGLE: D2R(40),

        // Head
        headBoneImage: baseImages["headBone"],
        headMeatImage: baseImages["headMeat"],
        headSkinImage: baseImages["headSkin"],
        headContImage: baseImages["headCont"],
        headWidth: I2W(264),
        headTop: I2W(100),
        headControlPoint1: I2W(132),
        headControlPoint2: I2W(400),
        headBottom: I2W(520),
        headNeckJointXPos: I2W(0),
        headNeckJointYPos: I2W(400),
        // Hair in front of face
        frontHairImage: baseImages["dummy"],
        // Hair behind the face but in front of the body
        sideHairImage: baseImages["dummy"],
        // Hair behind the body
        backHairImage: baseImages["dummy"],
        // Shadows from hair casted on the face
        faceShadowColor: "#ebc0ad",
        // Blush
        blushImage: baseImages["blush"],
        // Sweats
        sweat1Image: baseImages["sweat1"],
        sweat2Image: baseImages["sweat2"],
        // Shadow due to damage 1
        shadow1Image: baseImages["shadow1"],
        // Shadow due to damage 2
        shadow2Image: baseImages["shadow2"],
        // Tears
        tears1Image: baseImages["tears1"],
        tears2Image: baseImages["tears2"],
        tears3Image: baseImages["tears3"],
        // Glasses
        glassesImage: baseImages["dummy"],
        // Mouth
        mouth_close_smile: baseImages["mouth_close_smile"],
        mouth_close_neutral: baseImages["mouth_close_neutral"],
        mouth_close_sad: baseImages["mouth_close_sad"],
        mouth_close_squiggly: baseImages["mouth_close_squiggly"],
        mouth_close_pouting: baseImages["mouth_close_pouting"],
        mouth_open_small: baseImages["mouth_open_small"],
        mouth_open_medium: baseImages["mouth_open_medium"],
        mouth_open_large: baseImages["mouth_open_large"],
        mouth_open_squiggly: baseImages["mouth_open_squiggly"],
        mouth_happy_small: baseImages["mouth_happy_small"],
        mouth_happy_medium: baseImages["mouth_happy_medium"],
        mouth_happy_large: baseImages["mouth_happy_large"],
        mouth_small_o: baseImages["mouth_small_o"],
        mouth_small_triangle: baseImages["mouth_small_triangle"],
        mouth_orgasm_1: baseImages["mouth_orgasm_1"],
        mouth_orgasm_2: baseImages["mouth_orgasm_2"],
        mouth_dead: baseImages["mouth_dead"],
        mouth_clench_weak: baseImages["mouth_clench_weak"],
        mouth_clench_medium: baseImages["mouth_clench_medium"],
        mouth_clench_strong: baseImages["mouth_clench_strong"],
        mouth_clench_squiggly: baseImages["mouth_clench_squiggly"],
        // Nose
        noseImage: baseImages["nose"],
        noseBloodyImage: baseImages["nose_bloody"],

        // Neck
        neckBoneImage: baseImages["neckBone"],
        neckOrgansImage: baseImages["neckOrgans"],
        neckMeatImage: baseImages["neckMeat"],
        neckSkinImage: baseImages["neckSkin"],
        neckContImage: baseImages["neckCont"],
        neckWidth: I2W(112),
        neckHeight: I2W(120),
        neckHeadJointXPos: I2W(0),
        neckHeadJointYPos: I2W(20),
        neckBreastJointXPos: I2W(0),
        neckBreastJointYPos: I2W(110),

        // Breast (chest area)
        breastBoneImage: baseImages["breastBone"],
        breastOrgansImage: baseImages["breastOrgans"],
        breastMeatImage: baseImages["breastMeat"],
        breastSkinImage: baseImages["breastSkin"],
        breastContImage: baseImages["breastCont"],
        breastWidth: I2W(500),
        breastHeight: I2W(449),
        breastNeckJointXPos: I2W(0),
        breastNeckJointYPos: I2W(15),
        breastBellyJointXPos: I2W(0),
        breastBellyJointYPos: I2W(427),
        breastLuparmJointXPos: I2W(200),
        breastLuparmJointYPos: I2W(45),
        breastRuparmJointXPos: I2W(-200),
        breastRuparmJointYPos: I2W(45),

        // Belly
        bellyBoneImage: baseImages["bellyBone"],
        bellyOrgansImage: baseImages["bellyOrgans"],
        bellyMeatImage: baseImages["bellyMeat"],
        bellySkinImage: baseImages["bellySkin"],
        bellyContImage: baseImages["bellyCont"],
        bellyWidth     : I2W(300),
        bellyHeight    : I2W(153),
        bellyBreastJointXPos: I2W(0),
        bellyBreastJointYPos: I2W(21),
        bellyWaistJointXPos: I2W(0),
        bellyWaistJointYPos: I2W(127),

        // Waist
        waistBoneImage: baseImages["waistBone"],
        waistOrgansImage: baseImages["waistOrgans"],
        waistMeatImage: baseImages["waistMeat"],
        waistSkinImage: baseImages["waistSkin"],
        waistContImage: baseImages["waistCont"],
        waistWidth      : I2W(376),
        waistHeight     : I2W(301),
        waistBellyJointXPos: I2W(0),
        waistBellyJointYPos: I2W(20),
        waistLbuttJointXPos: I2W(110),
        waistLbuttJointYPos: I2W(187),
        waistRbuttJointXPos: I2W(-110),
        waistRbuttJointYPos: I2W(187),

        // Left butt (thigh)
        lbuttBoneImage: baseImages["lbuttBone"],
        lbuttMeatImage: baseImages["lbuttMeat"],
        lbuttSkinImage: baseImages["lbuttSkin"],
        lbuttContImage: baseImages["lbuttCont"],
        lbuttWidth     : I2W(210),
        lbuttHeight    : I2W(630),
        lbuttWaistJointXPos: I2W(-15),
        lbuttWaistJointYPos: I2W(110),
        lbuttLlegJointXPos: I2W(0),
        lbuttLlegJointYPos: I2W(600),

        // Right butt (thigh)
        rbuttBoneImage: baseImages["rbuttBone"],
        rbuttMeatImage: baseImages["rbuttMeat"],
        rbuttSkinImage: baseImages["rbuttSkin"],
        rbuttContImage: baseImages["rbuttCont"],
        rbuttWidth     : I2W(210),
        rbuttHeight    : I2W(630),
        rbuttWaistJointXPos: I2W(15),
        rbuttWaistJointYPos: I2W(110),
        rbuttRlegJointXPos: I2W(0),
        rbuttRlegJointYPos: I2W(600),

        // Left leg below the knee
        llegBoneImage: baseImages["llegBone"],
        llegMeatImage: baseImages["llegMeat"],
        llegSkinImage: baseImages["llegSkin"],
        llegContImage: baseImages["llegCont"],
        llegWidth     : I2W(150),
        llegHeight    : I2W(530),
        llegLbuttJointXPos: I2W(20),
        llegLbuttJointYPos: I2W(50),
        llegLfootJointXPos: I2W(-20),
        llegLfootJointYPos: I2W(495),
        // Right leg below the knee
        rlegBoneImage: baseImages["rlegBone"],
        rlegMeatImage: baseImages["rlegMeat"],
        rlegSkinImage: baseImages["rlegSkin"],
        rlegContImage: baseImages["rlegCont"],
        rlegWidth     : I2W(150),
        rlegHeight    : I2W(530),
        rlegRbuttJointXPos: I2W(-20),
        rlegRbuttJointYPos: I2W(50),
        rlegRfootJointXPos: I2W(20),
        rlegRfootJointYPos: I2W(495),

        // Left foot
        lfootBoneImage: baseImages["lfootBone"],
        lfootMeatImage: baseImages["lfootMeat"],
        lfootSkinImage: baseImages["lfootSkin"],
        lfootContImage: baseImages["lfootCont"],
        lfootWidth     : I2W(130),
        lfootHeight    : I2W(310),
        lfootLlegJointXPos: I2W(0),
        lfootLlegJointYPos: I2W(30),

        // Right foot
        rfootBoneImage: baseImages["rfootBone"],
        rfootMeatImage: baseImages["rfootMeat"],
        rfootSkinImage: baseImages["rfootSkin"],
        rfootContImage: baseImages["rfootCont"],
        rfootWidth     : I2W(130),
        rfootHeight    : I2W(310),
        rfootRlegJointXPos: I2W(0),
        rfootRlegJointYPos: I2W(30),

        // Left upper arm
        luparmBoneImage: baseImages["luparmBone"],
        luparmMeatImage: baseImages["luparmMeat"],
        luparmSkinImage: baseImages["luparmSkin"],
        luparmContImage: baseImages["luparmCont"],
        luparmWidth      : I2W(124),
        luparmHeight     : I2W(428),
        luparmBreastJointXPos: I2W(30),
        luparmBreastJointYPos: I2W(36),
        luparmLloarmJointXPos: I2W(-5),
        luparmLloarmJointYPos: I2W(400),

        // Right upper arm
        ruparmBoneImage: baseImages["ruparmBone"],
        ruparmMeatImage: baseImages["ruparmMeat"],
        ruparmSkinImage: baseImages["ruparmSkin"],
        ruparmContImage: baseImages["ruparmCont"],
        ruparmWidth      : I2W(124),
        ruparmHeight     : I2W(428),
        ruparmBreastJointXPos: I2W(-30),
        ruparmBreastJointYPos: I2W(36),
        ruparmRloarmJointXPos: I2W(5),
        ruparmRloarmJointYPos: I2W(400),

        // Left lower arm
        lloarmBoneImage: baseImages["lloarmBone"],
        lloarmMeatImage: baseImages["lloarmMeat"],
        lloarmSkinImage: baseImages["lloarmSkin"],
        lloarmContImage: baseImages["lloarmCont"],
        lloarmWidth     : I2W(80),
        lloarmHeight    : I2W(384),
        lloarmLuparmJointXPos: I2W(5),
        lloarmLuparmJointYPos: I2W(20),
        lloarmLhandJointXPos: I2W(0),
        lloarmLhandJointYPos: I2W(350),

        // Right lower arm
        rloarmBoneImage: baseImages["rloarmBone"],
        rloarmMeatImage: baseImages["rloarmMeat"],
        rloarmSkinImage: baseImages["rloarmSkin"],
        rloarmContImage: baseImages["rloarmCont"],
        rloarmWidth     : I2W(80),
        rloarmHeight    : I2W(384),
        rloarmRuparmJointXPos: I2W(-5),
        rloarmRuparmJointYPos: I2W(20),
        rloarmRhandJointXPos: I2W(0),
        rloarmRhandJointYPos: I2W(350),

        // Left hand
        lhandBoneImage: baseImages["lhandBone"],
        lhandMeatImage: baseImages["lhandMeat"],
        lhandSkinImage: baseImages["lhandSkin"],
        lhandContImage: baseImages["lhandCont"],
        lhandWidth     : I2W(128),
        lhandHeight    : I2W(122),
        lhandLloarmJointXPos: I2W(-10),
        lhandLloarmJointYPos: I2W(20),

        // Right hand
        rhandBoneImage: baseImages["rhandBone"],
        rhandMeatImage: baseImages["rhandMeat"],
        rhandSkinImage: baseImages["rhandSkin"],
        rhandContImage: baseImages["rhandCont"],
        rhandWidth     : I2W(128),
        rhandHeight    : I2W(122),
        rhandRloarmJointXPos: I2W(10),
        rhandRloarmJointYPos: I2W(20),
    };
}


code:
// Manages the entire game, handles scene change events mostly
class Game {
    // Create a resource manager
    rm = new ResourceManager();

    // Represents a scene in the game (for example title screen or play screen)
    #scene = undefined;

    // Dictionary which is passed to nearly every class containing the most important game data (global state)
    params = {};

    constructor() {
        // Capture screenshot
        // TODO handle keyboard events in a better structured manner
        let sKeyPressed = false;
        let onKeyDown = function (e) {
            if (!sKeyPressed && e.which == 83) { //83==s
                sKeyPressed = true;
                captureScreenshot();
            }
        };
        let onKeyUp = function (e) {
            if (e.which == 83 || e.type == "blur") {
                sKeyPressed = false;
            }
        };
        jQuery(window).unbind("keydown");
        jQuery(window).unbind("keyup blur");
        jQuery(window).bind("keydown", onKeyDown);
        jQuery(window).bind("keyup blur", onKeyUp);
    }

    // Switching between scenes
    #onChangeScene(event) {
        dbgLog("onChangeScene to " + event.detail.id);
        // Scenes have to set their own resolution explicitly now
        this.#scene.stop();
        switch (event.detail.id) {
            case "load":
                this.#scene = new LoadScene(this.rm, this.params);
                break;
            case "title":
                this.#scene = new TitleScene(this.rm, this.params);
                break;
            case "play":
                this.#scene = new PlayScene(this.rm, this.params);
                break;
            default:
                dbgLog("undefined scene id detected: " + event.detail.id);
        }
        this.#scene.start();
    }

    run() {
        document.addEventListener("change_scene", this.#onChangeScene.bind(this));
        this.#scene = new LoadScene(this.rm, this.params); // First scene
        this.#scene.start();
        setInterval(this.update.bind(this), TIMESTEP);
    }

    update() {
        this.#scene.update()
    }
}


code:
// Represents the character's head
class Head extends BodyPart {
    constructor(chara, def, world) {
        super("head", chara, def, world);
        this.height = def.headBottom;//-headTop;
        this.top = 0;
        this.bottom = this.height;

        this.box2dBody = createHumanPart(world, b2Body.b2_dynamicBody, BODYPART_HEAD, [
            new b2Vec2(0.0 / SCALE, def.headTop / SCALE),
            new b2Vec2(def.headWidth / 2 / SCALE, def.headControlPoint1 / SCALE),
            new b2Vec2(def.headWidth / 2 / SCALE, def.headControlPoint2 / SCALE),
            new b2Vec2(0.0 / SCALE, def.headBottom / SCALE),
            new b2Vec2(-def.headWidth / 2 / SCALE, def.headControlPoint2 / SCALE),
            new b2Vec2(-def.headWidth / 2 / SCALE, def.headControlPoint1 / SCALE)
            ], def.headXPos / SCALE, def.headYPos / SCALE, 0
        );

        if (def.headCanvasYSize === undefined) // A hack to allow large head elements (e.g front hair) to be rendered properly and not chopped off
            this.canvas = createCanvas(def.headSkinImage);
        else
            this.canvas = createCanvas(def.headSkinImage.width, def.headCanvasYSize);

        // Only used when rendering head
        // Some features belong to the head: hair, eyes, mouth, and are kept when the face is skinned
        // While eyebrows and facial effects should only be rendered where skin is present
        this.tempFaceSkinCanvas = createCanvas(def.headSkinImage);

        this.skinCanvas = createCanvas(def.headSkinImage);
        this.meatCanvas = createCanvas(def.headMeatImage);
        this.boneCanvas = createCanvas(def.headBoneImage);
        this.contourCanvas = createCanvas(def.headContImage);

        this.browCanvas = createCanvas(def.headContImage.width, def.headContImage.height);
        this.eyeCanvas = createCanvas(def.headContImage.width, def.headContImage.height);
        this.mouthCanvas = createCanvas(def.mouth_close_smile);
        this.noseCanvas = def.noseImage;
        this.noseBloodyCanvas = def.noseBloodyImage;

        this.frontHairCanvas = createCanvas(def.frontHairImage);
        this.sideHairCanvas = createCanvas(def.sideHairImage);
        this.backHairCanvas = createCanvas(def.backHairImage);

        this.glassesCanvas = createCanvas(def.glassesImage);

        // No separate canvas creation, as there is no need to paint over these images
        this.shadow1Canvas = def.shadow1Image;
        this.shadow2Canvas = def.shadow2Image;
        this.blushCanvas = def.blushImage;
        this.sweatCanvases = [undefined, def.sweat1Image, def.sweat2Image]; // padding with undefined for more comfortable indexing
        this.tearsCanvases = [undefined, def.tears1Image, def.tears2Image, def.tears3Image];

        // Rendering faceShadow over the face (generated from front hair)
        let skinCtx = this.skinCanvas.getContext("2d");
        skinCtx.save();
        skinCtx.globalCompositeOperation = "source-atop";
        let faceShadowCanvas = createCanvas(def.frontHairImage);
        let faceShadowCtx = faceShadowCanvas.getContext("2d");
        faceShadowCtx.globalCompositeOperation = "source-atop";
        faceShadowCtx.fillStyle = def.faceShadowColor;
        faceShadowCtx.fillRect(0, 0, faceShadowCanvas.width, faceShadowCanvas.height);
        skinCtx.drawImage(faceShadowCanvas, (this.skinCanvas.width - faceShadowCanvas.width) / 2,
            (this.skinCanvas.height - faceShadowCanvas.height) / 2 + I2W(15) /* Y Offset downwards */);
        skinCtx.restore();
    }

    renderItems(ctx) {
        for (const item of this.items) {
            if (!item.renderBeforeHairFlag) { // Render some special items above the skin but behind hair_front, so we skip them here
                item.render(ctx);
            }
        }
    }

    render(ctx) {
        let bx = this.box2dBody.GetPosition().x*SCALE;
        let by = this.box2dBody.GetPosition().y*SCALE;
        let bang = this.box2dBody.GetAngle();

        ctx.save();
        ctx.translate(bx, by);
        ctx.rotate(bang);
        ctx.drawImage(
            this.canvas,                          // src
            0, 0,                               // sx, sy
            this.canvas.width, this.canvas.height,  // sw, sh
            -this.canvas.width/2,                 // dx
            -(this.canvas.height-this.height)/2,    // dy Head rendering is weird af, explained below
            this.canvas.width, this.canvas.height   // dw, dh
        );
        ctx.restore();
    }

    /*
    o
                 x
              .  #  .
            #         #
            #         #
            #         #
            #         #
              #     #
                 #

    */
    // The head collision body looks like the shape made from hashmarks
    // It's not symmetrical to the horizontal axis as the top part is way more slanted than the bottom
    // If it were symmetrical it would follow the shape of dots and x mark
    // headBottom tells the whole height of the symmetrical structure, aka the distance between x and the lowermost hashmark
    // This x mark acts as position (0, 0) in the head's b2Body's local coordinate system (which has the interesting side effect of the origo laying outside of the collision bounds)
    // So to find the correct render position (dx and dy), one must find the vector x ---> o (o is the top-left corner)
    // The x coordinate is just like any other part's: -this.canvas.width/2
    // And the y coordinate has to be -(this.canvas.height-headBottom)/2
    // Nevertheless this convention should be remade to be way clearer

    renderBackHair(ctx) {
        let bx = this.box2dBody.GetPosition().x*SCALE;
        let by = this.box2dBody.GetPosition().y*SCALE;
        let bang = this.box2dBody.GetAngle();
        let sCanvas = this.backHairCanvas;

        ctx.save();
        ctx.translate(bx,by);
        ctx.rotate(bang);
        ctx.drawImage(
            sCanvas,                        // src
            0, 0,                           // sx, sy
            sCanvas.width, sCanvas.height,  // sw, sh
            -sCanvas.width/2,             // dx
            -(this.canvas.height-this.height)/2,    // dy
            sCanvas.width, sCanvas.height   // dw, dh
        );
        ctx.restore();
    }

    draw(image, type, lp, ang, {scale=1, calculate_mask=false, mask=undefined, gco="source-atop", alpha=1} = {}) {
        if (type == BodyPart.MEAT_ORGANS) type = BodyPart.MEAT;
        if (type == BodyPart.ORGANS) return; // These layers don't exist on head
        if (!mask) { // Default mask, if the one provided is undefined
            mask = createCanvas(image.width, image.height);
            let maskCtx = mask.getContext("2d");
            maskCtx.fillStyle = BodyPart.MASK_COLOR;
            maskCtx.fillRect(0, 0, mask.width, mask.height);
        }
        let maskCtx = mask.getContext("2d");
        maskCtx.globalCompositeOperation = "source-in";
        maskCtx.drawImage(image, 0, 0);
        image = mask;

        let canvas = undefined;
        switch (type) {
            case BodyPart.CONTOUR: canvas = this.contourCanvas;    break;
            case BodyPart.SKIN:    canvas = this.skinCanvas;       break;
            case BodyPart.MEAT:    canvas = this.meatCanvas;       break;
            case BodyPart.BONE:    canvas = this.boneCanvas;       break;
        }
        let bang = this.box2dBody.GetAngle();
        if (canvas) {
            let ctx = canvas.getContext("2d");
            ctx.save();
            ctx.globalCompositeOperation = gco;
            ctx.globalAlpha = alpha;
            ctx.translate(
                lp.x*SCALE + canvas.width/2,
                lp.y*SCALE + (canvas.height-this.height)/2
            );
            ctx.rotate(-bang+ang);
            ctx.drawImage(image,
                0, 0, image.width, image.height,
                -image.width/2 * scale, -image.height/2 * scale,
                image.width * scale, image.height * scale);
            ctx.restore();

            if (type == BodyPart.SKIN && gco == "destination-out") { // If skin is removed, remove hair as well in close proximity
                for (const hairCanvas of [this.frontHairCanvas, this.sideHairCanvas, this.backHairCanvas]) {
                    let ctx = hairCanvas.getContext("2d");
                    ctx.save();
                    ctx.globalCompositeOperation = gco;
                    ctx.globalAlpha = alpha;
                    ctx.translate(
                        lp.x*SCALE + hairCanvas.width/2,
                        lp.y*SCALE + (this.canvas.height-this.height)/2 // this part is hacky, since this.canvas and canvas can have different heights
                    );
                    ctx.rotate(-bang+ang);
                    ctx.drawImage(image,
                        0, 0, image.width, image.height,
                        -image.width/2 * scale, -image.height/2 * scale,
                        image.width * scale, image.height * scale);
                    ctx.restore();
                }
            }
        }

        if (calculate_mask) {
            let newMask = createCanvas(image.width, image.height);
            let newMaskCtx = newMask.getContext("2d");
            newMaskCtx.fillStyle = BodyPart.MASK_COLOR;
            newMaskCtx.fillRect(0, 0, newMask.width, newMask.height);
            newMaskCtx.save();
            newMaskCtx.globalCompositeOperation = "destination-out";

            newMaskCtx.translate( // Image translate
                image.width/2,
                image.height/2
            );
            newMaskCtx.scale(1/scale, 1/scale); // Inverse scale
            newMaskCtx.rotate(bang-ang); // Inverse rotation
            newMaskCtx.translate( // Inverse translate
                -(lp.x*SCALE),
                -(lp.y*SCALE)
            );

            newMaskCtx.drawImage(canvas, -canvas.width/2, -(canvas.height-this.height)/2);
            newMaskCtx.restore();
            return newMask;
        }
    }

    /**
     * Masks a given canvas with the skinCanvas
     * If skin or meat is present, the values from the given canvas will be shown
     * Otherwise transparent
     * The result is stored in this.tempFaceSkinCanvas
     * @param {*} canvas The canvas to mask
     * @param {boolean} meat True if the canvas should be shown on a skinned head, but not on the skull
     */
    applyFaceMask(canvas, meat=false) {
        let faceCtx = this.tempFaceSkinCanvas.getContext("2d");
        let hcw = this.tempFaceSkinCanvas.width;
        let hch = this.tempFaceSkinCanvas.height;
        clearCtx(faceCtx);
        faceCtx.save();
        if (meat) {
            faceCtx.drawImage(this.meatCanvas, (hcw-this.skinCanvas.width) / 2, (hch-this.skinCanvas.height) / 2);
        } else if (!this.character.autoSkin) {
            faceCtx.drawImage(this.skinCanvas, (hcw-this.skinCanvas.width) / 2, (hch-this.skinCanvas.height) / 2);
        }
        faceCtx.globalCompositeOperation = "source-in";
        faceCtx.drawImage(canvas, (hcw-canvas.width) / 2, (hch-canvas.height) / 2);
        faceCtx.restore();
    }

    updateCanvas() {
        let ctx = this.canvas.getContext("2d");

        let hcw = this.canvas.width;
        let hch = this.canvas.height;
        clearCtx(ctx);

        ctx.drawImage(this.sideHairCanvas, (hcw-this.sideHairCanvas.width) / 2, (hch-this.sideHairCanvas.height) / 2);
        ctx.drawImage(this.boneCanvas, (hcw-this.boneCanvas.width) / 2, (hch-this.boneCanvas.height) / 2);
        ctx.drawImage(this.meatCanvas, (hcw-this.meatCanvas.width) / 2, (hch-this.meatCanvas.height) / 2);
        if (!this.character.autoSkin)
            ctx.drawImage(this.skinCanvas, (hcw-this.skinCanvas.width) / 2, (hch-this.skinCanvas.height) / 2);
        ctx.drawImage(this.contourCanvas, (hcw-this.contourCanvas.width) / 2, (hch-this.contourCanvas.height) / 2);

        if (this.character.shadow1Flag || this.character.forcedShadow1Flag) {
            this.applyFaceMask(this.shadow1Canvas);
            ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.shadow1Canvas.width) / 2, (hch-this.shadow1Canvas.height) / 2);
        }
        if (this.character.shadow2Flag || this.character.forcedShadow2Flag) {
            this.applyFaceMask(this.shadow2Canvas);
            ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.shadow2Canvas.width) / 2, (hch-this.shadow2Canvas.height) / 2);
        }
        if (this.character.blushFlag || this.character.forcedBlushFlag) {
            this.applyFaceMask(this.blushCanvas);
            ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.blushCanvas.width) / 2, (hch-this.blushCanvas.height) / 2);
        }

        // Nosebleed could cover mouth, to prevent this nose is rendered before
        this.applyFaceMask(this.character.noseDestroyed ? this.noseBloodyCanvas : this.noseCanvas, true);
        ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.noseCanvas.width) / 2, (hch-this.noseCanvas.height) / 2);
        this.applyFaceMask(this.mouthCanvas, true);
        ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.mouthCanvas.width) / 2, (hch-this.mouthCanvas.height) / 2);
        this.applyFaceMask(this.browCanvas);
        ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.browCanvas.width) / 2, (hch-this.browCanvas.height) / 2);
        this.applyFaceMask(this.eyeCanvas, true);
        ctx.drawImage(this.tempFaceSkinCanvas, (hcw-this.eyeCanvas.width) / 2, (hch-this.eyeCanvas.height) / 2);

        let tearStatus = Math.max(this.character.tearsStatus, this.character.forcedTearsStatus);
        if (tearStatus > 0) {
            let tearsImage = this.tearsCanvases[tearStatus];
            this.applyFaceMask(tearsImage, true);
            ctx.drawImage(this.tempFaceSkinCanvas, (hcw-tearsImage.width) / 2, (hch-tearsImage.height) / 2);
        }

        let sweatStatus = Math.max(this.character.sweatStatus, this.character.forcedSweatStatus);
        if (sweatStatus > 0) {
            let sweatImage = this.sweatCanvases[sweatStatus];
            this.applyFaceMask(sweatImage, true);
            ctx.drawImage(this.tempFaceSkinCanvas, (hcw-sweatImage.width) / 2, (hch-sweatImage.height) / 2);
        }
        ctx.drawImage(this.glassesCanvas, (hcw-this.glassesCanvas.width) / 2, (hch-this.glassesCanvas.height) / 2);

        // Render some special items above the skin but behind hair_front
        for (const item of this.items) {
            if (item.renderBeforeHairFlag) {
                item.render(ctx);
            }
        }

        ctx.drawImage(this.frontHairCanvas, (hcw-this.frontHairCanvas.width) / 2, (hch-this.frontHairCanvas.height) / 2);
    }
}


code:
// Represents an item, which can be attached to body parts
// Custom items can be either subclasses of Item, or objects given they are simple
class Item {
    constructor(name, image, parent, lp, ang, params, attachStrength) {
        this.image = image;
        this.parent = parent;
        this.lp = lp;
        this.ang = ang - this.parent.box2dBody.GetAngle(); // From global to local rotation
        this.name = name;
        this.params = params;
        this.attachStrength = attachStrength;
    }

    update() {}

    delete() {}

    render(ctx) {
        let b = this.parent.box2dBody;
        let bx = b.GetPosition().x * SCALE;
        let by = b.GetPosition().y * SCALE;
        let bang = b.GetAngle();

        ctx.save();
        ctx.translate(bx, by);
        ctx.rotate(bang);
        ctx.translate(this.lp.x * SCALE, this.lp.y * SCALE);
        ctx.rotate(this.ang);
        ctx.translate(-this.image.width/2, -this.image.height/2);
        ctx.drawImage(this.image, 0, 0);
        ctx.restore();
    }

    setParent(part) {
        this.parent = part;
    }
}


code:
// Represents a joint between body parts
// Acts as a wrapper for regular box2D joints for easier handling
class Joint {
    constructor(world, bodyA, bodyB, jointXA, jointYA, jointXB, jointYB, jointMINAng, jointMAXAng) {
        this.world = world;
        this.bodyA = bodyA;
        this.bodyB = bodyB;
        this.jointXA = jointXA;
        this.jointYA = jointYA;
        this.jointXB = jointXB;
        this.jointYB = jointYB;
        this.jointMINAng = jointMINAng;
        this.jointMAXAng = jointMAXAng;

        this.joint = createRevoluteJoint(world, bodyA.box2dBody, bodyB.box2dBody, jointXA, jointYA, jointXB, jointYB, jointMINAng, jointMAXAng);
    }

    copyToNewBodyA(newBodyA) {
        return new Joint(this.world, newBodyA, this.bodyB, this.jointXA, this.jointYA, this.jointXB, this.jointYB,
            this.jointMINAng, this.jointMAXAng);
    }

    copyToNewBodyB(newBodyB) {
        return new Joint(this.world, this.bodyA, newBodyB, this.jointXA, this.jointYA, this.jointXB, this.jointYB,
            this.jointMINAng, this.jointMAXAng);
    }
}


code:
// Loads required mod javascript, image and sound files
class LoadScene {
    #dom = jQuery("#game")[0];
    #ctx = jQuery("#canvas")[0].getContext("2d");
    #files = jQuery("#game-files")[0];

    // Helper variable for rendering, keeps track of how many entries should be displayed
    #STATE = 0;

    #collectionScriptList = collectionIDs.map(id => "mods/collections/" + id + "/" + id + ".js");
    #mapScriptList = mapIDs.map(id => "mods/maps/" + id + "/" + id + ".js");
    #characterScriptList = characterIDs.map(id => "mods/characters/" + id + "/" + id + ".js");
    #toolScriptList = toolIDs.map(id => "mods/tools/" + id + "/" + id + ".js");

    #collectionScriptLoader = new ScriptLoader(this.#collectionScriptList);
    #mapScriptLoader = new ScriptLoader(this.#mapScriptList);
    #characterScriptLoader = new ScriptLoader(this.#characterScriptList);
    #toolScriptLoader = new ScriptLoader(this.#toolScriptList);
    #rm;
    #params;
    #updated = true;

    constructor(rm, params) {
        setResolution(WIDTH, HEIGHT);
        this.#files.addEventListener("change", e => this.#rm.setFiles(e.target.files));

        this.#ctx.font = "normal bold 20px sans-serif";
        this.#ctx.textAlign = "left";
        this.#ctx.textBaseline = "top";

        this.#rm = rm;
        this.#params = params
    }

    /**
     * Lazy drawing in case if ResourceManager would push it's load_resources_progress events too quickly when using async threads
     */
    #setUpdated() {
        if (this.#rm.usesThreads()) {
            this.#updated = true;
        } else {
            this.#render();
        }
    }

    #render() {
        const ctx = this.#ctx;
        ctx.fillStyle = "#000000";
        ctx.fillRect(0, 0, WIDTH, HEIGHT);
        ctx.fillStyle = "#FFFFFF";
        ctx.fillText("Torture Chamber " + VERSION, 20, 40);
        ctx.fillText("Now loading...", 40, 60);

        const fillText = (name, num, den, pos) => ctx.fillText(`Loaded ${name}=${num}/${den}`, 40, pos);
        switch (this.#STATE) { // Progressive draw based on loading this.#STATE
            case 4: fillText("resources", this.#rm.loadedResourceCount, this.#rm.totalResourceCount, 160);
            case 3: fillText("tool scripts", this.#toolScriptLoader.loadedScriptCount, this.#toolScriptLoader.totalScriptCount, 140);
            case 2: fillText("character scripts", this.#characterScriptLoader.loadedScriptCount, this.#characterScriptLoader.totalScriptCount, 120);
            case 1: fillText("map scripts", this.#mapScriptLoader.loadedScriptCount, this.#mapScriptLoader.totalScriptCount, 100);
            case 0: fillText("collection scripts", this.#collectionScriptLoader.loadedScriptCount, this.#collectionScriptLoader.totalScriptCount, 80);
        }
    }

    // Start-up with loading each script
    start() {
        this.#dom.addEventListener("load_scripts_progress", this.#setUpdated.bind(this));
        this.#dom.addEventListener("load_scripts_complete", this.#onLoadCollectionScriptsComplete.bind(this), {once: true});
        this.#setUpdated();
        this.#collectionScriptLoader.load();
    }

    // Start loading map scripts after collections are done
    #onLoadCollectionScriptsComplete() {
        this.#STATE++;
        this.#dom.addEventListener("load_scripts_complete", this.#onLoadMapScriptsComplete.bind(this), {once: true});
        this.#setUpdated();
        this.#mapScriptLoader.load();
    }

    // Start loading character scripts after maps are done
    #onLoadMapScriptsComplete() {
        this.#STATE++;
        this.#dom.addEventListener("load_scripts_complete", this.#onLoadCharacterScriptsComplete.bind(this), {once: true});
        this.#setUpdated();
        this.#characterScriptLoader.load();
    }

    // Start loading tool scripts after characters are done
    #onLoadCharacterScriptsComplete() {
        this.#STATE++;
        this.#dom.addEventListener("load_scripts_complete", this.#onLoadToolScriptsComplete.bind(this), {once: true});
        this.#setUpdated();
        this.#toolScriptLoader.load();
    }

    // Start loading resources after collections are done
    #onLoadToolScriptsComplete() {
        this.#STATE++;
        this.#dom.removeEventListener("load_scripts_progress", this.#setUpdated.bind(this));
        this.#dom.addEventListener("load_resources_progress", this.#setUpdated.bind(this));
        this.#dom.addEventListener("load_resources_complete", this.#onLoadResourcesComplete.bind(this), {once: true});
        this.#rm.init();
        this.#setUpdated();
        this.#rm.load(this.#params);
    }

    // Transition to title scene when resource loading is completed
    #onLoadResourcesComplete() {
        this.#dom.removeEventListener("load_resources_progress", this.#setUpdated.bind(this));
        this.#render();
        this.#dom.dispatchEvent(createChangeSceneEvent("title"));
    }

    update() {
        if (this.#updated) {
            this.#render();
            this.#updated = false;
        }
    }

    stop() { }; // No need for cleanup
}


code:
// Main entry point, starts up the game
let game = undefined;
jQuery(document).ready(function() {
    game = new Game();
    game.run();
});


code:
// Map base class
// Extend your custom map from this
class TMap { // Named TMap to avoid naming conflict with regular javascript maps
    constructor(params, rm) {
        this.characterCount = this.constructor.characterCount;
        this.images = rm.images.maps[this.constructor.name];
        this.params = params;
        this.world = params.world;
        this.characters = params.characters;
        this.tools = params.tools;

        this.backgroundImage = undefined;

        this.renderShadow = true;

        // Handling tool dependencies (display an alert) NOT USED YET
        this.dependencies = [];
        this.width = WIDTH;
        this.height = HEIGHT;
    }

    init() {
        this.wall = this.createWall();
        setResolution(this.width, this.height);
    }

    // Returns a dict containing the head position and limb rotations (=pose) for each characters present
    getPoseInfo() {}

    drawOnBackground(image, x, y, ang, {gco="source-atop", alpha=1} = {}) {
        let ctx = this.backgroundImage.getContext("2d");
        ctx.save();
        ctx.globalCompositeOperation = gco;
        ctx.globalAlpha = alpha;
        ctx.translate(x, y);
        ctx.rotate(ang);
        ctx.drawImage(image, -image.width/2, -image.height/2);
        ctx.restore();
    }

    renderBackround(ctx) { }
    renderBetweenBodyAndHead(ctx) { }
    renderForeground(ctx) { }

    checkDependencies() {
        for (const dependency of this.dependencies) {
            if (typeof eval(dependency) !== 'function') {
                console.log("WARNING: map depends on " + dependency + " which does not exist");
            }
        }
    }

    /**
     * Fixates a box2Dbody to the back wall with a revolute joint
     * Calculate global wall position based on body transform and local pos
     * @param {*} body The box2Dbody to fixate
     * @param {*} bx Local position of joint relative to body (x coordinate)
     * @param {*} by Local position of joint relative to body (y coordinate)
     * @returns The joint if successful, false otherwise
     */
    addWallFixation(body, bx, by) {
        let worldPoint = body.GetWorldPoint(new b2Vec2(bx, by));
        let joint = createRevoluteJoint(this.params.world, body, this.wall, bx, by, worldPoint.x, worldPoint.y);
        return joint; // Fixation is always successful by default, but you can override this behaviour and return false sometimes
    }

    addWallFixationItem(part, bx, by) {
        let joint = this.addWallFixation(part.box2dBody, bx / SCALE, by / SCALE);
        if (joint) {
            part.addItem(new FixationItem(part, new b2Vec2(bx / SCALE, by / SCALE), this.params, joint));
        }
    }

    // Creates a non-collidable background which is used for nail attachment
    createWall() {
        let world = this.world;

        let vertices = [new b2Vec2(0/SCALE, 0/SCALE),
                        new b2Vec2(this.width/SCALE, 0/SCALE),
                        new b2Vec2(this.width/SCALE, this.height/SCALE),
                        new b2Vec2(0/SCALE, this.height/SCALE)
                    ];
        let fd = new b2FixtureDef;
        fd.shape = new b2PolygonShape();
        fd.shape.SetAsArray(vertices, vertices.length);
        fd.filter.categoryBits = NO_COLL.categoryBits;
        fd.filter.maskBits = NO_COLL.maskBits;
        let bd = new b2BodyDef();
        bd.type = b2Body.b2_staticBody;
        bd.position.x = 0;
        bd.position.y = 0;
        let body = world.CreateBody(bd);
        body.CreateFixture(fd);
        return body;
    }

    // Creates collidable borders around the playable area to prevent bodies from falling down
    createBorders(top=true, bottom=true, left=true, right=true) {
        let world = this.world;
        const borderWidth = 10;
        this.borders = [];

        let vertices = []; // Vertices must be given in a clockwise order
        if (top) vertices.push([
            new b2Vec2(0/SCALE, -borderWidth/SCALE),
            new b2Vec2(this.width/SCALE, -borderWidth/SCALE),
            new b2Vec2(this.width/SCALE, 0/SCALE),
            new b2Vec2(0/SCALE, 0/SCALE)
        ]);
        if (bottom) vertices.push([
            new b2Vec2(0/SCALE, this.height/SCALE),
            new b2Vec2(this.width/SCALE, this.height/SCALE),
            new b2Vec2(this.width/SCALE, (this.height+borderWidth)/SCALE),
            new b2Vec2(0/SCALE, (this.height+borderWidth)/SCALE),
        ]);
        if (left) vertices.push([
            new b2Vec2(0/SCALE, 0/SCALE),
            new b2Vec2(0/SCALE, this.height/SCALE),
            new b2Vec2(-borderWidth/SCALE, this.height/SCALE),
            new b2Vec2(-borderWidth/SCALE, 0/SCALE),
        ]);
        if (right) vertices.push([
            new b2Vec2((this.width+borderWidth)/SCALE, 0/SCALE),
            new b2Vec2((this.width+borderWidth)/SCALE, this.height/SCALE),
            new b2Vec2(this.width/SCALE, this.height/SCALE),
            new b2Vec2(this.width/SCALE, 0/SCALE),
        ]);

        for (let i = 0; i < vertices.length; i++) {
            let fd = new b2FixtureDef;
            fd.shape = new b2PolygonShape();
            fd.shape.SetAsArray(vertices[i], vertices[i].length);
            fd.filter.categoryBits = WALL_C.categoryBits;
            fd.filter.maskBits = WALL_C.maskBits;
            let bd = new b2BodyDef();
            bd.type = b2Body.b2_staticBody;
            bd.position.x = 0;
            bd.position.y = 0;
            let body = world.CreateBody(bd);
            body.CreateFixture(fd);
            this.borders[i] = body;
        }
    }

    update() {}

    delete() {}
}

class FixationItem extends Item {
    constructor(parent, lp, params, joint) {
        super("Fixation", undefined, parent, lp, 0, params, 5000);
        this.joint = joint;
    }

    render(ctx) {} // Dont render anything

    setParent(part) {
        super.setParent(part);
        this.params.world.DestroyJoint(this.joint);
        this.joint = this.params.map.addWallFixation(this.parent.box2dBody, this.lp.x, this.lp.y);
    }

    delete() {
        this.params.world.DestroyJoint(this.joint);
    }
}