code:
class GammaLab extends TMap {
    static Resources = {
        images: [
            "bg.jpg", "fg",
        ],
        sounds: [],
    };

    static creator = "zmod";
    static fullName = "Gamma Laboratory";
    static characterCount = 3;
    static depends = [];

    constructor(params, rm) {
        super(params, rm);
        this.backgroundImage = createCanvas(this.images["bg.jpg"]);
    }

    init() {
        super.init();

        for (const character of this.characters) {
            // Fixing limbs
            let fixationReferenceList = [
                [character.lfoot, I2W(10), I2W(100)],
                [character.rfoot, I2W(-10), I2W(100)],
                [character.lhand, I2W(0), I2W(35)],
                [character.rhand, I2W(0), I2W(35)],
                [character.lleg, I2W(0), I2W(150)],
                [character.rleg, I2W(0), I2W(150)],
                [character.lloarm, I2W(0), I2W(70)],
                [character.rloarm, I2W(0), I2W(70)],
            ];
            for (const [part, xpos, ypos] of fixationReferenceList) {
                this.addWallFixationItem(part, xpos, ypos);
            }
        }
    }

    renderBackround(ctx) {
        ctx.drawImage(this.backgroundImage, 0, 0);
    }

    renderBetweenBodyAndHead(ctx) {
        ctx.drawImage(this.images.fg, 0, 0);
    }

    getPoseInfo() {
        return [{
            headXPos: I2W(680),
            headYPos: I2W(150),

            neckHeadJointAngle: D2R(0),
            breastNeckJointAngle: D2R(0),
            bellyBreastJointAngle: D2R(0),
            waistBellyJointAngle: D2R(0),
            lbuttWaistJointAngle: D2R(-15),
            rbuttWaistJointAngle: D2R(15),
            llegLbuttJointAngle: D2R(-2),
            rlegRbuttJointAngle: D2R(2),
            lfootLlegJointAngle: D2R(-4),
            rfootRlegJointAngle: D2R(4),
            luparmBreastJointAngle: D2R(-165),
            ruparmBreastJointAngle: D2R(165),
            lloarmLuparmJointAngle: D2R(-15),
            rloarmRuparmJointAngle: D2R(15),
            lhandLloarmJointAngle: D2R(-10),
            rhandRloarmJointAngle: D2R(10),
        },
        {
            headXPos: I2W(1920),
            headYPos: I2W(150),

            neckHeadJointAngle: D2R(0),
            breastNeckJointAngle: D2R(0),
            bellyBreastJointAngle: D2R(0),
            waistBellyJointAngle: D2R(0),
            lbuttWaistJointAngle: D2R(-15),
            rbuttWaistJointAngle: D2R(15),
            llegLbuttJointAngle: D2R(-2),
            rlegRbuttJointAngle: D2R(2),
            lfootLlegJointAngle: D2R(-4),
            rfootRlegJointAngle: D2R(4),
            luparmBreastJointAngle: D2R(-165),
            ruparmBreastJointAngle: D2R(165),
            lloarmLuparmJointAngle: D2R(-15),
            rloarmRuparmJointAngle: D2R(15),
            lhandLloarmJointAngle: D2R(-10),
            rhandRloarmJointAngle: D2R(10),
        },
        {
            headXPos: I2W(3160),
            headYPos: I2W(150),

            neckHeadJointAngle: D2R(0),
            breastNeckJointAngle: D2R(0),
            bellyBreastJointAngle: D2R(0),
            waistBellyJointAngle: D2R(0),
            lbuttWaistJointAngle: D2R(-15),
            rbuttWaistJointAngle: D2R(15),
            llegLbuttJointAngle: D2R(-2),
            rlegRbuttJointAngle: D2R(2),
            lfootLlegJointAngle: D2R(-4),
            rfootRlegJointAngle: D2R(4),
            luparmBreastJointAngle: D2R(-165),
            ruparmBreastJointAngle: D2R(165),
            lloarmLuparmJointAngle: D2R(-15),
            rloarmRuparmJointAngle: D2R(15),
            lhandLloarmJointAngle: D2R(-10),
            rhandRloarmJointAngle: D2R(10),
        },
        ];
    }
}


code:
class Acid extends ClickTool {
    static Resources = {
        images: [
            "beaker", "beaker2",
            "chemburn01", "chemburn02", "chemburn03", "chemburn04", "chemburn05",
            "hole01", "hole02", "hole03", "hole04", "hole05",
        ],
        sounds: [
            "acid"
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "Acid";
        this.cursorImage = this.images.beaker;
        this.cursorImageShiftX = I2W(80);
        this.cursorImageShiftY = I2W(80);
        this.collisionShape = circleShape(I2W(72));

        // Tilted beaker as alternative cursor
        this.cursorImage2 = this.images.beaker2;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0, "image": this.cursorImage});
        this.anim.addKeyframe(5, {"ang": D2R(-45), "image": this.cursorImage2}, ()=>{
            playSound(this.sounds["acid"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(30, {}, ()=>{this.finalize();});
        this.anim.interpolations["ang"] = "rotation";
    }

    execute() {
        let imageNum = zfill(randint(1, 5), 2);
        let chemburn = this.images["chemburn" + imageNum];
        let hole = this.images["hole" + imageNum];
        let ang = randfloat(2 * Math.PI);

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(chemburn, BodyPart.SKIN, lp, ang, {calculate_mask: true});
            part.draw(chemburn, BodyPart.MEAT_ORGANS, lp, ang, {mask: meatMask});
            part.drawOnClothes(hole, lp, ang, {gco: "destination-out"});
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_strong", 30, direction]);
        character.setMouth(["clench_strong", 30]);
    }

    damage(character, flags) {
        if (flags.leye)
            character.destroyLeye("close");
        if (flags.reye)
            character.destroyReye("close");
        if (flags.direction == "center") {
            character.changeHP(-5);
        } else {
            character.changeHP(-2);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        this.anim.callActions(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx - this.cursorImage.width / 2 + this.cursorImageShiftX,
            this.my - this.cursorImage.height / 2 + this.cursorImageShiftY
        );
        ctx.rotate(v.ang);
        ctx.drawImage(v.image, 0, 0);
        ctx.restore();

        super.animate();
    }
}

repo_name: Tomajc/torture

license: mit


code:
class BBPistol extends ClickTool {
    static Resources = {
        images: [
            "pistol01", "pistol02",
            "wound01", "wound02", "wound03", "wound_meat",
        ],
        sounds: [
            "pistol",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "BB Pistol";
        this.cursorImage = this.images["pistol01"];
        this.cursorImage2 = this.images["pistol02"];
        this.cursorImageShiftX = I2W(62);
        this.cursorImageShiftY = I2W(182);
        this.collisionShape = circleShape(I2W(20));
        this.allowInvalidHit = true;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"image": this.cursorImage});
        this.anim.addKeyframe(2, {}, ()=>{
            playSound(this.sounds["pistol"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(3, {"image": this.cursorImage2});
        this.anim.addKeyframe(10, {}, ()=>{this.finalize();});
    }

    execute() {
        let wound = this.images["wound" + zfill(randint(1, 3), 2)];
        let ang = randfloat(D2R(360));

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, ang, {calculate_mask: true});
            part.draw(this.images["wound_meat"], BodyPart.MEAT_ORGANS, lp, ang, {mask: meatMask});
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_weak", 20, direction]);
        character.setMouth(["clench_medium", 20]);
    }

    damage(character, flags) {
        character.changeHP(-1);
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        this.anim.callActions(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(this.mx, this.my);
        ctx.drawImage(v.image,
            -this.cursorImageShiftX, -this.cursorImageShiftY);
        ctx.restore();

        super.animate();
    }
}


code:
class Bat extends ChargeTool {
    static Resources = {
        images: [
            "bat",
            "hit01", "hit02", "hit03",
            "bruise01", "bruise02", "bruise03",
        ],
        sounds: [
            "bat_hit", "bat_bruise",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.ang = 0;

        this.name = "Bat";
        this.cursorImage = this.images["bat"];
        this.cursorImageShiftX = I2W(65);
        this.cursorImageShiftY = I2W(53);
        this.maxCharge = 15;
        this.hitRegisterChargeLimit = 3; // Minimum charge for hit to have an effect
        this.bruiseChargeLimit = 12;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0});
        this.anim.addKeyframe(this.maxCharge, {"ang": D2R(-45)});
        this.anim.addKeyframe(this.maxCharge + 5, {"ang": 0}, ()=>{
            if (this.currentCharge > this.bruiseChargeLimit) {
                playSound(this.sounds["bat_bruise"], this.params.volume);
            } else {
                playSound(this.sounds["bat_hit"], this.params.volume);
            }

            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
            if (this.currentCharge > this.bruiseChargeLimit) this.applyImpulses();
            this.finalize();
        });
        this.anim.interpolations["ang"] = "rotation";
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(-30), D2R(30));
        this.collisionShape = rectShape(I2W(300), I2W(180), this.ang);
    }

    updateChargingCursor(x, y) {
        let v = this.anim.getValues(this.currentCharge);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            x + this.cursorImage.width * 15 / 16,
            y + this.cursorImage.height * 15 / 16
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
    }

    // Thrust the body parts in a random direction
    applyImpulses() {
        let ix, iy;
        [ix, iy] = randcircle(6);
        for (const [part, lp] of this.hitParts) {
            let b = part.box2dBody;
            let mass = b.GetMass();
            b.ApplyImpulse(new b2Vec2(ix * mass, iy * mass), lp);
        }
    }

    execute() {
        let hit = this.images["hit" + zfill(randint(1, 3), 2)];
        let bruise = this.images["bruise" + zfill(randint(1, 3), 2)];
        let ang = this.ang;
        let alpha = Math.min(this.currentCharge / this.bruiseChargeLimit, 1);
        let wound = this.currentCharge > this.bruiseChargeLimit ? bruise : hit;
        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, ang, {calculate_mask: true, alpha: alpha});
            part.draw(wound, BodyPart.MEAT_ORGANS, lp, ang, {mask: meatMask, alpha: alpha});
        }
    }

    doReaction(character, flags, direction) {
        if (this.currentCharge > this.bruiseChargeLimit) {
            character.setEyes(["pain_strong", 20, direction]);
            character.setMouth(["clench_squiggly", 20]);
        } else {
            character.setEyes(["pain_weak", 20, direction]);
            character.setMouth(["clench_squiggly", 20]);
        }
    }

    damage(character, flags) {
        if (flags.nose)
            character.destroyNose();
        let scale = this.currentCharge > this.bruiseChargeLimit ? 3.5 : 1;
        if (flags.direction == "center") {
            character.changeHP(-3 * scale);
        } else {
            character.changeHP(-2 * scale);
        }
        if (flags.head && this.currentCharge > this.bruiseChargeLimit) {
            character.changeHP(-2 * scale, true);
        }
    }

    animate() {
        if (this.currentCharge <= this.hitRegisterChargeLimit) {
            this.finalize();
            return;
        }
        let tick = this.maxCharge + this.animateStep * this.currentCharge / this.maxCharge + 5 * (this.maxCharge - this.currentCharge) / this.maxCharge;
        let v = this.anim.getValues(tick);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx + this.cursorImage.width * 15 / 16,
            this.my + this.cursorImage.height * 15 / 16
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
        this.anim.callActions(tick);

        super.animate();
    }
}


code:
class Butcher extends ClickTool {
    static Resources = {
        images: [
            "butcher", "splatter",
            "notch", "blood_bone", "blood_skin", "hole_clothes",
        ],
        sounds: [
            "butcher",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.gui = true;

        this.name = "Butcher";
        this.cursorImage = this.images["butcher"];
        this.cursorImageShiftX = I2W(10);
        this.cursorImageShiftY = I2W(10);
        this.itemRemovalStrength = 1500;
        this.canTargetClothAlone = false;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0});
        this.anim.addKeyframe(5, {"ang": D2R(45)}, ()=>{
            playSound(this.sounds["butcher"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.removeHitItems();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(6, {"ang": D2R(-45)});
        this.anim.addKeyframe(10, {}, ()=>{this.finalize();});
        this.anim.interpolations["ang"] = "rotation";

        this.affectOrgans = false; // Whether hits remove organ layer contents or not
        this.setScale(1); // Tool effect scale
    }

    execute() {
        let ang = randfloat(2 * Math.PI);
        for (const [part, lp] of this.hitParts) {
            part.draw(this.images["notch"], BodyPart.CONTOUR, lp, ang, {gco: "destination-out", scale: this.scale});
            part.draw(this.images["notch"], BodyPart.SKIN, lp, ang, {gco: "destination-out", scale: this.scale});
            part.draw(this.images["notch"], BodyPart.MEAT, lp, ang, {gco: "destination-out", scale: this.scale});
            if (this.affectOrgans) {
                part.draw(this.images["notch"], BodyPart.ORGANS, lp, ang, {gco: "destination-out", scale: this.scale});
            }
            let meatBloodMask = part.draw(this.images["blood_skin"], BodyPart.SKIN, lp, ang, {calculate_mask: true, scale: this.scale});
            part.draw(this.images["blood_skin"], BodyPart.MEAT, lp, ang, {mask: meatBloodMask, scale: this.scale});
            part.draw(this.images["blood_bone"], BodyPart.BONE, lp, ang, {scale: this.scale});
            part.drawOnClothes(this.images["hole_clothes"], lp, ang, {gco: "destination-out", scale: this.scale});
        }
        this.params.map.drawOnBackground(this.images["splatter"], this.mx, this.my, ang);
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_strong", 40, direction]);
        character.setMouth(["clench_squiggly", 40]);
    }

    damage(character, flags) {
        let dmgScale = Math.sqrt(this.scale);
        if (flags.leye)
            character.destroyLeye("close");
        if (flags.reye)
            character.destroyReye("close");
        if (flags.direction == "center") {
            if (flags.head) {
                character.changeHP(-50 * dmgScale, true);
            } else if (flags.heart) {
                character.changeHP(-30 * dmgScale, true);
            } else {
                character.changeHP(-15 * dmgScale);
            }
            character.applyBleeding(2.5 * dmgScale, true);
        } else {
            character.changeHP(-9 * dmgScale);
        }
        if (flags.waist || flags.belly || flags.breast || flags.head || flags.neck) {
            character.applyBleeding(2 * dmgScale, true);
        } else {
            character.applyBleeding(1 * dmgScale);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx - this.cursorImage.width / 2 + this.cursorImage.width,
            this.my - this.cursorImage.height / 2 + this.cursorImage.height
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }

    toggleAffectOrgans() {
        this.affectOrgans = !this.affectOrgans;
    }

    setScale(scale) {
        this.scale = scale;
        this.collisionShape = circleShape(I2W(90) * this.scale);
    }
}

class ButcherGUI extends preact.Component {
    constructor(props) {
        super(props);
    }

    setScale = (scale) => {
        this.props.tool.setScale(scale);
    }

    toggleAffectOrgans = () => {
        this.props.tool.toggleAffectOrgans();
    }

    render() {
        return(html`
            <${RangeInput} min=0.3 max=2 step=0.1 value=${this.props.tool.scale} onChange=${this.setScale} label="Scale"/>
            <${Checkbox} value=${this.props.tool.affectOrgans} onChange=${this.toggleAffectOrgans} label="Affect Organs"/>
        `);
    }
}


code:
class Candle extends ClickTool {
    static Resources = {
        images: [
            "candle", "candle_tilted",
            "wax01", "wax02", "wax03",
        ],
        sounds: [
            "candle",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.cursorImage = this.images["candle"];
        this.cursorImage2 = this.images["candle_tilted"];

        this.name = "Candle";
        this.cursorImageShiftX = I2W(200);
        this.cursorImageShiftY = I2W(62);
        this.collisionShape = circleShape(I2W(40));

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": D2R(0), "image": this.cursorImage});
        this.anim.addKeyframe(8, {"image": this.cursorImage2});
        this.anim.addKeyframe(12, {"ang": D2R(-90)}, ()=>{
            playSound(this.sounds["candle"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
        });
        this.anim.addKeyframe(16, {"image": this.cursorImage});
        this.anim.addKeyframe(20, {"ang": D2R(0), "image": this.cursorImage}, ()=>{
            this.finalize();
        });
        this.anim.interpolations["ang"] = "rotation";
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(360));
    }

    execute() {
        let wax = this.images["wax" + zfill(randint(1, 3), 2)];

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wax, BodyPart.SKIN, lp, this.ang, {calculate_mask: true});
            let boneMask = part.draw(wax, BodyPart.MEAT_ORGANS, lp, this.ang, {calculate_mask: true, mask: meatMask});
            part.draw(wax, BodyPart.BONE, lp, this.ang, {mask: boneMask});
            part.drawOnClothes(wax, lp, this.ang);
        }
    }

    doReaction(character, flags, direction) {
        character.setMouth(["open_medium", 25]);
        if (flags.leye || flags.reye) {
            character.setEyes(["pain_strong", 25, direction]);
        } else {
            character.setEyes(["pain_weak", 25, direction]);
        }
    }

    damage(character, flags) {
        if (flags.head) {
            character.changeHP(-0.5);
        } else {
            character.changeHP(-0.25);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        this.anim.callActions(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx - this.cursorImage.width / 2 + this.cursorImageShiftX,
            this.my - this.cursorImage.height / 2 + this.cursorImageShiftY + I2W(100)
        );
        ctx.rotate(v.ang);
        ctx.drawImage(v.image, -this.cursorImageShiftX, -this.cursorImageShiftY);
        ctx.restore();

        super.animate();
    }
}


code:
class Cane extends ClickTool {
    static Resources = {
        images: [
            "cane",
            "wound01", "wound02", "wound03",
            "wound_hard01", "wound_hard02", "wound_hard03",
        ],
        sounds: [
            "cane01", "cane02", "cane03",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.ang = 0;

        this.name = "Cane";
        this.cursorImage = this.images["cane"];
        this.cursorImageShiftX = I2W(104);
        this.cursorImageShiftY = I2W(62);

        // A crude estimation on how quickly (or often) the player attacks
        // Above a certain rate, more harder wounds will be delivered
        this.currentRate = 0;
        this.maxRate = 10;
        this.rateThreshold = 6;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": D2R(10)});
        this.anim.addKeyframe(4, {"ang": D2R(0)}, ()=>{
            playSound(this.sounds["cane" + zfill(randint(1, 3), 2)], this.params.volume);
            this.increaseCurrentRate();
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(5, {}, ()=>{this.finalize();});
        this.anim.interpolations["ang"] = "rotation";
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(-20), D2R(20));
        this.collisionShape = rectShape(I2W(160), I2W(50), this.ang);
    }

    increaseCurrentRate() {
        this.currentRate = Math.min(this.currentRate+1, this.maxRate);
        setTimeout(() => { this.currentRate = Math.max(this.currentRate - 1, 0); }, 3000);
    }

    execute() {
        let imageNum = zfill(randint(1, 3), 2);
        let wound = this.images["wound" + imageNum];
        if (this.currentRate >= this.rateThreshold) {
            wound = this.images["wound_hard" + imageNum];
        }

        for (const [part, lp] of this.hitParts) {
            part.draw(wound, BodyPart.SKIN, lp, this.ang);
        }
    }

    doReaction(character, flags, direction) {
        if (this.currentRate >= this.rateThreshold) {
            character.setEyes(["pain_strong", 20, direction]);
            character.setMouth(["open_medium", 20]);
        } else {
            character.setEyes(["pain_weak", 20, direction]);
            character.setMouth(["clench_weak", 20]);
        }
    }

    damage(character, flags) {
        if (this.currentRate >= this.rateThreshold) {
            character.changeHP(-0.3);
        } else {
            character.changeHP(-0.2);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        this.anim.callActions(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx + I2W(500),
            this.my + this.cursorImageShiftY
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width+I2W(100), -this.cursorImage.height);
        ctx.restore();

        super.animate();
    }
}


code:
class Cigar extends ClickTool {
    static Resources = {
        images: [
            "cigar", "cigar_active",
            "wound01", "wound02", "wound_meat01", "wound_meat02",
            "hole",
        ],
        sounds: [
            "cigar",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.ang = 0;

        this.name = "Cigar";
        this.cursorImage = this.images["cigar"];
        this.cursorImage2 = this.images["cigar_active"];
        this.cursorImageShiftX = I2W(36);
        this.cursorImageShiftY = I2W(74);
        this.collisionShape = circleShape(I2W(30));

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"image": this.cursorImage, "tr_x": 0, "tr_y": 0});
        this.anim.addKeyframe(5, {"image": this.cursorImage2, "tr_x": 0, "tr_y": 0}, ()=>{
            playSound(this.sounds["cigar"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(6, {"tr_x": 2, "tr_y": 2});
        this.anim.addKeyframe(8, {"tr_x": -2, "tr_y": -2});
        this.anim.addKeyframe(10, {"tr_x": 2, "tr_y": 2});
        this.anim.addKeyframe(12, {"tr_x": 0, "tr_y": 0, "image": this.cursorImage});
        this.anim.addKeyframe(14, {}, ()=>{this.finalize();});
    }

    initializeParticleEffects() {
        this.smokeEffect = new CigarSmoke(this.rm, this.params);
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(360));
    }

    execute() {
        let imageNum = zfill(randint(1, 2), 2);
        let wound = this.images["wound" + imageNum];
        let wound_meat = this.images["wound_meat" + imageNum];
        let ang = this.ang;

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, ang, {calculate_mask: true});
            part.draw(wound_meat, BodyPart.MEAT_ORGANS, lp, ang, {mask: meatMask});
            part.drawOnClothes(this.images["hole"], lp, ang, {gco: "destination-out"});
        }
    }

    doReaction(character, flags, direction) {
        if (flags.head || flags.lnipple || flags.rnipple || flags.pussy) {
            character.setEyes(["pain_strong", 30, direction]);
            character.setMouth(["clench_squiggly", 30]);
        } else {
            character.setEyes(["pain_weak", 25, direction]);
            character.setMouth(["clench_strong", 25]);
        }
    }

    damage(character, flags) {
        if (flags.leye)
            character.destroyLeye("open");
        if (flags.reye)
            character.destroyReye("open");
        if (flags.direction == "center") {
            character.changeHP(-2);
        } else {
            character.changeHP(-1);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx - this.cursorImage.width / 2 + I2W(85),
            this.my - this.cursorImage.height / 2 /*+ this.cursorImageShiftY*/
        );
        ctx.drawImage(v.image, v.tr_x, v.tr_y);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }

    updateCursor(mx, my) {
        super.updateCursor(mx, my);
        if (this.params.tick % 5 == 0) {
            this.smokeEffect.apply(mx, my);
        }
    }
}

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

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

        // initial parameters
        this.emitter.addInitialize(new Proton.Body("mods/tools/Cigar/images/smoke.png", I2W(150), I2W(350)));
        this.emitter.addInitialize(new Proton.Life(1, 2));
        this.emitter.addInitialize(new Proton.Mass(1));
        this.emitter.addInitialize(new Proton.Velocity(new Proton.Span(1, 3), new Proton.Span(-30, 30), "polar"));

        // behaviours
        this.emitter.addBehaviour(new Proton.Rotate());
        this.emitter.addBehaviour(new Proton.Scale(new Proton.Span(1, 0.75), new Proton.Span(1.25, 1.5)));
        this.emitter.addBehaviour(new Proton.Alpha(1, 0));
        this.emissionCount = 0.3;

        this.make();
    }
}


code:
class Crop extends ClickTool {
    static Resources = {
        images: [
            "crop",
            "wound01", "wound02", "wound03",
        ],
        sounds: [
            "crop",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.cursorImage = this.images["crop"];

        this.name = "Crop";
        this.cursorImageShiftX = I2W(95);
        this.cursorImageShiftY = I2W(95);
        this.collisionShape = circleShape(I2W(70));

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": D2R(0)});
        this.anim.addKeyframe(4, {"ang": D2R(35)});
        this.anim.addKeyframe(5, {"ang": D2R(0)}, ()=>{
            playSound(this.sounds["crop"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
        });
        this.anim.addKeyframe(6, {}, ()=>{this.finalize();});
        this.anim.interpolations["ang"] = "rotation";
    }

    execute() {
        let wound = this.images["wound" + zfill(randint(1, 3), 2)];
        let ang = randfloat(D2R(-30), D2R(30));

        for (const [part, lp] of this.hitParts) {
            part.draw(wound, BodyPart.SKIN, lp, ang);
        }
    }

    doReaction(character, flags, direction) {
        if (character.hp > 90) {
            character.setMouth(["clench_weak", 25]);
        } else {
            character.setMouth(["clench_medium", 25]);
        }
        if (flags.lnipple || flags.rnipple || flags.pussy) {
            character.setEyes(["pain_strong", 25, direction]);
        } else {
            character.setEyes(["pain_weak", 25, direction]);
        }
    }

    damage(character, flags) {
        character.changeHP(-0.6);
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx + this.cursorImage.width * 7/8,
            this.my + this.cursorImage.height * 7/8
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width + this.cursorImageShiftX, -this.cursorImage.height + this.cursorImageShiftY);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }
}


code:
class Crowbar extends ChargeTool {
    static Resources = {
        images: [
            "crowbar", "bloodeffect",
            "hit01", "hit02", "hit03",
            "bruise01", "bruise02", "bruise03",
        ],
        sounds: [
            "crowbar_hit", "crowbar_bruise",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.ang = 0;

        this.name = "Crowbar";
        this.cursorImage = createCanvas(this.images["crowbar"]);

        // The tool gets bloodier as it is used
        this.cursorImageCtx = this.cursorImage.getContext("2d");
        this.cursorImageCtx.globalCompositeOperation = "source-atop";
        this.bloodEffect = this.images["bloodeffect"];

        this.cursorImageShiftX = I2W(86);
        this.cursorImageShiftY = I2W(152);
        this.maxCharge = 12;
        this.hitRegisterChargeLimit = 3; // Minimum charge for hit to have an effect
        this.bruiseChargeLimit = 9;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0});
        this.anim.addKeyframe(this.maxCharge, {"ang": D2R(55)});
        this.anim.addKeyframe(this.maxCharge + 4, {"ang": 0}, ()=>{
            if (this.currentCharge > this.bruiseChargeLimit) {
                playSound(this.sounds["crowbar_bruise"], this.params.volume);
            } else {
                playSound(this.sounds["crowbar_hit"], this.params.volume);
            }

            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
            if (this.currentCharge > this.bruiseChargeLimit) this.applyImpulses();
            this.finalize();
        });
        this.anim.interpolations["ang"] = "rotation";
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(360));
        this.collisionShape = circleShape(I2W(120));
    }

    updateChargingCursor(x, y) {
        let v = this.anim.getValues(this.currentCharge);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            x + this.cursorImage.width * 15 / 16,
            y + this.cursorImage.height * 15 / 16
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
    }

    // Thrust the body parts in a random direction
    applyImpulses() {
        let ix, iy;
        [ix, iy] = randcircle(6);
        for (const [part, lp] of this.hitParts) {
            let b = part.box2dBody;
            let mass = b.GetMass();
            let mult = this.currentCharge / this.maxCharge;
            b.ApplyImpulse(new b2Vec2(ix * mass * mult, iy * mass * mult), lp);
        }
    }

    execute() {
        let hit = this.images["hit" + zfill(randint(1, 3), 2)];
        let bruise = this.images["bruise" + zfill(randint(1, 3), 2)];
        let ang = this.ang;
        let alpha = Math.min(this.currentCharge / this.bruiseChargeLimit, 1);
        let wound = this.currentCharge > this.bruiseChargeLimit ? bruise : hit;
        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, ang, {calculate_mask: true, alpha: alpha});
            part.draw(wound, BodyPart.MEAT_ORGANS, lp, ang, {mask: meatMask, alpha: alpha});
        }

        if (this.currentCharge > this.bruiseChargeLimit) {
            // A complicated way to express preference for coordinates closer near the top left corner
            this.cursorImageCtx.drawImage(this.bloodEffect,
                Math.pow(randfloat(0, 1), 2) * (this.cursorImage.width - this.bloodEffect.width),
                Math.pow(randfloat(0, 1), 2) * (this.cursorImage.height - this.bloodEffect.height)
            );
        }
    }

    doReaction(character, flags, direction) {
        if (this.currentCharge > this.bruiseChargeLimit) {
            character.setEyes(["pain_strong", 30, direction]);
            character.setMouth(["clench_strong", 30]);
        } else {
            character.setEyes(["pain_strong", 20, direction]);
            character.setMouth(["clench_squiggly", 20]);
        }
    }

    damage(character, flags) {
        if (flags.nose)
            character.destroyNose();
        let scale = this.currentCharge > this.bruiseChargeLimit ? 4 : 1;
        if (flags.direction == "center") {
            character.changeHP(-4 * scale);
        } else {
            character.changeHP(-1 * scale);
        }
        if (flags.head && this.currentCharge > this.bruiseChargeLimit) {
            character.changeHP(-3 * scale, true);
        }
    }

    animate() {
        if (this.currentCharge <= this.hitRegisterChargeLimit) {
            this.finalize();
            return;
        }
        let tick = this.maxCharge + this.animateStep * this.currentCharge / this.maxCharge + 4 * (this.maxCharge - this.currentCharge) / this.maxCharge;
        let v = this.anim.getValues(tick);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx + this.cursorImage.width * 15 / 16,
            this.my + this.cursorImage.height * 15 / 16
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
        this.anim.callActions(tick);

        super.animate();
    }
}


code:
class Dildo extends SingleTargetTool {
    static Resources = {
        images: [
            "dildo",
        ],
        sounds: [
            "fuck",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.character = undefined;
        this.waist = undefined;

        this.cursorImage = this.images["dildo"];

        this.name = "Dildo";
        this.cursorImageShiftX = I2W(200);
        this.cursorImageShiftY = I2W(20);
        this.thrusts = 5;

        // Positions and angles are dependent on waist transformation which is not known beforehand
        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0, "tr_x": 0, "tr_y": 0});
        for (let t = 20; t < 20 + this.thrusts * 10; t+=10) {
            this.anim.addKeyframe(t, {"ang": 0, "tr_x": 0, "tr_y": 0}, ()=>{
                playSound(this.sounds["fuck"], this.params.volume);
            });
        }
        for (let t = 22; t < 22 + this.thrusts * 10; t+=10) {
            this.anim.addKeyframe(t, {"ang": 0, "tr_x": 0, "tr_y": 0}, ()=>{
                this.doReactionAll();
            });
        }
        this.anim.addKeyframe(22 + this.thrusts * 10, {}, ()=>{
            clearCtx(this.params.canvas.backEffectCtx);
            this.finalize();
        })
        this.anim.interpolations["ang"] = "rotation";
    }

    doReaction(character) {
        if (character.pleasure > 60) {
            character.setEyes([["happy_strong", 5], ["happy_weak", 8]]);
            character.setMouth(["happy_large", 16]);
        } else if (character.pleasure > 20) {
            character.setEyes(["happy_weak", 16]);
            character.setMouth(["happy_medium", 16]);
        } else {
            character.setEyes(["ashamed_weak", 16]);
            character.setMouth(["clench_strong", 16]);
        }
        character.forceSweat(16, 1);
        character.changePleasure(5);
        // Pushing waist upwards
        let mass = character.waist.box2dBody.GetMass();
        character.waist.box2dBody.ApplyImpulse(new b2Vec2(randfloat(-4, -1) * mass, 0), character.pussyRegion.GetLocalPosition());
    }

    animate() {
        this.character = this.singleCharacterTarget;
        this.waist = this.character.waist;

        if (this.waist.destroyed) {
            clearCtx(this.params.canvas.backEffectCtx);
            this.finalize();
            return;
        }

        if (this.animateStep % 5 == 0) { // Position correction, maybe this could run on every anim iteration
            this.anim.keyframes["tr_x"][0] = this.mx;
            this.anim.keyframes["tr_y"][0] = this.my;

            let b = this.waist.box2dBody;
            let bang = b.GetAngle();
            let pussyX = this.character.pussyRegion.GetLocalPosition().x;
            let pussyY = this.character.pussyRegion.GetLocalPosition().y;
            let pussyYoffset = pussyY - I2W(300) / SCALE;
            let posx = (b.GetPosition().x + pussyX*Math.cos(-bang) + pussyY*Math.sin(-bang)) * SCALE;
            let posy = (b.GetPosition().y - pussyX*Math.sin(-bang) + pussyY*Math.cos(-bang)) * SCALE;
            let posxoffset = (b.GetPosition().x + pussyX*Math.cos(-bang) + pussyYoffset*Math.sin(-bang)) * SCALE;
            let posyoffset = (b.GetPosition().y - pussyX*Math.sin(-bang) + pussyYoffset*Math.cos(-bang)) * SCALE;
            for (let t = 20; t < 20 + this.thrusts * 10; t+=10) {
                this.anim.keyframes["tr_x"][t] = posx;
                this.anim.keyframes["tr_y"][t] = posy;
                this.anim.keyframes["ang"][t] = bang;
            }
            for (let t = 22; t < 22 + this.thrusts * 10; t+=10) {
                this.anim.keyframes["tr_x"][t] = posxoffset;
                this.anim.keyframes["tr_y"][t] = posyoffset;
                this.anim.keyframes["ang"][t] = bang;
            }
        }

        clearCtx(this.params.canvas.cursorCtx);
        clearCtx(this.params.canvas.backEffectCtx);

        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.backEffectCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(v.tr_x, v.tr_y);
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage, -this.cursorImageShiftX, -this.cursorImageShiftY);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }
}


code:
class Drill extends ClickTool {
    static Resources = {
        images: [
            "drill", "drill_active", "screw",
            "wound_bone", "wound01", "wound02", "wound03", "hole_clothes",
        ],
        sounds: [
            "drill",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "Drill";
        this.cursorImage = this.images["drill"];
        this.cursorImageActive = this.images["drill_active"];
        this.cursorImageShiftX = I2W(15);
        this.cursorImageShiftY = I2W(182);
        this.collisionShape = circleShape(I2W(22));
        this.canTargetClothAlone = false;
    }

    execute() {
        let wound = this.images["wound" + zfill(randint(1, 3), 2)];
        let screw = this.images["screw"];

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, 0, {calculate_mask: true});
            part.draw(wound, BodyPart.MEAT_ORGANS, lp, 0, {mask: meatMask});
            part.draw(this.images["wound_bone"], BodyPart.BONE, lp, 0);
            part.draw(screw, BodyPart.SKIN, lp, 0);
            part.draw(screw, BodyPart.MEAT_ORGANS, lp, 0);
            part.drawOnClothes(this.images["hole_clothes"], lp, 0, {gco: "destination-out"});
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_strong", 25, direction]);
        character.setEyes(["pain_strong", 25]);
        if (character.hp > 55) {
            if (flags.neck || flags.head)
                character.forceShadow1(60);
            character.setMouth([["open_medium", 30], ["clench_medium", 30]]);
        } else {
            if (flags.neck || flags.head)
                character.forceShadow2(60);
            character.setMouth([["open_squiggly", 30], ["clench_strong", 30]]);
        }

    }

    damage(character, flags) {
        if (flags.leye)
            character.destroyLeye("open");
        if (flags.reye)
            character.destroyReye("open");
        if (flags.head || flags.neck) {
            character.changeHP(-12, true);
        } else if (flags.direction == "center") {
            character.changeHP(-6);
            character.applyBleeding(0.5);
        } else {
            character.changeHP(-2);
        }
    }

    animate() {
        let animateStep = this.animateStep;

        if (this._shortcut) {
            this.execute();
            this.finalize();
            return;
        }

        if (animateStep == 0) {
            playSound(this.sounds["drill"], this.params.volume);
            this.doReactionAll();
            this.execute();
            this.damageAll();
        } else if (animateStep == 45 || (animateStep >= 25 && this.sounds["drill"].ended)) {
            this.finalize();
            return;
        }

        // Cursor shaking
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(this.mx, this.my);
        if (parseInt(animateStep/4) % 2 == 0) {
            ctx.rotate(D2R(15));
        } else {
            ctx.rotate(D2R(-15));
        }
        ctx.translate(-I2W(200), -I2W(80));
        ctx.drawImage(this.cursorImageActive, 0, 0);
        ctx.restore();

        this.animateStep++;
        setTimeout(() => { this.animate(); }, TIMESTEP);
    }
}


code:
class Enema extends SingleTargetTool {
    static Resources = {
        images: [
            "enema01", "enema02",
            "shit",
        ],
        sounds: [
            "injection", "shitting",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.character = undefined;
        this.waist = undefined;

        this.cursorImage1 = this.images["enema01"];
        this.cursorImage2 = this.images["enema02"];
        this.shit = this.images["shit"];

        this.name = "Enema";
        this.cursorImage = this.cursorImage1;
        this.cursorImageShiftX = I2W(40);
        this.cursorImageShiftY = I2W(40);

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0, "tr_x": 0, "tr_y": 0, "image": this.cursorImage1, "alpha": 1, "shity": I2W(-100)}, ()=>{
            this.doReaction1();
        });
        this.anim.addKeyframe(18, {"ang": 0, "tr_x": 0, "tr_y": 0});
        this.anim.addKeyframe(20, {"image": this.cursorImage2}, ()=>{
            this.doReaction2();
            playSound(this.sounds["injection"], this.params.volume);
        });
        this.anim.addKeyframe(50, {"alpha": 1}, ()=>{
            this.doReaction3();
        });
        this.anim.addKeyframe(109, {"ang": 0, "tr_x": 0, "tr_y": 0, "alpha": 0, "image": this.shit});
        this.anim.addKeyframe(110, {"ang": 0, "tr_x": 0, "tr_y": 0, "alpha": 1, "shity": I2W(-100)}, ()=>{
            this.doReaction4();
            playSound(this.sounds["shitting"], this.params.volume);
        });
        this.anim.addKeyframe(170, {"ang": 0, "tr_x": 0, "tr_y": 0, "alpha": 1, "shity": I2W(200)});
        this.anim.addKeyframe(230, {"ang": 0, "tr_x": 0, "tr_y": 0, "alpha": 0, "shity": I2W(500)}, ()=>{
            clearCtx(this.params.canvas.backEffectCtx);
            this.finalize();
        });
        this.anim.interpolations["ang"] = "rotation";
    }

    // Insert enema
    doReaction1() {
        if (!this.singleCharacterFeeling || this.character.dead())
            return;
        this.character.forceSweat(60, 1);
        this.character.forceShadow1(60);
        this.character.setEyes(["surprised_weak", 30]);
        this.character.setMouth(["open_medium", 30]);
    }

    // Inject liquid
    doReaction2() {
        if (!this.singleCharacterFeeling || this.character.dead())
            return;
        this.character.forceSweat(100, 2);
        this.character.forceShadow1(100);
        this.character.forceBlush(100);
        this.character.setEyes(["surprised_weak", 100]);
        this.character.setMouth(["clench_medium", 100]);
    }

    // Holding liquid
    doReaction3() {
        if (!this.singleCharacterFeeling || this.character.dead())
            return;
        this.character.forceSweat(200, 2);
        this.character.forceShadow1(200);
        this.character.forceBlush(200);
        this.character.setEyes([["surprised_weak", 30], ["pain_strong", 100]]);
        this.character.setMouth([["clench_weak", 30], ["clench_strong", 100]]);
    }

    // Pull out
    doReaction4() {
        if (!this.singleCharacterFeeling || this.character.dead())
            return;
        this.character.forceSweat(100, 2);
        this.character.forceShadow1(100);
        this.character.forceBlush(180);
        this.character.setEyes([["ashamed_strong", 50], ["surprised_weak", 50], ["ashamed_weak", 80]]);
        this.character.setMouth([["clench_strong", 50], ["open_medium", 50], ["clench_weak", 80]]);
    }

    animate() {
        this.character = this.singleCharacterTarget;
        this.waist = this.character.waist;

        if (this.waist.destroyed) {
            clearCtx(this.params.canvas.backEffectCtx);
            this.finalize();
            return;
        }

        this.anim.keyframes["tr_x"][0] = this.mx;
        this.anim.keyframes["tr_y"][0] = this.my;

        let b = this.waist.box2dBody;
        let bang = b.GetAngle();
        let assX = this.character.pussyRegion.GetLocalPosition().x;
        let assY = this.character.pussyRegion.GetLocalPosition().y;
        let posx = (b.GetPosition().x + assX*Math.cos(-bang) + assY*Math.sin(-bang)) * SCALE;
        let posy = (b.GetPosition().y - assX*Math.sin(-bang) + assY*Math.cos(-bang)) * SCALE;
        for (const t of [18, 109]) {
            this.anim.keyframes["tr_x"][t] = posx;
            this.anim.keyframes["tr_y"][t] = posy;
            this.anim.keyframes["ang"][t] = bang - D2R(45);
        }
        for (const t of [110, 170, 230]) {
            let assYoffset = assY + this.anim.keyframes["shity"][t] / SCALE;
            let posxoffset = (b.GetPosition().x + assX*Math.cos(-bang) + assYoffset*Math.sin(-bang)) * SCALE;
            let posyoffset = (b.GetPosition().y - assX*Math.sin(-bang) + assYoffset*Math.cos(-bang)) * SCALE;
            this.anim.keyframes["tr_x"][t] = posxoffset;
            this.anim.keyframes["tr_y"][t] = posyoffset;
            this.anim.keyframes["ang"][t] = bang;
        }

        clearCtx(this.params.canvas.cursorCtx);
        clearCtx(this.params.canvas.backEffectCtx);

        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.backEffectCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.globalAlpha = v.alpha;
        let offX, offY;
        if (v.image == this.images["shit"]) {
            offX = I2W(200);
            offY = I2W(50);
        } else {
            offX = this.cursorImageShiftX;
            offY = this.cursorImageShiftY;
        }
        ctx.translate(v.tr_x, v.tr_y);
        ctx.rotate(v.ang);
        ctx.drawImage(v.image, -offX, -offY);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }
}


code:
class Fireaxe extends ChargeTool {
    static Resources = {
        images: [
            "fireaxe",
            "wound01", "wound_meat01", "wound_cloth01",
            "wound02", "wound_meat02", "wound_cloth02",
        ],
        sounds: [
            "fireaxe01", "fireaxe02", "fireaxe_slice",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.world = params.world;
        this.ang = 0;

        this.name = "Fireaxe";
        this.cursorImage = this.images["fireaxe"];
        this.cursorImageShiftX = I2W(64);
        this.cursorImageShiftY = I2W(198);
        this.maxCharge = 15;
        this.hitRegisterChargeLimit = 3; // Minimum charge for hit to have an effect
        this.bruiseChargeLimit = 13;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0});
        this.anim.addKeyframe(this.maxCharge, {"ang": D2R(55)});
        this.anim.addKeyframe(this.maxCharge + 4, {"ang": 0}, ()=>{
            if (this.currentCharge > this.bruiseChargeLimit) {
                playSound(this.sounds["fireaxe_slice"], this.params.volume);
            } else {
                playSound(this.sounds["fireaxe" + zfill(randint(1, 2), 2)], this.params.volume);
            }

            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
            this.finalize();
        });
        this.anim.interpolations["ang"] = "rotation";
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(360));
        this.collisionShape = rectShape(I2W(56), I2W(210), this.ang);
    }

    updateChargingCursor(x, y) {
        let v = this.anim.getValues(this.currentCharge);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            x + this.cursorImage.width * 15 / 16,
            y + this.cursorImage.height * 15 / 16
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
    }

    execute() {
        let woundIdx = zfill(randint(1, 2), 2)
        let wound = this.images["wound" + woundIdx];
        let woundMeat = this.images["wound_meat" + woundIdx];
        let woundCloth = this.images["wound_cloth" + woundIdx];

        let slice = this.currentCharge > this.bruiseChargeLimit;
        let ang = this.ang;
        let garbageBodies = new Array();
        let notch = this.params.tools["Saw"].images["notch"];
        let blood = this.params.tools["Saw"].images["blood"];
        for (const [part, lp] of this.hitParts) {
            let name = part.name;
            let b = part.box2dBody;
            let character = part.character;

            if (slice) {
                garbageBodies.push(b);
                let newParts = part.split(lp);
                character.updateBodyParts(part, newParts);

                // Stop breathing
                switch (name) {
                    case "neck":;
                    case "breast":
                        character.stopBreath("breast");
                    case "belly": character.stopBreath("belly");
                }

                // TODO make this code original and not dependent on Saw
                let bang = b.GetAngle();
                part.draw(notch, BodyPart.CONTOUR, lp, bang, {gco: "destination-out"});
                part.draw(notch, BodyPart.SKIN, lp, bang, {gco: "destination-out"});
                part.draw(notch, BodyPart.MEAT_ORGANS, lp, bang, {gco: "destination-out"});
                part.draw(notch, BodyPart.BONE, lp, bang, {gco: "destination-out"});
                let meatBloodMask = part.draw(blood, BodyPart.SKIN, lp, bang, {calculate_mask: true});
                let boneBloodMask = part.draw(blood, BodyPart.MEAT_ORGANS, lp, bang, {calculate_mask: true, mask: meatBloodMask});
                part.draw(blood, BodyPart.BONE, lp, bang, {mask: boneBloodMask});
                part.drawOnClothes(notch, lp, bang, {gco: "destination-out"});
            } else {
                part.draw(woundMeat, BodyPart.SKIN, lp, ang, {gco: "destination-out"});
                let meatMask = part.draw(wound, BodyPart.SKIN, lp, ang, {calculate_mask: true});
                part.draw(wound, BodyPart.MEAT_ORGANS, lp, ang, {mask: meatMask});
                part.drawOnClothes(woundCloth, lp, ang, {gco: "destination-out"});
            }
        }

        // Disposing garbage bodies
        if (slice) {
            for (let i = 0; i < garbageBodies.length; i++) {
                this.world.DestroyBody(garbageBodies[i]);
                garbageBodies[i] = null;
            }
            refreshConnectedBodies(this.params);
        }
    }

    doReaction(character, flags, direction) {
        if (direction == "center") {
            if (character.hp > 75) {
                character.forceShadow1(50);
                character.setEyes([["surprised_strong", 25], ["pain_strong", 25]]);
                character.setMouth([["clench_strong", 25], ["open_medium", 25]]);
            } else {
                character.forceShadow2(50);
                character.setEyes([["surprised_strong", 25], ["pain_strong", 25]]);
                character.setMouth([["clench_squiggly", 25], ["open_squiggly", 25]]);
            }
        } else {
            character.setEyes([["pain_strong", 25, direction], ["pain_weak", 25, direction]]);
            character.setMouth([["clench_strong", 25], ["open_medium", 25]]);
        }
    }

    damage(character, flags) {
        if (this.currentCharge > this.bruiseChargeLimit) {
            if (flags.neck) {
                character.changeHP(-100, true);
            } else if (flags.heart) {
                character.changeHP(-50, true);
                character.applyBleeding(5, true);
            } else if (flags.direction == "center") {
                character.changeHP(-25);
                character.applyBleeding(3, true);
            } else {
                character.changeHP(-6);
                if (flags.waist || flags.belly) {
                    character.applyBleeding(3, true);
                } else {
                    character.applyBleeding(1.5);
                }
            }
        } else {
            if (flags.direction == "center") {
                character.changeHP(-10);
                character.applyBleeding(2, false);
            } else {
                character.changeHP(-5);
            }
        }
    }

    sliceCollision(part, mx, my) {
        if (!pointCollisionWithBody(part.box2dBody, mx, my))
            return false;
        let name = part.name;
        if (name == "head")
            return false;
        let lp = part.box2dBody.GetLocalPoint(new b2Vec2(mx / SCALE, my / SCALE));

        // Special rules for overlapping bodies
        if (name == "waist" && lp.y * SCALE >= 40)
            return false;
        if (name == "waist" && part.top == 0 && lp.y * SCALE - part.top > I2W(28))
            return true;
        if (name == "breast" && part.top == 0 && lp.y * SCALE <= I2W(160))
            return false;
        if (name == "breast" && part.bottom == part.height && part.bottom - lp.y * SCALE > I2W(20))
            return true;
        if (name == "belly" && (part.bottom - lp.y * SCALE > I2W(28) || lp.y * SCALE - part.top > I2W(28)))
            return true;
        if (part.bottom - lp.y * SCALE <= I2W(40) || lp.y * SCALE - part.top <= I2W(40))
            return false; // discard too small items (which are close to invisible anyways)
        return true;
    }

    collision(part, mx, my) {
        if (this.currentCharge > this.bruiseChargeLimit) {
            return this.sliceCollision(part, mx, my);
        } else {
            return shapeCollisionWithBody(part.box2dBody, mx, my, this.collisionShape);
        }
    }

    animate() {
        if (this.currentCharge <= this.hitRegisterChargeLimit) {
            this.finalize();
            return;
        }
        let tick = this.maxCharge + this.animateStep * this.currentCharge / this.maxCharge + 4 * (this.maxCharge - this.currentCharge) / this.maxCharge;
        let v = this.anim.getValues(tick);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx + this.cursorImage.width * 15 / 16,
            this.my + this.cursorImage.height * 15 / 16
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
        this.anim.callActions(tick);

        super.animate();
    }
}


code:
class Gag extends SingleTargetTool {
    static Resources = {
        images: [
            "gag",
        ],
        sounds: []
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "Mouth gag";
        this.cursorImage = this.images["gag"];
        this.cursorImageShiftX = I2W(150);
        this.cursorImageShiftY = I2W(80);

        this.activeCharacters = {};
    }

    animate() {
        let character = this.singleCharacterTarget;
        if (this.animateStep == 0) {
            if (character.ID in this.activeCharacters) {
                let item = this.activeCharacters[character.ID]
                item.parent.removeItem(item);
                delete this.activeCharacters[character.ID];
            } else {
                if (!character.mouthForced) {
                    let item = new GagItem(this.images["gag"], character.head, character.mouthRegion.GetLocalPosition(), this.params, this);
                    character.head.addItem(item);
                    this.activeCharacters[character.ID] = item;
                }
            }

            this.finalize();
            return;
        }
        super.animate();
    }
}

class GagItem extends Item {
    constructor(image, parent, lp, params, tool) {
        super("Mouth gag", image, parent, lp, 0, params, 1000);
        this.parent.character.forceMouth("open_large");
        this.tool = tool;
        this.renderBeforeHairFlag = true;
    }

    setParent(part) {
        super.setParent(part);
        this.parent.removeItem(this);
    }

    render(ctx) {
        // Hacky rendering cause ctx is not the main canvas here, but the head canvas
        // So no box2d transformation is needed
        let x = this.lp.x * SCALE;
        let y = this.lp.y * SCALE;
        ctx.drawImage(this.image,
            x + (this.parent.canvas.width - this.image.width) / 2,
            y + (this.parent.canvas.height - this.parent.bottom - this.image.height) / 2);
    }

    delete() {
        this.parent.character.unforceMouth();
        delete this.tool.activeCharacters[this.parent.character.ID];
    }
}


code:
class Iron extends ClickTool {
    static Resources = {
        images: [
            "iron", "iron_active",
            "wound",
        ],
        sounds: [
            "iron",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "Iron";
        this.cursorImage = this.images["iron"];
        this.cursorImageActive = this.images["iron_active"];
        this.cursorImageShiftX = I2W(200);
        this.cursorImageShiftY = I2W(200);
        this.collisionShape = rectShape(I2W(300), I2W(370), 0);
        this.canTargetClothAlone = false;
    }

    initializeParticleEffects() {
        this.smokeEffect = new IronSmoke(this.rm, this.params);
    }

    execute() {
        let wound = this.images["wound"];

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, 0, {calculate_mask: true});
            part.draw(wound, BodyPart.MEAT_ORGANS, lp, 0, {mask: meatMask});
        }

        this.smokeEffect.apply(this.mx, this.my - I2W(100));
    }

    doReaction(character, flags, direction) {
        if (character.hp > 55) {
            character.forceTears(100, 3);
            character.setEyes(["pain_strong", 100, direction]);
            character.setMouth([["open_large", 50], ["clench_strong", 50]]);
        } else {
            character.setEyes(["pain_strong", 100]);
            character.setMouth([["open_squiggly", 50], ["clench_strong", 50]]);
        }
    }

    damage(character, flags) {
        if (flags.direction == "center") {
            character.changeHP(-10);
        } else {
            character.changeHP(-6);
        }
    }

    animate() {
        let animateStep = this.animateStep;

        if (animateStep == 0) {
            playSound(this.sounds["iron"], this.params.volume);
            this.doReactionAll();
            this.execute();
            this.damageAll();
        } else if (animateStep == 100) {
            this.finalize();
            return;
        }

        // Cursor shaking
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(this.mx, this.my);
        if (parseInt(animateStep/4) % 2 == 0) {
            ctx.translate(0, -I2W(10));
        } else {
            ctx.translate(0, -I2W(-10));
        }
        ctx.drawImage(this.cursorImageActive, -this.cursorImageShiftX, -this.cursorImageShiftY);
        ctx.restore();

        this.animateStep++;
        setTimeout(() => { this.animate(); }, TIMESTEP);
    }
}

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

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

        // initial parameters
        this.emitter.addInitialize(new Proton.Body("mods/tools/Iron/images/smoke.png", I2W(300), I2W(300)));
        this.emitter.addInitialize(new Proton.Life(2, 3));
        this.emitter.addInitialize(new Proton.Mass(1));
        this.emitter.addInitialize(new Proton.Velocity(new Proton.Span(1, 4), new Proton.Span(-60, 60), "polar"));

        // behaviours
        this.emitter.addBehaviour(new Proton.Scale(new Proton.Span(1, 0.5), new Proton.Span(1.25, 1.5)));
        this.emitter.addBehaviour(new Proton.Alpha(1, 0));
        this.emissionCount = 1.2;

        this.make();
    }
}


code:
class Knife extends ClickTool {
    static Resources = {
        images: [
            "knife",
            "cut01", "cut02", "cut03", "cutBone",
            "hole01", "hole02", "hole03",
        ],
        sounds: [
            "knife",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.ang = 0;

        this.name = "Knife";
        this.cursorImage = this.images["knife"];
        this.cursorImageShiftX = I2W(8);
        this.cursorImageShiftY = I2W(8);

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0});
        this.anim.addKeyframe(5, {"ang": D2R(45)}, ()=>{
            playSound(this.sounds["knife"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(6, {"ang": D2R(-45)});
        this.anim.addKeyframe(10, {}, ()=>{this.finalize();});
        this.anim.interpolations["ang"] = "rotation";
    }

    reset(mx, my, shortcut) {
        super.reset(mx, my, shortcut);
        this.ang = randfloat(D2R(360));
        this.collisionShape = rectShape(I2W(44), I2W(160), this.ang);
    }

    execute() {
        let imageNum = zfill(randint(1, 3), 2);
        let cut = this.images["cut" + imageNum];
        let hole = this.images["hole" + imageNum];
        let ang = this.ang;

        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(cut, BodyPart.SKIN, lp, ang, {calculate_mask: true});
            let boneMask = part.draw(cut, BodyPart.MEAT_ORGANS, lp, ang, {calculate_mask: true, mask: meatMask});
            part.draw(this.images["cutBone"], BodyPart.BONE, lp, ang, {mask: boneMask});
            part.drawOnClothes(hole, lp, ang, {gco: "destination-out"});
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_weak", 25, direction]);
        character.setMouth(["clench_strong", 25]);
    }

    damage(character, flags) {
        if (flags.leye)
            character.destroyLeye("close");
        if (flags.reye)
            character.destroyReye("close");
        if (flags.direction == "center") {
            character.changeHP(-2);
        } else {
            character.changeHP(-1);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx - this.cursorImage.width / 2 + this.cursorImage.width,
            this.my - this.cursorImage.height / 2 + this.cursorImage.height
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }
}


code:
class Lighter extends DragTool {
    static Resources = {
        images: [
            "lighter_off", "lighter_on1", "lighter_on2",
            "wound", "wound_hard",
        ],
        sounds: [
            "lighter",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.ang = 0;

        this.name = "Lighter";
        this.cursorImageOff = this.images["lighter_off"];
        this.cursorImageOn1 = this.images["lighter_on1"];
        this.cursorImageOn2 = this.images["lighter_on2"];
        this.cursorImageAnim = 0;
        this.cursorImage = this.cursorImageOff;
        this.cursorImageShiftX = I2W(76);
        this.cursorImageShiftY = I2W(16);
        this.collisionShape = circleShape(I2W(35));
    }

    initializeParticleEffects() {
        this.smokeEffect = new LighterSmoke(this.rm, this.params);
    }

    execute() {
        let wound = this.cursorImageAnim > 100 ? this.images["wound_hard"] : this.images["wound"];
        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(wound, BodyPart.SKIN, lp, this.ang, {calculate_mask: true});
            part.draw(wound, BodyPart.MEAT_ORGANS, lp, this.ang, {mask: meatMask});
        }
    }

    doReaction(character, flags, direction) {
        if (this.cursorImageAnim > 50) {
            character.setEyes(["pain_weak", 20, direction]);
            character.setMouth(["open_medium", 20]);
        } else {
            character.setEyes(["ashamed_weak", 20, direction]);
            character.setMouth(["open_small", 20]);
        }
    }

    damage(character, flags) {
        if (flags.leye)
            character.destroyLeye("open");
        if (flags.reye)
            character.destroyReye("open");
        character.changeHP(-0.03);
    }

    drag() {
        if (this.cursorImageAnim == 0) {
            this.ang = randfloat(D2R(360)); // Reroll angle only when the dragging is started
            playSound(this.sounds["lighter"], this.params.volume);
        }
        this.cursorImageAnim++;
        if (parseInt(this.cursorImageAnim/4) % 2 == 0) {
            this.cursorImage = this.cursorImageOn2;
            this.smokeEffect.apply(this.mx, this.my - I2W(10));
        } else {
            this.cursorImage = this.cursorImageOn1;
        }
        this.execute();
        this.doReactionAll();
        this.damageAll();
    }

    mouseUp(mx, my) {
        super.mouseUp(mx, my);
        this.cursorImageAnim = 0;
        this.cursorImage = this.cursorImageOff;
    }
}

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

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

        // initial parameters
        this.emitter.addInitialize(new Proton.Body("mods/tools/Lighter/images/smoke.png", I2W(200), I2W(200)));
        this.emitter.addInitialize(new Proton.Life(1, 2));
        this.emitter.addInitialize(new Proton.Mass(1));
        this.emitter.addInitialize(new Proton.Velocity(new Proton.Span(1, 3), new Proton.Span(-30, 30), "polar"));

        // behaviours
        this.emitter.addBehaviour(new Proton.Rotate());
        this.emitter.addBehaviour(new Proton.Scale(new Proton.Span(1, 0.75), new Proton.Span(1.25, 1.5)));
        this.emitter.addBehaviour(new Proton.Alpha(1, 0));
        this.emissionCount = 0.4;

        this.make();
    }
}


code:
class Marker extends DragTool {
    static Resources = {
        images: [
            "marker", "marker_color",
        ],
        sounds: []
    };

    constructor(params, rm) {
        super(params, rm);
        this.gui = true;

        this.name = "Marker";
        this.cursorImage = createCanvas(this.images["marker"]);
        this.cursorCtx = this.cursorImage.getContext("2d");
        this.cursorImageShiftX = I2W(23);
        this.cursorImageShiftY = I2W(383);
        this.collisionShape = circleShape(I2W(6)); // TODO rework collision (rectShape based on current line direction)
        this.color = "#ff0000";
        this.lineWidth = 4;
        this.alpha = 255;
        this.tempCanvas = createCanvas(1000, 1000);
        this.tempCtx = this.tempCanvas.getContext("2d");
        this.setColor(this.color);
        this.setLineWidth(this.lineWidth);
        this.tempCtx.lineCap = "round";
    }

    setLineWidth(lineWidth) {
        this.lineWidth = lineWidth;
        this.tempCtx.lineWidth = this.lineWidth;
    }

    setColor(color) {
        this.color = color;
        this.tempCtx.strokeStyle = addAlpha(this.color, this.alpha);
        clearCtx(this.cursorCtx);
        this.cursorCtx.drawImage(this.images["marker_color"], 0, 0);
        this.cursorCtx.save();
        this.cursorCtx.fillStyle = this.color;
        this.cursorCtx.globalCompositeOperation = "source-atop";
        this.cursorCtx.fillRect(0, 0, this.cursorImage.width, this.cursorImage.height);
        this.cursorCtx.restore();
        this.cursorCtx.drawImage(this.images["marker"], 0, 0);
    }

    setAlpha(alpha) {
        this.alpha = alpha;
        this.tempCtx.strokeStyle = addAlpha(this.color, this.alpha);
    }

    execute() {
        clearCtx(this.tempCtx);
        this.tempCtx.save();
        this.tempCtx.beginPath();
        this.tempCtx.lineTo(this.prevMX - this.mx + this.tempCanvas.width / 2,
                            this.prevMY - this.my + this.tempCanvas.height / 2);
        this.tempCtx.lineTo(this.tempCanvas.width / 2, this.tempCanvas.height / 2);
        this.tempCtx.stroke();
        this.tempCtx.restore();
        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(this.tempCanvas, BodyPart.SKIN, lp, 0, {calculate_mask: true});
            let boneMask = part.draw(this.tempCanvas, BodyPart.MEAT_ORGANS, lp, 0, {calculate_mask: true, mask: meatMask});
            part.draw(this.tempCanvas, BodyPart.BONE, lp, 0, {mask: boneMask});
            part.drawOnClothes(this.tempCanvas, lp, 0);
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["ashamed_weak", 20, direction]);
    }

    drag() {
        this.execute();
        this.doReactionAll();
    }
}

class MarkerGUI extends preact.Component {
    constructor(props) {
        super(props);
    }

    setLineWidth = (x) => {
        this.props.tool.setLineWidth(x);
    }

    setColor = (x) => {
        this.props.tool.setColor(x);
    }

    setAlpha = (x) => {
        this.props.tool.setAlpha(x);
    }

    render() {
        return(html`
            <${RangeInput} min=1 max=10 value=${this.props.tool.lineWidth} onChange=${this.setLineWidth} label="Line width"/>
            <${ColorPicker} value="${this.props.tool.color}" onChange=${this.setColor} label="Color"/>
            <${RangeInput} min=0 max=255 value=${this.props.tool.alpha} onChange=${this.setAlpha} label="Alpha"/>
        `);
    }
}


code:
class Nail extends ClickTool {
    static Resources = {
        images: [
            "hammer", "nail",
            "nail01", "nail02", "nail03", "nail04", "nail05",
            "nail06", "nail07", "nail08", "nail09", "nail10",
        ],
        sounds: [
            "nail",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.cursorImage2 = this.images["hammer"];
        this.name = "Nail";
        this.cursorImage = this.images["nail"];
        this.cursorImageShiftX = I2W(20);
        this.cursorImageShiftY = I2W(20);
        this.canTargetClothAlone = false;
    }

    execute() {
        let nailImage = this.images["nail" + zfill(randint(1, 10), 2)];

        for (const [part, lp] of this.hitParts) {
            let name = part.name;
            let character = part.character;

            switch (name) {
                case "head":;
                case "neck":;
                case "breast":
                    character.stopBreath("breast");
                case "belly": character.stopBreath("belly");
            }

            // Check if can be fixed to the wall -- if yes, then draw the nail
            let joint = this.params.map.addWallFixation(part.box2dBody, lp.x, lp.y);
            if (joint) {
                let nailItem = new NailItem(nailImage, part, lp, this.params, joint);
                part.addItem(nailItem);
            }
            break;
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_strong", 50]);
        if (character.hp > 55) {
            if (flags.heart || flags.neck || flags.head)
                character.forceShadow1(50);
            character.setMouth([["clench_medium", 25], ["open_medium", 25]]);
        } else {
            if (flags.heart || flags.neck || flags.head)
                character.forceShadow2(50);
            character.setMouth([["clench_strong", 25], ["open_large", 25]]);
        }
    }

    damage(character, flags) {
        if (flags.mouth)
            character.forceMouth("open_medium");
        if (flags.leye)
            character.destroyLeye("open");
        if (flags.reye)
            character.destroyReye("open");
        if (flags.head || flags.neck || flags.heart) {
            character.changeHP(-15, true);
        } else if (flags.direction == "center") {
            character.changeHP(-7);
            character.applyBleeding(2.5, true);
        } else {
            character.changeHP(-3);
            if (flags.waist || flags.belly) {
                character.applyBleeding(2.5, true);
            } else {
                character.applyBleeding(1);
            }
        }
    }

    animate() {
        let animateStep = this.animateStep;

        // Skip animation
        if (this.shortcut) {
            this.execute();
            this.finalize();
            return;
        }

        if (animateStep == 0) {
            playSound(this.sounds["nail"], this.params.volume);
            this.doReactionAll();
            this.execute();
            this.damageAll();
        } else if (animateStep == 45 || (animateStep >= 25 && this.sounds["nail"].ended)) {
            this.finalize();
            return;
        }

        clearCtx(this.params.canvas.cursorCtx);
        // Cursor shaking
        if (animateStep % 2 == 0) {
            this.params.canvas.cursorCtx.drawImage(this.cursorImage2,
                this.mx - this.cursorImageShiftX + I2W(20),
                this.my - this.cursorImageShiftY - I2W(140 - 20));
        } else {
            this.params.canvas.cursorCtx.drawImage(this.cursorImage2,
                this.mx - this.cursorImageShiftX - I2W(20),
                this.my - this.cursorImageShiftY - I2W(140 + 20));
        }

        this.animateStep++;
        setTimeout(() => { this.animate(); }, TIMESTEP);
    }
}

class NailItem extends Item {
    constructor(image, parent, lp, params, joint) {
        super("Nail", image, parent, lp, 0, params, 5000);
        this.joint = joint;
    }

    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(-bang); // Disallow (undo) rotation
        ctx.translate(-this.image.width/2, -this.image.height/2);
        ctx.drawImage(this.image, 0, 0);
        ctx.restore();
    }

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


code:
class Needle extends ClickTool {
    static Resources = {
        images: [
            "needle",
            "needle_in", "blood",
        ],
        sounds: [
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.gui = true;

        this.cursorImage = this.images["needle"];

        this.name = "Needle";
        this.cursorImageShiftX = I2W(20);
        this.cursorImageShiftY = I2W(20);
    }

    execute() {
        let blood = this.images["blood"];
        let ang = randfloat(D2R(360));

        let first = true;
        for (const [part, lp] of this.hitParts) {
            if (first) part.addItem(this.name, this.images["needle_in"], lp, ang, 900);
            if (randint(0, 1) == 1) { // Only draw blood for half of the activations
                part.draw(blood, BodyPart.SKIN, lp, ang);
                part.drawOnClothes(blood, lp, ang);
            }
            first = false;
        }
    }

    doReaction(character, flags, direction) {
        character.setMouth(["clench_medium", 25]);
        if (flags.leye || flags.reye || flags.lnipple || flags.rnipple || flags.pussy) {
            character.setEyes(["surprised_weak", 25, direction]);
        } else {
            character.setEyes(["pain_weak", 25, direction]);
        }
    }

    damage(character, flags) {
        if (flags.leye)
            character.destroyLeye("close");
        if (flags.reye)
            character.destroyReye("close");
        if (flags.head) {
            character.changeHP(-1);
        } else {
            character.changeHP(-0.5);
        }
    }

    animate() {
        if (this.animateStep == 0) {
            this.execute();
            this.doReactionAll();
            this.damageAll();
        }

        if (this.animateStep == 4) {
            this.finalize();
        }

        super.animate();
    }

    // Pulls out every needle
    pull() {
        let yankedCharacters = new Set();
        for (const item of [...this.params.items]) {
            if (item.name == this.name) {
                if (connectedWith(item.parent, item.parent.character.head)) {
                    yankedCharacters.add(item.parent.character);
                }
                item.parent.removeItem(item);
            }
        }
        for (const character of yankedCharacters) {
            character.setMouth(["clench_strong", 40]);
            character.setEyes(["pain_weak", 40]);
        }
    }
}

class NeedleGUI extends preact.Component {
    constructor(props) {
        super(props);
    }

    onClick = () => {
        this.props.tool.pull();
    }

    render() {
        return(html`
            <${Button} onClick=${this.onClick} label="Pull out"/>
        `);
    }
}


code:
class Punch extends ChargeTool {
    static Resources = {
        images: [
            "fist",
            "boko01", "boko02",
        ],
        sounds:[
            "punch_strong", "punch_weak",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "Punch";
        this.cursorImage = this.images["fist"];
        this.cursorImageShiftX = I2W(160);
        this.cursorImageShiftY = I2W(160);
        this.collisionShape = circleShape(I2W(96));
        this.canTargetClothAlone = false;

        this.maxCharge = 20;
        this.bellyHitChargeLimit = 10;
        this.hitRegisterChargeLimit = 3; // Minimum charge for hit to have an effect
    }

    updateChargingCursor(x, y) {
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.drawImage(this.cursorImage,
            0, 0, this.cursorImage.width, this.cursorImage.height,
            x - this.cursorImageShiftX,
            y - this.cursorImageShiftY,
            this.cursorImage.width * (this.currentCharge/40+1), this.cursorImage.height * (this.currentCharge/40+1));
    }

    // Thrust the body parts in a random direction
    applyImpulses() {
        let ix, iy;
        [ix, iy] = randcircle(5);
        for (const [part, lp] of this.hitParts) {
            let b = part.box2dBody;
            let mass = b.GetMass();
            let scale = this.currentCharge / this.maxCharge;
            b.ApplyImpulse(new b2Vec2(ix * mass * scale, iy * mass * scale), lp);
        }
    }

    execute() {
        let imageNum = zfill(randint(1, 2), 2);
        let boko = this.images["boko" + imageNum];
        let alpha = this.currentCharge / this.maxCharge;
        for (const [part, lp] of this.hitParts) {
            let meatMask = part.draw(boko, BodyPart.SKIN, lp, 0, {calculate_mask: true, alpha: alpha});
            part.draw(boko, BodyPart.MEAT_ORGANS, lp, 0, {mask: meatMask, alpha: alpha});
        }
    }

    doReaction(character, flags, direction) {
        if (flags.belly && this.currentCharge > this.bellyHitChargeLimit) {
            if (character.hp > 50) {
                character.forceShadow1(25);
                character.setEyes([["surprised_weak", 25], ["pain_weak", 25]]);
                character.setMouth([["open_medium", 25], ["open_small", 25]]);
            } else {
                character.forceShadow1(25);
                character.setEyes([["surprised_weak", 25], ["pain_strong", 25]]);
                character.setMouth([["open_large", 25], ["open_large", 25]]);
            }
            if (character.mouthState != "close") {
                character.spit();
            }
        } else {
            character.setEyes(["pain_weak", 25]);
            if (direction == "center") {
                character.setMouth(["clench_strong", 25]);
            } else {
                character.setMouth(["clench_medium", 25]);
            }
        }
    }

    damage(character, flags) {
        if (flags.nose)
            character.destroyNose();
        let damageScale = this.currentCharge / this.maxCharge;
        if (flags.belly) {
            character.changeHP(-5.5 * damageScale);
        } else if (flags.direction == "center") {
            character.changeHP(-2 * damageScale);
        } else {
            character.changeHP(-1 * damageScale);
        }
    }

    animate() {
        let animateStep = this.animateStep;
        if (this.currentCharge <= this.hitRegisterChargeLimit) {
            this.finalize();
            return;
        }
        if (animateStep == 2) {
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
            this.applyImpulses();

            let bellyFlag = false; // If any belly is hit
            for (let chidx in this.characterFlags) {
                if (this.characterFlags[chidx].belly) {
                    bellyFlag = true;
                    break;
                }
            }
            if (bellyFlag && this.currentCharge > this.bellyHitChargeLimit) {
                playSound(this.sounds["punch_strong"], this.params.volume);
            } else {
                playSound(this.sounds["punch_weak"], this.params.volume);
            }
        }

        // Cursor drawing
        let ctx = this.params.canvas.cursorCtx;
        let fistScale = 1;
        if (animateStep <= 2) {
            fistScale = 1 + this.currentCharge/40 * (2-animateStep)/2; // Shrink cursor
        }
        clearCtx(ctx);
        ctx.drawImage(this.cursorImage,
            0, 0, this.cursorImage.width, this.cursorImage.height,
            this.mx - this.cursorImageShiftX,
            this.my - this.cursorImageShiftY,
            this.cursorImage.width * fistScale, this.cursorImage.height * fistScale);

        // End of animation
        if (animateStep == 5) {
            this.finalize();
            return;
        }

        this.animateStep++;
        setTimeout(() => { this.animate(); }, TIMESTEP);
    }
}


code:
class Razor extends ClickTool {
    static Resources = {
        images: [
            "razor",
            "notch", "blood_meat", "blood_skin", "hole_clothes",
        ],
        sounds: [
            "razor",
        ]
    };

    constructor(params, rm) {
        super(params, rm);
        this.gui = true;

        this.name = "Razor";
        this.cursorImage = this.images["razor"];
        this.cursorImageShiftX = I2W(150);
        this.cursorImageShiftY = I2W(240);
        this.itemRemovalStrength = 1100;
        this.canTargetClothAlone = false;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"ang": 0});
        this.anim.addKeyframe(5, {"ang": D2R(45)}, ()=>{
            playSound(this.sounds["razor"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.removeHitItems();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(6, {"ang": D2R(-45)});
        this.anim.addKeyframe(10, {}, ()=>{this.finalize();});
        this.anim.interpolations["ang"] = "rotation";

        this.setScale(1); // Tool effect scale
    }

    execute() {
        let ang = randfloat(2 * Math.PI);
        for (const [part, lp] of this.hitParts) {
            part.draw(this.images["notch"], BodyPart.CONTOUR, lp, ang, {gco: "destination-out", scale: this.scale});
            part.draw(this.images["notch"], BodyPart.SKIN, lp, ang, {gco: "destination-out", scale: this.scale});
            part.draw(this.images["blood_skin"], BodyPart.SKIN, lp, ang, {scale: this.scale});
            part.draw(this.images["blood_meat"], BodyPart.MEAT_ORGANS, lp, ang, {scale: this.scale});
            part.drawOnClothes(this.images["hole_clothes"], lp, ang, {gco: "destination-out", scale: this.scale});
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_strong", 40, direction]);
        character.setMouth(["clench_strong", 40]);
    }

    damage(character, flags) {
        let dmgScale = Math.sqrt(this.scale);
        if (flags.leye)
            character.destroyLeye("close");
        if (flags.reye)
            character.destroyReye("close");
        if (flags.direction == "center") {
            character.changeHP(-10 * dmgScale);
            character.applyBleeding(1 * dmgScale);
        } else {
            character.changeHP(-6 * dmgScale);
            character.applyBleeding(0.5 * dmgScale);
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(
            this.mx - this.cursorImage.width / 2 + this.cursorImage.width,
            this.my - this.cursorImage.height / 2 + this.cursorImage.height
        );
        ctx.rotate(v.ang);
        ctx.drawImage(this.cursorImage,
            -this.cursorImage.width, -this.cursorImage.height);
        ctx.restore();
        this.anim.callActions(this.animateStep);

        super.animate();
    }

    setScale(scale) {
        this.scale = scale;
        this.collisionShape = circleShape(I2W(70) * this.scale);
    }
}

class RazorGUI extends preact.Component {
    constructor(props) {
        super(props);
    }

    setScale = (scale) => {
        this.props.tool.setScale(scale);
    }

    render() {
        return(html`
            <${RangeInput} min=0.3 max=2 step=0.1 value=${this.props.tool.scale} onChange=${this.setScale} label="Scale"/>
        `);
    }
}


code:
class Revolver extends ClickTool {
    static Resources = {
        images: [
            "wound01_skin", "wound02_skin", "wound01_meat", "wound02_meat", "wound01_bone", "wound02_bone",
            "revolver01", "revolver02", "splatter", "wall_hole01", "wall_hole02",
        ],
        sounds: [
            "revolver",
        ]
    };

    constructor(params, rm) {
        super(params, rm);

        this.name = "Revolver";
        this.cursorImage = this.images["revolver01"];
        this.cursorImage2 = this.images["revolver02"];
        this.cursorImageShiftX = I2W(62);
        this.cursorImageShiftY = I2W(382);
        this.collisionShape = circleShape(I2W(20));
        this.allowInvalidHit = true;

        this.anim = new Animation();
        this.anim.addKeyframe(0, {"image": this.cursorImage});
        this.anim.addKeyframe(2, {}, ()=>{
            playSound(this.sounds["revolver"], this.params.volume);
            this.execute();
            this.doReactionAll();
            this.damageAll();
            this.releaseStoppedBodies();
        });
        this.anim.addKeyframe(3, {"image": this.cursorImage2});
        this.anim.addKeyframe(10, {}, ()=>{this.finalize();});
    }

    execute() {
        let imageNum = zfill(randint(1, 2), 2);
        let woundSkin = this.images["wound" + imageNum + "_skin"];
        let woundMeat = this.images["wound" + imageNum + "_meat"];
        let woundBone = this.images["wound" + imageNum + "_bone"];
        let wallHole = this.images["wall_hole" + imageNum];
        let ang = randfloat(D2R(360));

        for (const [part, lp] of this.hitParts) {
            part.draw(woundSkin, BodyPart.SKIN, lp, ang);
            part.draw(woundMeat, BodyPart.MEAT_ORGANS, lp, ang);
            part.draw(woundBone, BodyPart.BONE, lp, ang);
            part.drawOnClothes(woundMeat, lp, ang);
        }

        this.params.map.drawOnBackground(wallHole, this.mx, this.my, ang);
        if (this.validHit) {
            this.params.map.drawOnBackground(this.images["splatter"], this.mx, this.my, 0);
        }
    }

    doReaction(character, flags, direction) {
        character.setEyes(["pain_strong", 20, direction]);
        character.setMouth(["open_squiggly", 20]);
    }

    damage(character, flags) {
        if (flags.head) {
            character.changeHP(-100, true);
        } else if (flags.heart || flags.neck) {
            character.changeHP(-95, true);
            character.applyBleeding(4, true);
        } else if (flags.direction == "center") {
            character.changeHP(-20);
            character.applyBleeding(2, true);
        } else {
            character.changeHP(-10);
            if (flags.waist || flags.belly) {
                character.applyBleeding(2, true);
            } else {
                character.applyBleeding(1);
            }
        }
    }

    animate() {
        let v = this.anim.getValues(this.animateStep);
        this.anim.callActions(this.animateStep);
        let ctx = this.params.canvas.cursorCtx;
        clearCtx(ctx);
        ctx.save();
        ctx.translate(this.mx, this.my);
        ctx.drawImage(v.image,
            -this.cursorImageShiftX, -this.cursorImageShiftY);
        ctx.restore();

        super.animate();
    }
}