code:
*.idea/
package-lock.json
package.json
node_modules/*
jsconfig.json
.vscode/*

repo_name: Tomajc/torture

path: .gitignore

license: mit


code:
# Torture Chamber

18+ browser game

As of version 3.8.0, no compilation is needed, the game can be run directly from the source

### UP TO VERSION 3.7.1 - Generating eye and brow images

To get the code in this repository running, one must generate eye and brow images first for every character. This requires [Node.JS](https://nodejs.org).

Install the dependencies (possibly into the main folder):

```
npm install canvas vector2d canvas-5-polyfill
```

Edit *utility/browgen.js* and *utility/eyegen.js* files. Find the line starting with `var confs = ...` and add each character into the list, for example:

`var confs = [HonokaConfig, KotoriConfig, UmiConfig]`

Then run these files via node. This might take a while:

```
node utility/browgen.js
node utility/eyegen.js
```


code:
* {
    margin: 0;
    padding: 0;
    border: 0;
    text-decoration: none;
    line-height: 1.5em;
    word-wrap: break-word;
}

h1, h2, h3, h4, h5, h6 {
    font-size: 1em;
}

q {
    font-style: italic;
}

a {
    color: #000000;
    border-width: 0 0 1px 0;
    border-style: dotted;
    font-weight: bold;
}

a:active {
    border: none;
}

a:hover {
    color: #C00000;
}

a[name] {
    border: none;
    color: inherit;
}

a.thumbnail {
    border: none;
}

a.thumbnail img {
    border-width: 1px;
    padding: 2px;
    border-color: #999999;
    border-style: solid;
}

a.thumbnail img:hover {
    background-color: #C00000;
}

#contents {
    max-width: 1600px;
    margin: 10px auto 200px auto;
    border-style: double;
    border-width: 0 0 5px 0;
    padding-bottom: 4px;
}

header h1 {
    font-size: 2em;
    border-style: double;
    border-width: 0 0 5px 0;
    padding-bottom: 4px;
    margin: 10px 0px;
}

section {
    clear: both;
    padding: 15px 0;
    line-height: 1.4em;
    font-size: 1.2em;
    text-align: justify;
}

section h1 {
    border-style: dashed;
    border-width: 0 0 1px 0;
    font-size: 1.4em;
    font-weight: bold;
    margin: 4px 0;
    text-indent : 18px;
    background-image: url('h1.png');
    background-repeat: no-repeat;
    background-position: 0% 50%;
}

section h2 {
    font-weight: bold;
    padding-top: 20px;
    font-size: 1.2em;
}

section h1+h2 {
    padding-top:0;
}

section p, section pre, section h2, section ul, section h3 {
    margin: 0 20px;
}

section p {
    margin: 10px 20px;
}

section h2+p {
    margin: 0 20px 10px 20px;
}

section h3 {
    text-decoration: underline;
    font-weight: bold;
}

section ul {
    margin-left: 40px;
}

section img {
    display: block;
    margin: 0 auto;
    border: solid;
    border-width: 1px;
}

section p {
    text-indent: 2em;
}

section p.continue {
    text-indent: 0em;
}

section p.image {
    margin: 1em 0;
    text-align: center;
    text-indent: none;
}

section span.code {
    border-style: solid;
    border-width: 0 0 1px 0;
}

section .path {
    font-style: italic;
    border-style: dotted;
    border-width: 0 0 1px 0;
}

section pre.code {
    font-size: 0.7em;
    font-style: monospace;
    border-style: solid;
    border-width: 1px;
}

footer {
    border-style: solid;
    border-width: 1px 0 0 0;
    text-align: center;
    padding: 4px 0;
}

table {
    border-collapse: collapse;
    width: 400px;
}

th, td {
    padding: 8px;
    text-align: left;
    border-bottom: 1px solid #ddd;
}

.alert {
    color: #C00000;
    font-weight: bold;
}


code:
class MyCharacter extends Character {
    static Resources = {
        images : [
            // Image files should be listed here
        ],
    };

    static creator = "Anonymous";
    static fullName = "My Character";
    static costumes = [
        ["nude", "Nude"],
        // Costumes should be listed here
    ];
    static options = [
        // Misc. options should be listed here
    ];

    constructor(params, rm, style, pose) {
        super(params, rm, style, pose);
        let def = {};

        // Custom definitions should be added here

        this.init(def);
    }
}


code:
let MyCollection = { Resources: {
    images: [
        // Add your image files here
    ],
    sounds: [
        // Add your sound files here
    ]
}};


code:
class MyCollectionClass {
    static Resources = {
        images: [
            // Add your image files here
        ],
        sounds: [
            // Add your sound files here
        ]
    };
}


code:
class MyMap extends TMap {
    static Resources = {
        images: [
            // Image files should be listed here
        ],
        sounds: [],
    };

    static creator = "Anonymous";
    static fullName = "My map";
    static characterCount = 1;

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

    init() {
        super.init();

        // Initialize map here
    }

    getPoseInfo() {
        return [{
            // Set your pose(s) here

            headXPos: I2W(1000),
            headYPos: I2W(100),

            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:
<!DOCTYPE html>

        <meta charset="utf-8">
        <title>Torture Chamber</title>
        <link rel="stylesheet" type="text/css" href="../common/default.css">
        <link rel="icon" type="image/x-icon" href="../common/favicon.png">

License @ Torture Chamber

License

Torture Chamber

Copyright (C) 2013 granony, 2021 zmod

Except for some elements listed below, this software is released under the MIT License
                (http://opensource.org/licenses/mit-license.php)

Used libraries:

Box2dWeb 2.1.a.3 licensed under the zlib license

jQuery 1.10.1 licensed under the MIT license

htm 3.1.0 (Hyperscript Tagged Markup) licensed under the Apache License 2.0

Preact 10.5.14 licensed under the MIT license

workerpool 6.2.1 licensed under the Apache License 2.0

Proton 5.4.5 (https://drawcall.github.io/Proton/) licensed under the MIT license

prism 1.29.0 (only for documentation) licensed under the MIT license

Used assets:

Creative Commons Attribution 3.0

Dish & Knife1.wav by gelo_papas
                    (
                    http://www.freesound.org/people/gelo_papas/sounds/65482/)

Jab Sound by Mike Koenig
                    (
                    http://soundbible.com/995-Jab.html)

Real Messy Fart Sound by Mike Koenig
                    (
                    http://soundbible.com/399-Real-Messy-Fart.html)

Spark 01.wav by chipfork
                    (
                    http://www.freesound.org/people/chipfork/sounds/52597/)

Spit Splat Sound by Mike Koenig
                    (
                    http://soundbible.com/1733-Spit-Splat.html)

Water Drop Low Sound by Mike Koenig
                    (
                    http://soundbible.com/1139-Water-Drop-Low.html)

Whip Crack Sound by snottyboi
                    (
                    http://soundbible.com/1375-Whip-Crack.html)

Scissors sound by dersuperanton
                    (
                    https://freesound.org/people/dersuperanton/sounds/433650/)

Drill sound by GowlerMusic
                    (
                    https://freesound.org/people/GowlerMusic/sounds/262265/)

Lighter sound by SomeoneCool15
                    (
                    https://freesound.org/people/SomeoneCool15/sounds/423777/)

SIL license

Chomsky font (https://www.dafont.com/chomsky.font)

Google material symbols (https://fonts.google.com/icons)

            Index


code:
<!DOCTYPE html>

        <meta charset="utf-8">
        <title>Torture Chamber</title>
        <link rel="stylesheet" type="text/css" href="../common/default.css">
        <link rel="icon" type="image/x-icon" href="../common/favicon.png">

Player manual

Playing the game is straightforward. Select the maps and the characters, and you can begin using the tools.

Troubleshooting

If you run into problems (e.g. the game freezes) open the developer console with key F12, then navigate to the console tab. You'll probably see some error messages in red. If you send those alongside what you did when the error occured to the developer(s), the bug might get fixed.

Taking screenshots

You can take screenshots by pressing the s button or the camera icon. This however, probably won't work and you'll get an error about tainted canvases in the console. This is caused by a policy and is not something we can fix easily on our side. The possible fixes are:

Run chrome with the flag --allow-file-access-from-files

Run the game on a local server. See this guide (https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server) for instructions, but running a server will likely result in longer loading times.

Run the game on Firefox

Take screenshots by right-clicking + Copy Image

Changing resolution

Open game/Config.js and edit TEXTURE_SCALE. The game supports 16:9 aspect ratios, and the default resolution is 1920x1080, when TEXTURE_SCALE is 0.5.

Resolution	TEXTURE_SCALE

3840x2160	1

1920x1080	0.5

1600x900	0.4167

1366x768	0.3557

1280x720	0.3333

960x540	0.25

640x360	0.1667

                Index


code:
<!DOCTYPE html>

        <meta charset="utf-8">
        <title>Torture Chamber</title>
        <link rel="stylesheet" type="text/css" href="../common/default.css">
        <link rel="icon" type="image/x-icon" href="../common/favicon.png">
        <link href="prism-1.29.0.min.css" rel="stylesheet" />

Character mods

Creating an example character

To create a new character, (e.g. MyCharacter),

Create a folder named MyCharacter in game/mods/characters

Make a new file called MyCharacter.js inside the newly created folder.

Add MyCharacter to the list of loaded characters in game/CharacterIDs.js

Character names must follow the mod naming rules.

A minimal working character example looks like this:

class MyCharacter extends Character {
    static Resources = {
        images : [
            // Image files should be listed here
        ],
    };

    static creator = "Anonymous";
    static fullName = "My Character";
    static costumes = [
        ["nude", "Nude"],
        // Costumes should be listed here
    ];
    static options = [
        // Misc. options should be listed here
    ];

    constructor(params, rm, style, pose) {
        super(params, rm, style, pose);
        let def = {};

        // Custom definitions should be added here

        this.init(def);
    }
}

This should result in a bald girl with questionable eye colors. The full name of the character and the creator has no strict restrictions on naming, you can use any unicode letter, space, emoji, etc.

Adding images

Custom images should be put in the MyCharacter/images folder and should be listed in Resources/images enclosed in quotation marks. As long as the file is a .png file, file extension does not have to be specified. For example hair_front.png should be listed as:</trong>

static Resources = {
    images : [
        "hair_front",
    ], ...

This image can be accessed later in character functions by:

let frontHairImage = this.images["hair_front"];

Overriding definitions

Character creation is largely done by using a large dictionary, which has info about body part images, body part sizes and joint positions. You can find their default values in game/system/scripts/DefaultDefs.js. If you want to overwrite a specific value, add it to the def dictionary in the constructor part (both syntaxes are OK):

...
    let def = {};
    def["overwritten_property"] = 123;

    this.init(def);
}

...
    let def = {};
    def.overwritten_property = 123;

    this.init(def);
}

this.init(def); should always be called at the very end of the contstructor.

DefaultDefs.js' properties can be divided into four categories:

Character images

Character images specify the looks of the body parts, and some character effects. You can overwrite them with your loaded images:

def.frontHairImage = this.images["hair_front"];

The main categories of character images are:

Hair images: (frontHairImage, sideHairImage, backHairImage), transparent by default. frontHairImage is rendered in front of the head and body, sideHairImage is rendered in front of the body but behind the head, and backHairImage is rendered behind the head and the body.

Hair (and every other facial) images should be drawn to the same sized canvas as the face and should match when overlaid on the top of each other without moving them around. This is not something that can be accomplished with long hair, as the default head image is 800x800 and you might run out of space when drawing longer hair. Back hair can be solved by simply extending the hair image downwards. For front hair, you can also do the same extension, but you also have to set a specific hacky flag headCanvasYSize to make it work. This increases the canvas size in the vertical dimension, but it has no X pair.

def.headCanvasYSize = I2W(1000); // If your frontHairImage is 800x1000

Blush image: blush is forced on the face by certain tools, and when the character's pleasure hits a threshold.

Sweat images: sweat is forced on the face by certain tools and when the character is below a specified HP. The two images specify stronger effects in increasing order.

Shadow images: shadow is forced on the face by certain tools and when the character is below a specified HP. These two images can coexist on the face, as they target different areas.

Tears images: tears are forced on the face by certain tools and when the character is below a specified HP. The three images specify stronger effects in increasing order.

Glasses image: glasses are transparent by default.

Mouth images: each mouth state/emotion has its own corresponding image. Tools can force specific mouths.

Nose images: (nose, nose_bloody) nose can enter a bloody state when destroyed.

Bone, Meat, Skin and Contour images: each major body part (head, breast, belly, waist, lbutt, rbutt, lleg, rleg, lfoot, rfoot, luparm, ruparm, lloarm, rloarm, lhand, rhand) is made of (at least) four layers: bone, meat, skin and contour.

Major body parts shown

To make effect drawing nicer, lineart (contour) is separated from skin and its shading. If you make the skin layer smaller (for smaller legs for example) the bone and meat layer behind will stick out causing ugly graphical effects, so it's recommended to handle every layer at the same time for a given bodypart.

Torso body parts (neck, breast, belly, waist) are recommended to be drawn on a single image and be split later by hand or by program. utility/bodysplit.py is a such program, but may be hard to use, since it's not user friendly.

Organs images: for neck, breast, belly and waist, there is a fifth organ layer behind the meat and above the bone layer, which should be handled the same way as the other layers.

Body part sizes

Regular major body parts (everything excluding head) are rectangular shapes. They are specified only by two parameters: width and height, these are used to create collision entities:

def.luparmWidth = I2W(123);
def.luparmHeight = I2W(456);

Body parts sould always be drawn centered on their image. The image below shows this visually; body parts span horizontally from (imageWith - bodypartWidth) / 2 to (imageWith + bodypartWidth) / 2 and vertically from (imageHeight - bodypartHeight) / 2 to (imageHeight + bodypartHeight) / 2

Body part sizes related to image sizes

The actual (0, 0) coordinates ingame are at the top middle point of the body part. This will be useful when poisitioning joints. You should also note that the rendered area is not restricted to the collision box, it can extend, but only horizontally:

The magenta area shows the rendered part

The head is a special body part as it has a hexagonal shape. headWidth, headTop, headControlPoint1, headControlPoint2, and headBottom control the actual positions of the control points. These can be a bit tricky to set, so you should experiment which works best for your head shape. The full head canvas is drawn everytime when rendered, so those parameters only affect collision.

Joint positions

Body parts are connected by joints. Revolute joints in the physics engine allow relative rotation of the body parts within a certain limit. Limits are given in DefaultDefs.js, but they should NOT be overwritten.

Suppose you have two bodies connected with a joint as seen below:

Two bodies connected with a joint

You have to provide the relative location of the joint with respect to the origins of the connected body parts (and you also have to take rotation into account). Each vector has two component (x any y), and if the joint is positioned leftwards from the origin, its x coordinate will be negative.

Two bodies connected with a joint

def.breastLuparmJointXPos = I2W(12); // Joint position relative to the breast's origin
def.breastLuparmJointYPos = I2W(34);
def.luparmBreastJointXPos = I2W(56); // Joint position relative to the luparm's origin
def.luparmBreastJointYPos = I2W(78);

You can test this positioning quickly using the character poser tool., just overwrite const CHARACTER = "..."; in the javascript file with the name of your character.

Face shadow color

Face shadow is an imprint of the front hair on the skin of the head, shifted slightly downwards. You can specify its color by:

def.faceShadowColor = "#A000A0"; // Creates a dark magenta shadow

Rendered character face shadow

Initial mouth and eyes

You can specify a character's initial emotions, when she has not taken much damage yet and is not affected by pleasure. Initial eyes are not specific shapes, but rather morphs on existing eye shapes. The eyes can have three different initial states: neutral, angry and sad:

this.initialEyes = "angry"; // Or neutral or sad

Any existing mouth in DefaultDefs.js can be set as initial state, but you have to strip the mouth_ prefix. For example to use mouth_happy_medium, type:

this.initialMouth = "happy_medium";

You can also define your own custom mouths as initial state:

static Resources = {
    images: [
        ..., "mouth_custom",
    ],

    [...]

def.mouth_custom = this.images["mouth_custom"];
this.initialMouth = "custom";

Eyebrows

Eyebrows are rendered dynamically using given colors and vector shapes. To preview quickly any changes made to facial features, open face editor and set const CHARACTER = "..."; to the name of your character. To clear any morphs, also set this.initialEyes to neutral.

Eyebrow position, rotation and scale can be controlled as seen below:

this.browRenderer.globalTranslateX += I2W(25); // Move eyebrows outwards
this.browRenderer.globalTranslateX -= I2W(25); // Move eyebrows inwards
this.browRenderer.globalTranslateY += I2W(25); // Move eyebrows downwards
this.browRenderer.globalTranslateY -= I2W(25); // Move eyebrows upwards

this.browRenderer.globalRotate = D2R(15);
this.browRenderer.globalScale = I2W(1.4);

Eyebrows have a color, outline and an outline color. You can set them by:

this.browRenderer.innerColor = "#1234AF";
this.browRenderer.outlineColor = "#111111";
this.browRenderer.outlineWidth = 2; // You can I2W() this if you want, but it's not necessary

Eyebrow shape is defined by an SVG path, which can be edited using external SVG editing software such as Inkscape. Path data should be enclosed in quotation marks and inside of the function parseSvgPathData(). In an external editor you can also translate, rotate and scale the path, but for fine tuning I found setting global variables more comfortable, as you don't have to paste path data back and forth.

this.browRenderer.baseShape = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0"); // Simple square shaped eyebrows

Eyebrows can be morphed to be flipped upside down. The target shape can be defined in a similar way as the base shape:

this.browRenderer.morphFlip = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");

Intermediate states are calculated by using the weighted average of each vertex position, so there are a few restraints on this morph:

Morphs should have the same number of vertices as the base shape.

Vertices should be listed in the same order. Since the eyebrow is usually a closed shape, this might not always be trivial. Incorrect shaping will cause twisted and botched intermediate states:

Ordering is important with SVG paths

Morphs should have the same type of vertices as the base shape at the same position, so you have to be careful when mixing line segments and cubic Bézier segments.

Eye rendering

Eye rendering is similar to eyebrow rendering, but more complicated. Eyes are rendered dynamically using given colors and vector shapes. To preview quickly any changes made to facial features, open face editor and set const CHARACTER = "..."; to the name of your character. To clear any morphs, also set this.initialEyes to neutral.

Eye position, rotation and scale can be controlled as seen below:

this.eyeRenderer.globalTranslateX += I2W(25); // Move eyes outwards
this.eyeRenderer.globalTranslateX -= I2W(25); // Move eyes inwards
this.eyeRenderer.globalTranslateY += I2W(25); // Move eyes downwards
this.eyeRenderer.globalTranslateY -= I2W(25); // Move eyebs upwards

this.eyeRenderer.globalRotate = D2R(15);
this.eyeRenderer.globalScale = I2W(1.4);

Eyes can be divided into two parts: general eye shape and iris. The general eye shape is made of the white part of the eye and the eye outline, both have their specific color and shape. Eye outline is rendered above the white part, so you can make an eyeWhiteShape that is overlapping the outline. For more about general SVG paths and shapes, check the eyebrow section.

this.eyeRenderer.outlineShape = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0"); // Eye outline shape
this.eyeRenderer.outlineColor = "#1234AF"; // Eye outline color
this.eyeRenderer.eyeWhiteShape = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0"); // Eye white shape
this.eyeRenderer.eyeWhiteColor = "#9876BE"; // Eye white color

When the eye is partially closed a morph is applied to both the outline and eyeWhite part. If the eye is fully closed, the outline should completely overlap the eyeWhite part. Check the eyebrow section about morphs in general.

this.eyeRenderer.outlineMorphClosed = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");
this.eyeRenderer.eyeWhiteMorphClosed = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");

There are two more separate parts around the eye as part of the general outline: one slight arc above the eye and one below/at the bottom of the eye. Their color is controlled by outlineColor, but their shape can be set independently. The lower shape also has its own morph when the eye is closed:

this.eyeRenderer.outlineUpperArc = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");
this.eyeRenderer.outlineDownArc = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");
this.eyeRenderer.outlineDownArcClosed = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0"); // Bottom arc morph

These shapes (and any shape) can also be transformed independently from the gobal transforms:

this.eyeRenderer.outlineDownArc.translate(3, 4); // Move 3 pixels outwards and 4 pixels downwards, do NOT call I2W on these
this.eyeRenderer.outlineDownArc.rotate(D2R(15));
this.eyeRenderer.outlineDownArc.scale(3); // Do not call I2W on any scaling except the global one

Eye shapes summary

The iris is made up of a few ellipses and other shapes. The colored iris has four parts: a main ellipse, a smaller ellipse shifted downwards, an arc-like shape and the pupil. You can control the colors and the shape of the arc by:

this.eyeRenderer.irisColorOuter = "#1234AF";
this.eyeRenderer.irisColorInner = "#2345BF";
this.eyeRenderer.irisColorArc = "#3456CF";
this.eyeRenderer.irisColorPupil = "111111";
this.eyeRenderer.irisArcShape = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");

You can set the iris' position, rotation and size by tweaking the following variables:

this.eyeRenderer.defaultIrisPosX += I2W(4); // Move iris outwards
this.eyeRenderer.defaultIrisPosY += I2W(5); // Move iris downwards
this.eyeRenderer.defaultIrisRotation += D2R(15);
this.eyeRenderer.defaultIrisSizeX = 12; // Do not call I2W on this
this.eyeRenderer.defaultIrisSizeY = 15; // Do not call I2W on this

The iris has a black-ish border, whose width stays constant, even when the iris is scaled down during certain emotions. You can control its color and constant width by:

this.eyeRenderer.irisColorBlack = "#012345";
this.eyeRenderer.irisColorBlackOffset = I2W(5);

You can set the scale of the inner ellipse and the pupil relative to the main iris size and also the ratio by which the inner ellipse is shifted vertically:

this.eyeRenderer.irisInnerScale = 0.5;
this.eyeRenderer.irisPupilScale = 0.25;
this.eyeRenderer.irisInnerOffset = 0.5; // Should be between 0 and 1

When a character's pleasure levels hit a certain threshold, a heart shape will appear on the top of the pupil. You can control its shape and color by:

this.eyeRenderer.irisHeartShape = parseSvgPathData("M 0 0 H 10 V 10 H 0 L 0 0");
this.eyeRenderer.irisColorHeart = "#985421";

The eye outline casts a shadow to the iris and eyeWhite. You can control its offset and opacity by:

this.eyeRenderer.eyeShadowOffsetX = I2W(3);
this.eyeRenderer.eyeShadowOffsetX = I2W(4);
this.eyeRenderer.eyeShadowAlpha = 0.5; // 0: transparent, 1: opaque

Three specular blobs are shown on the eye in the beginning, which are lost one by one as the character receives damage. To edit the color (global across every 'blob') and other properties, type:

this.eyeRenderer.specularColor = "#124578";
// Editing the first one
this.eyeRenderer.speculars[0].x = I2W(8); // Position relative to the center of the eye
this.eyeRenderer.speculars[0].y = I2W(1);
this.eyeRenderer.speculars[0].sx = 0.4; // Size in the x and y directions
this.eyeRenderer.speculars[0].sy = 0.3;
this.eyeRenderer.speculars[0].r = D2R(60); // Rotation

Body part extras

You may have noticed that certain bodyparts have difficulties hiding certain lines in our new character, which makes a somewhat ugly effect:

The arm line around the armpits should be hidden

One way to solve this is to use extras. Bodyparts marked with extra will be rendered twice, the second time with a subtractive mask. The subtractive mask should be drawn with a white color and exported alone. To add this mask to the actual bodyparts, you have to override the initExtraParts() function.

Applied extra to breast

initExtraParts() {
    this.breast.setExtra(this.def["breastExtraImage"]);
    this.luparm.setExtra(this.def["luparmExtraImage"]);
    this.ruparm.setExtra(this.def["ruparmExtraImage"]);
    this.extraParts = [this.luparm, this.ruparm, this.breast]; // This is the order in which they will be rendered
}

Costumes and clothes

A costume is a selectable outfit in the menu for a character. Each costume is made of multiple clothes, where each cloth belongs to a body part, and each body part is capable of holding up to 8 clothes.

To define a costume, add an entry to the list costumes. Short name should be kept simple and is only used internally, while the long name will be displayed on the menu.

static costumes = [
    ["nude", "Nude"],
    ["shortname", "Costume long name"],
];

You have to initialize these outfits by overriding initClothes. this.costumeID stores the short name of your costume, so you can enclose the possible costumes in a switch case block. To add a single clothing to a bodypart, call addCloth() on it. The first parameter is the order, clothes with higher numbers will be rendered above lowers, the second parameter is the image.

initClothes() {
    switch (this.costumeID) {
        case "costume1":
            this.breast.addCloth(0, this.images["clothpiece1"]);
            this.waist.addCloth(1, this.images["clothpiece2"]);
            break;
        case "costume2":
            this.luparm.addCloth(5, this.images["clothpiece3"]);
            this.lfoot.addCloth(7, this.images["clothpiece4"]);
            break;
        case "costume3":
            this.rleg.addCloth(3, this.images["clothpiece5"]);
            this.head.addCloth(0, this.images["clothpiece6"]);
            break;
    }
}

You may have clothes that extend beyond the boundaries of your body part. This is not a problem in the horizontal case (see the image about the rendered area), but can be with vertical overstreching. To solve this, you have do decrease the top of the cloth and/or increase the bottom:

this.breast.addCloth(4, this.images["tooLongCloth"]);
this.breast.clothes[4].top -= I2W(15);
this.breast.clothes[4].bottom += I2W(20);

Character options

You can specify binary (true or false) options that are displayed on the menu and can alter character appearance. To define an option, add an entry to the list options. Short name should be kept simple and is only used internally, while the long name will be displayed on the menu.

static options = [
    ["shortname", "Option long name"],
];

Options are stored in this.options and can be accessed in the constructor easily by enclosing them in a condition:

if (this.options.shortname) {
    // Custom logic here
}

You can do basically anything with options, so I provided two examples below. One adds glasses to the character, while the other draws pubic hair over the waist contour image.

if (this.options.glasses) {
    def.glassesImage = this.images["glasses"];
}
if (this.options.pubes) {
    let pubesWaist = createCanvas(def.waistContImage);
    pubesWaist.getContext("2d").drawImage(this.images["pubes"], 0, 0);
    def.waistContImage = pubesWaist;
}

In the latest example it's helpful to understand the difference between images and canvases. Images are loaded from the disk and can not be drawn over, while canvases can. The function createCanvas() copies a given canvas or image to a new canvas, thus ensuring that its result will always be drawable over.

Specific regions

Character regions are 'hotspots' on a character, and tools can detect if they hit such a hotspot. Currently there are seven spots for each character: left eye, right eye, nose and mouth on the head, left nipple and right nipple on breast and pussy on waist. You can redefine a region by:

this.rnippleRegion = circleRegion(I2W(-100), I2W(250), I2W(45)); // x y r

The function circleRegion() accepts the regions x and y coordinates relative to the parent body part's origin and the radius of the circle.

Right nipple region

Preview

To add a character preview icon, create and add preview.png to the main folder of the character.

NOTE Don't add it to the folder MyCharacter/images, and you shouldn't add this entry to the list of images in Resources/images. This image is not processed by the game engine, only by the user interface.

                Index


code:
<!DOCTYPE html>

        <meta charset="utf-8">
        <title>Torture Chamber</title>
        <link rel="stylesheet" type="text/css" href="../common/default.css">
        <link rel="icon" type="image/x-icon" href="../common/favicon.png">
        <link href="prism-1.29.0.min.css" rel="stylesheet" />

Collection mods

Collections are specific types of mods, that are only used to store shared images, sounds and code across multiple characters/tools/maps.

If you have an image file (e.g. common body part) which you want to use for multiple characters, you can either copy it (which wastes space and RAM) or cross-reference from one character on an other, which creates needless dependencies. The collection mod type solves this problem.

Creating an example collection

To create a new collection, (e.g. MyCollection),

Create a folder named MyCollection in game/mods/collections

Make a new file called MyCollection.js inside the newly created folder.

Add MyCollection to the list of loaded collections in game/CollectionIDs.js

Collection names must follow the mod naming rules.

The only requirement of the content of the .js file is to have a way to access MyCollection.Resources. This can be done in two ways, use whichever you prefer

let MyCollection = { Resources: {
    images: [
        // Add your image files here
    ],
    sounds: [
        // Add your sound files here
    ]
}};

class MyCollection {
    static Resources = {
        images: [
            // Add your image files here
        ],
        sounds: [
            // Add your sound files here
        ]
    };
}

Images should be stored in MyCollection/images folder, while sounds in MyCollection/sounds

You can also define any custom function or class in the file if needed.

Resources in MyCollection should be accessed by other mods via:

let sharedImage = this.rm.images.collections.MyCollection["sharedImage"];
let sharedSound = this.rm.sounds.collections.MyCollection["sharedSound"];

                Index


code:
<!DOCTYPE html>

        <meta charset="utf-8">
        <title>Torture Chamber</title>
        <link rel="stylesheet" type="text/css" href="../common/default.css">
        <link rel="icon" type="image/x-icon" href="../common/favicon.png">
        <link href="prism-1.29.0.min.css" rel="stylesheet" />

Common modding tips

This section contains a few practices which are useful in all types of modding.

I2W

The game rescales all loaded images when the game is launched. This scaling ratio is controlled by TEXTURE_SCALE in game/Config.js, which also makes positioning everything on the maps dependent on this variable (unless you are working with Box2Ds coordinates). The function I2W(...) solves this issue, and you should wrap every position related variable into this macro.

For example if you have TEXTURE_SCALE set to 1, have a map with a width of 1800 and want the character's head in the center, instead of

let charaHeadPosX = 900;

Use this

let charaHeadPosX = I2W(900);

D2R

Unless noted otherwise, angles should be given in radians whenever some sort of rotation is set. To quickly convert degrees to radians use the macro D2R:

let bodyRotation = D2R(90); // 90 degrees

Developer console

If you make a typing mistake when modding and the game crashes or freezes, open the developer console with key F12, then navigate to the console tab. The error messages in red will indicate the line that caused the fault.

You can also access game variables realtime from the console. Type game.params to access the game's global state where you can see every character and tool present. You can navigate this tree view yourself or use more specfic queries. For example, to get the first character's hp, type

game.params.characters[0].hp

You can also force her orgasm similarly

game.params.characters[0].orgasm()

Developer console view

Mod naming rules

As mod names are used as Javascript variables, folder names, and file names, there are a few restrictions on what an acceptable mod name can be.

Variable names must only contain english letters, numbers, or the underscore (_) character.

The first character must be a letter. You can't use a number as the first character and the underscore should be avoided.

Mod names are case sensitive. MyMod and mymod refer to two different mods.

Mod names must not conflict with Javascript reserved words.

Debug mode

You can turn debug mode on by clicking the green bug icon on the menu, which allows you to see the collision boxes and joints of each body part. If you don't have this icon, since it is not visible on release versions, open game/Config.js and delete the word const before DEBUG.

                Index


code:
<!DOCTYPE html>

        <meta charset="utf-8">
        <title>Torture Chamber</title>
        <link rel="stylesheet" type="text/css" href="../common/default.css">
        <link rel="icon" type="image/x-icon" href="../common/favicon.png">
        <link href="prism-1.29.0.min.css" rel="stylesheet" />

Map mods

Creating an example map

To create a new map, (e.g. MyMap),

Create a folder named MyMap in game/mods/maps

Make a new file called MyMap.js inside the newly created folder.

Add MyMap to the list of loaded maps in game/MapIDs.js

Map names must follow the mod naming rules.

A minimal working map example looks like this:

class MyMap extends TMap {
    static Resources = {
        images: [
            // Image files should be listed here
        ],
        sounds: [],
    };

    static creator = "Anonymous";
    static fullName = "My map";
    static characterCount = 1;

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

    init() {
        super.init();

        // Initialize map here
    }

    getPoseInfo() {
        return [{
            // Set your pose(s) here

            headXPos: I2W(1050),
            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),
        }];
    }
}

Upon loading the map, you will probably see a black background, the character will fall down without any fixation, and the tools will leave their marks permanently.

Adding images

Custom images should be put in the MyMap/images folder and should be listed in Resources/images enclosed in quotation marks. As long as the file is a .png file, file extension does not have to be specified. For example foreground.png should be listed as:</trong>

static Resources = {
    images : [
        "foreground",
    ], ...

This image can be accessed later in map functions by:

let foregroundImage = this.images["foreground"];

Adding wall fixations

To prevent characters from falling down, we must fixate some of their body parts to the background wall, by calling addWallFixationItem():

init() {
    super.init();

    this.addWallFixationItem(
        this.characters[0].breast, // this.characters stores available characters in a list
        I2W(20), I2W(30), // x and y position of the fixation relative to the origin (see character modding guide)
    );
}

If you have multiple characters and want to fixate them to wall at multiple locations, you can wrap this piece of code in loops. Fixations by default are invisible, so you have to mask them with items in the foreground. Alternatively you can use nails for fixations, but you have to use global coordinates for them:

init() {
    super.init();

    let posx = I2W(20);
    let posy = I2W(30);
    let b = this.characters[0].breast.box2dBody;
    let bang = b.GetAngle();
    let posxWorld = b.GetPosition().x*SCALE + posx*Math.cos(-bang) + posy*Math.sin(-bang);
    let posyWorld = b.GetPosition().y*SCALE - posx*Math.sin(-bang) + posy*Math.cos(-bang);
    this.tools["Nail"].click(posxWorld, posyWorld, true);
}

Now the girl won't fall down

Pose info and character count

The number of characters available for a map is given in characterCount:

static characterCount = 5;

...

init() {
    console.log(this.characterCount); // 5
}

You must supply a pose for each character in getPoseInfo(), which function expects a list of dictionary items as return value. The angles define the general pose similarly to an FK system, except the root body part here is the head. You can edit poses using the poser tool. headXPos and headYPos are used define the character's position.

Borders

Rendering

Custom sizes and other parameters

To use a custom resolution, set width and height in the constructor. Your size of your background and foreground images should match with your given values.

constructor(params, rm) {
    super(params, rm);
    this.width = I2W(1234); // Use I2W() on these values
    this.height = I2W(2468);
}

You can also turn off shadow rendering (for outdoor maps for example) similarly:

constructor(params, rm) {
    super(params, rm);
    this.renderShadow = false;
}

Custom wall fixations

Preview

To add a map preview icon, create and add preview.png to the main folder of the map.

NOTE Don't add it to the folder MyMap/images, and you shouldn't add this entry to the list of images in Resources/images. This image is not processed by the game engine, only by the user interface.

                Index


code:
let characterIDs = [
    "Honoka",
    "Kotori",
    "Umi",
    "Rin",
    "Maki",
    "Hanayo",
    "Nico",
    "Eli",
    "Nozomi",
    "Tsubasa",
    "Anju",
    "Erena",
    "Chika",
    "You",
    "Riko",
    "Yoshiko",
    "Hanamaru",
    "Ruby",
    "Dia",
    "Kanan",
    "Mari",
    "Leah",
    "Sarah",
    "Yuu",
    "Kanata",
    "Setsuna",
    "Kasumi",
    "Ayumu",
    "Emma",
    "Ai",
];


code:
let collectionIDs = [
    "ZmodLLPack",
    "ZmodLLClothesPack",
];


code:
// Global settings

const DEBUG    = false;    // Flag for debugging
const VERSION  = "3.12.0";

// By how much the textures are scaled when loaded
// Change this value to alter resolution
const TEXTURE_SCALE = 0.5;

const WIDTH    = 1920 * TEXTURE_SCALE*2;      // Horizontal resolution
const HEIGHT   = 1080 * TEXTURE_SCALE*2;      // Vertical resolution
const SCALE    = 200 * TEXTURE_SCALE;      // Scale between b2World and screen coordinates (affects weights for example)
const TIMESTEP = 1000/30;  // Animation time interval milliseconds


code:
let mapIDs = [
    "DefaultMap",
    "BetaLab",
    "GammaLab",
    "DeltaLab",
    "DeadHills",
    "CircWheel",
];


code:
let toolIDs = [
    "Knife",
    "Whip",
    "Punch",
    "Staplegun",
    "Stungun",
    "Acid",
    "Nail", // don't delete (TODO better dependency handling)
    "Butcher",
    "Saw",
    "Enema",
    "Syringe",
    "Bat",
    "BBPistol",
    "Razor",
    "Revolver",
    "Scissors",
    "Drill",
    "Marker",
    "Crowbar",
    "Fireaxe",
    "Lighter",
    "Iron",
    "Cane",
    "Needle",
    "Vibrator",
    "Candle",
    "Cigar",
    "VibrEgg",
    "Crop",
    "Dildo",
    "Gag",
];


code:
#game-files {
    width: 0.1px;
    height: 0.1px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    z-index: -1;
}

input:enabled+.custom-file-input {
    box-shadow:inset 0px 1px 0px 0px #9acc85;
    background:linear-gradient(to bottom, #74ad5a 5%, #68a54b 100%);
    background-color:#74ad5a;
    border:1px solid #3b6e22;
    display:inline-block;
    cursor:pointer;
    color:#ffffff;
    font-family:Arial;
    font-size:15px;
    font-weight:bold;
    padding:6px 12px;
    text-decoration:none;
}

input:enabled+.custom-file-input:hover {
    background:linear-gradient(to bottom, #68a54b 5%, #74ad5a 100%);
    background-color:#68a54b;
}

input:enabled+.custom-file-input:active {
    position:relative;
    top:1px;
}

input:disabled+.custom-file-input {
    visibility: hidden;
    padding: 0px;
    line-height: 0px;
    display: none;
}


code:
.custom_checkbox .option_container {
    display: block;
    position: relative;
    margin-bottom: 12px;
    cursor: pointer;
    font-size: 24px;
    color: #fff;
}

.custom_checkbox .option_container input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
    height: 0;
    width: 0;
}

.custom_checkbox .option_container .option_checkmark {
    position: absolute;
    top: 0;
    right: 20px;
    height: 35px;
    width: 35px;
    background-color: #222;
    margin-right: 5px;
}

.custom_checkbox .option_container:hover input ~ .option_checkmark {
    background-color: #444;
}

.custom_checkbox .option_container input:checked ~ .option_checkmark {
    background-color: #444;
}

.custom_checkbox .option_checkmark:after {
    content: "";
    position: absolute;
    display: none;
}

.custom_checkbox .option_container input:checked ~ .option_checkmark:after {
    display: block;
}

.custom_checkbox .option_container .option_checkmark:after {
    left: 9px;
    top: 5px;
    width: 9px;
    height: 18px;
    border: solid var(--active-color);
    border-width: 0 5px 5px 0;
    -webkit-transform: rotate(45deg);
    -ms-transform: rotate(45deg);
    transform: rotate(45deg);
}

.custom_range input[type="range"] {
    -webkit-appearance: none;
    margin-right: 15px;
    width: 200px;
    height: 7px;
    background: rgba(255, 255, 255, 0.6);
    border-radius: 5px;
    background-image: linear-gradient(var(--active-color), var(--active-color));
    background-size: 70% 100%;
    background-repeat: no-repeat;
}

.custom_range input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    height: 30px;
    width: 15px;
    border-radius: 5px;
    border-color: #111;
    border-width: 2px;
    background: var(--active-color);
    cursor: ew-resize;
    box-shadow: 0 0 2px 0 #555;
}

.custom_range input[type="range"]::-moz-slider-thumb {
    -moz-appearance: none;
    height: 30px;
    width: 15px;
    border-radius: 5px;
    border-color: #111;
    border-width: 2px;
    background: var(--active-color);
    cursor: ew-resize;
    box-shadow: 0 0 2px 0 #555;
}

.custom_range input[type=range]::-webkit-slider-runnable-track {
    -webkit-appearance: none;
    box-shadow: none;
    border: none;
    background: transparent;
}

.custom_range input[type=range]::-moz-range-track {
    -moz-appearance: none;
    box-shadow: none;
    border: none;
    background: transparent;
}

.custom_range .range_number_container {
    position: absolute;
    right: 20px;
    top: 0px;
}

.custom_range {
    display: block;
    position: relative;
    margin-bottom: 12px;
    cursor: pointer;
    font-size: 24px;
    color: #fff;
}

.custom_range input[type="number"] {
    width: 50px;
    height: 30px;
    border: 1px solid #bbb;
    background-color: #444;
    color: #fff;
    font-size: 16px;
}

.custom_color {
    display: block;
    position: relative;
    margin-bottom: 12px;
    font-size: 24px;
    color: #fff;
}

.custom_color input[type="color"] {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    width: 40px;
    height: 40px;
    background-color: transparent;
    border: none;
    cursor: pointer;
    position: absolute;
    right: 20px;
}

.custom_color input[type="color"]::-webkit-color-swatch {
    border-width: 2px;
    border-color: #fff;
}

.custom_color input[type="color"]::-moz-color-swatch {
    border-width: 2px;
    border-color: #fff;
}

.custom_button {
    display: block;
    position: relative;
    width: 100%;
    display: flex;
    justify-content: flex-end;
}

.custom_button button {
    margin-left: auto;
    margin-right: 20px;
    min-width: 100px;
    height: 50px;
    border: 2px solid #111;
    background-image: linear-gradient(to top, #222, #444 50%);
    color: var(--active-color);
    text-align: center;
    font-size: 16px;
    cursor: pointer;
    font-weight: bold;
    border-radius: 5px;
}

.custom_button button:hover {
    color: var(--active-more);
}


code:
@font-face {
    font-family: "Chomsky";
    src: url("system/font/Chomsky.woff") format('woff');
}

body {
    margin: 0px;
    background-color: #000;
}

.noCursor {
    cursor: none;
}

.transparent {
    pointer-events: none;
    display: none;
}

.hidden {
    display: none;
}

#game {
    position: relative;
    padding: 0;
    margin: auto;
}

#game #menu_gui {
    position: absolute;
    width: 100%;
    height: 100%;
    margin: 0;
}

#game #canvas {
    position: absolute;
    margin: 0;
}

#ingame_gui .ingame_ui_hidden {
    box-sizing: border-box;
    position: absolute;
    right: 0;
    top: 100px;
    width: 60px;
    height: 60px;
    background-color: #0000007f;
    user-select: none;
}

#ingame_gui .ingame_ui_hidden img {
    cursor: pointer;
    width: 100%;
    height: 100%;
}

#ingame_gui .ingame_ui_right {
    box-sizing: border-box;
    position: absolute;
    right: 0;
    width: 480px;
    height: 100%;
    margin: 0;
    background-color: #0000007f;
    overflow-y: overlay;
    padding: 10px;
    font-family: sans;
    user-select: none;
}

#ingame_gui .ingame_ui_left {
    box-sizing: border-box;
    position: absolute;
    left: 0;
    width: 480px;
    height: 100%;
    margin: 0;
    background-color: #0000007f;
    overflow-y: overlay;
    padding: 10px;
    font-family: sans;
    user-select: none;
}

#ingame_gui .options_icons img {
    cursor: pointer;
    margin-left: 20px;
}

#ingame_gui h1 {
    box-sizing: border-box;
    display: block;
    width: 100%;
    color: #fff;
    background-color: #777;
    font-variant: small-caps;
    font-size: 36px;
    font-weight: bold;
    padding-left: 20px;
    padding-top: 5px;
    padding-bottom: 5px;
    margin-bottom: 0px;
    border-bottom: 3px solid #999;
    border-right: 3px solid #999;
}

#ingame_gui .character_list {
    box-sizing: border-box;
    margin-top: 0px;
    width: 100%;
}

#ingame_gui .single_character_full {
    display: flex;
    flex-direction: row;
    background-color: #444;
    margin-bottom: 7px;
    padding: 5px;
}

#ingame_gui .portrait {
    margin-right: 12px;
}

#ingame_gui .portrait .image_holder {
    position: relative;
    width: 100px;
    height: 100px;
    margin-bottom: 6px;
}

#ingame_gui .portrait img {
    position: relative;
    border-radius: 50%;
    width: inherit;
}

#ingame_gui .portrait img.dead {
    filter: grayscale(100%);
}

#ingame_gui .portrait .bleed {
    width: 35px;
    height: 35px;
    position: absolute;
    right: 0;
}

#ingame_gui .portrait .hidden {
    display: none;
}

#ingame_gui .portrait .pleasurebar_full {
    width: 100px;
    height: 7px;
    border-left: 2px solid transparent;
    border-right: 2px solid transparent;
}

#ingame_gui .portrait .pleasurebar {
    background-image: linear-gradient(to top, #2bd7e4 20%, #384bee 100%);
    height: 100%;
}

#ingame_gui .portrait .pleasurebar.orgasm {
    background-image: linear-gradient(to top, #befff1 20%, #2bd7e4 100%);
}

#ingame_gui .portrait .hpbar_full {
    width: 100px;
    background-image: linear-gradient(to top, #740000 20%, #410000 100%);
    height: 15px;
    border: 2px solid #000;
}

#ingame_gui .portrait .hpbar {
    background-image: linear-gradient(to top, #00dd00 20%, #005f00 100%);
    height: 100%;
}

#ingame_gui .name_buttons h2 {
    color: #fff;
    font-size: 24px;
    margin-bottom: 8px;
    margin-top: 10px;
}

#ingame_gui .name_buttons button {
    font-size: 20px;
    font-weight: bold;
    width: 30px;
    height: 30px;
}

/* disabled */
#ingame_gui .name_buttons button.state_0 {
    background-color: #888;
    color: #bbb;

}

/* on */
#ingame_gui .name_buttons button.state_1 {
    background-color: var(--active-color);
    color: #fff;
    cursor: pointer;
}

/* off */
#ingame_gui .name_buttons button.state_2 {
    background-color: #fff;
    color: var(--active-color);
    border: 1px solid var(--active-color);
    cursor: pointer;
}

#ingame_gui .autoskin {
    width: 40px;
    height: 40px;
    cursor: pointer;
}

#ingame_gui .tool_icons {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}

#ingame_gui .tool_icons .image_holder {
    box-sizing: border-box;
    width: 75px;
    height: 75px;
    background-color: #000;
}

#ingame_gui .tool_icons .image_holder:hover {
    background-color: #fff;
}

#ingame_gui .tool_icons img {
    box-sizing: border-box;
    width: inherit;
    height: inherit;
    border: 2px solid #fff;
}

#ingame_gui .tool_icons img.active {
    border: 4px solid #f00;
}

#ingame_gui .tool_custom_ui {
    background-color: #555;
    padding: 5px;
    margin-top: 10px;
}

#ingame_gui .credits {
    font-size: 18px;
    color: #fff;
    font-weight: bold;
}

#menu_gui {
    font-family: Chomsky;
}

#menu_gui .title_text {
    margin: auto;
    font-size: 80px;
    width: 100%;
    height: 10%;
    text-align: center;
    vertical-align: middle;
    line-height: 90px;
}

#menu_gui .map_select {
    display: flex;
    flex-flow: row wrap;
    width: 100%;
    height: 80%;
    overflow-y: scroll;
}

#menu_gui .map_select img {
    margin: 0 auto;
    max-width: 533px; /* TODO remove hardcoded */
    max-height: 300px;
    border: 5px transparent;
    box-sizing: border-box;
}

#menu_gui .map_select img.selected {
    border: 5px solid var(--active-color);
}

#menu_gui .character_list {
    display: flex;
    flex-flow: column;
    align-content: stretch;
    margin: auto;
    width: 100%;
    height: 80%;
    overflow-y: scroll;
}

#menu_gui .character_element {
    padding: 10px;
    display: flex;
    flex-flow: row;
    margin: auto;
    background-color: #0000007f;
    width: 90%;
    align-items: center;
    margin-bottom: 5px;
    margin-top: 0px;
}

#menu_gui .character_element .image_holder {
    box-sizing: border-box;
    border-radius: 50%;
    width: 100px;
    height: 100px;
    background-color: rgb(70, 70, 70);
}

#menu_gui .character_element img.chara_portrait {
    box-sizing: border-box;
    border-radius: 50%;
    width: inherit;
    border: 3px solid #ddd;
}

#menu_gui .chara_index {
    flex: 1;
    font-size: 60px;
    text-align: center;
    vertical-align: middle;
    color: #fff;
}

:root {
    --select-border: #222;
    --select-arrow: var(--active-color);

    --active-color: #c57000;
    --active-more: #b90000;
}

#menu_gui .chara_picker {
    flex: 5;

    padding: 20px;
}

#menu_gui .select_helper {
    border: 1px solid var(--select-border);
    border-radius: 0.25em;
    padding-right: 0.5em;
    font-size: 36px;
    cursor: pointer;
    line-height: 1.1;
    background-color: #444;
    background-image: linear-gradient(to top, #222, #444 33%);

    display: grid;
    grid-template-areas: "select";
    align-items: center;
}

#menu_gui select, #menu_gui .select_helper:after {
    grid-area: select;
}

#menu_gui .select_helper::after {
    content: "";
    width: 0.8em;
    height: 0.5em;
    background-color: var(--select-arrow);
    clip-path: polygon(100% 0%, 0 0%, 50% 100%);

    justify-self: end;
}

#menu_gui select {
    appearance: none;
    background-color: transparent;
    border: none;
    padding: 0.25em 0.5em;
    margin: 0;
    width: 100%;
    font-family: inherit;
    font-size: inherit;
    cursor: inherit;
    line-height: inherit;
    outline: none;
    color: #fff;
}

#menu_gui select > option {
    background: #444;
}

#menu_gui .chara_clothes {
    flex: 4;
    padding-left: 40px;
    padding-right: 20px;
}

#menu_gui .chara_clothes .select_helper {
    font-size: 30px;
}

#menu_gui .chara_options {
    flex: 4;
}

#menu_gui .option_container {
    display: block;
    position: relative;
    padding-left: 35px;
    margin-bottom: 12px;
    cursor: pointer;
    font-size: 28px;
    color: #fff;
}

#menu_gui .option_container input {
    position: absolute;
    opacity: 0;
    cursor: pointer;
    height: 0;
    width: 0;
}

#menu_gui .option_container .option_checkmark {
    position: absolute;
    top: 0;
    left: 0;
    height: 35px;
    width: 35px;
    background-color: #222;
    margin-right: 5px;
}

#menu_gui .option_container:hover input ~ .option_checkmark {
    background-color: #444;
}

#menu_gui .option_container input:checked ~ .option_checkmark {
    background-color: #444;
}

#menu_gui .option_checkmark:after {
    content: "";
    position: absolute;
    display: none;
}

#menu_gui .option_container input:checked ~ .option_checkmark:after {
    display: block;
}

#menu_gui .option_container .option_checkmark:after {
    left: 9px;
    top: 5px;
    width: 9px;
    height: 18px;
    border: solid var(--active-color);
    border-width: 0 5px 5px 0;
    -webkit-transform: rotate(45deg);
    -ms-transform: rotate(45deg);
    transform: rotate(45deg);
}

#menu_gui .button_group {
    display: flex;
    flex-wrap: row wrap;
    justify-content: space-evenly;
    height: 10%;
    padding-top: 20px;
    padding-bottom: 20px;
    box-sizing: border-box;
}

#menu_gui .button_group button {
    width: 400px;
    height: auto;
    font-size: 36px;
    font-family: Chomsky;
    color: var(--active-color);
    background-color: #444;
    background-image: linear-gradient(to top, #222, #444 40%);
    border: 5px solid #222;
    border-radius: 5px;
}

#menu_gui .button_group button:disabled {
    color: #999;
}

#menu_gui .button_group button:hover:not([disabled]) {
    color: var(--active-more);
    cursor: pointer;
}


code:
let ZmodLLClothesPack = {Resources: {
    images: [
        "breastUniformXL", "bellyUniformXL", "waistUniformXL", "waistSkirtXL",
        "luparmUniform", "ruparmUniform", "lloarmUniform", "rloarmUniform",
        "lbuttSocksXL", "rbuttSocksXL", "llegSocks", "rlegSocks", "lfootShoes", "rfootShoes",

        "waistTigerS", "breastTigerS", "waistTigerM", "breastTigerM",
        "waistTigerL", "breastTigerL", "waistTigerXL", "breastTigerXL",
        "lbuttTigerS", "rbuttTigerS", "lbuttTiger", "rbuttTiger",
        "llegTiger", "rlegTiger", "lfootTiger", "rfootTiger",
        "luparmTiger", "ruparmTiger", "lloarmTiger", "rloarmTiger", "lhandTiger", "rhandTiger",
        "tigerEars",

        "breastGymRS", "breastGymGS", "breastGymBS",
        "breastGymRM", "breastGymGM", "breastGymBM",
        "breastGymRL", "breastGymGL", "breastGymBL",
        "bellyGymS", "bellyGymM", "bellyGymL",
        "waistGymS", "waistGymM", "waistGymL",
        "waistGymBottomRS", "waistGymBottomGS", "waistGymBottomBS",
        "waistGymBottomRM", "waistGymBottomGM", "waistGymBottomBM",
        "waistGymBottomRL", "waistGymBottomGL", "waistGymBottomBL",
        "luparmGymR", "luparmGymG", "luparmGymB",
        "ruparmGymR", "ruparmGymG", "ruparmGymB",
        "lbuttGymRS", "lbuttGymGS", "lbuttGymBS", "rbuttGymRS", "rbuttGymGS", "rbuttGymBS",
        "lbuttGymRM", "lbuttGymGM", "lbuttGymBM", "rbuttGymRM", "rbuttGymGM", "rbuttGymBM",
        "lbuttGymRL", "lbuttGymGL", "lbuttGymBL", "rbuttGymRL", "rbuttGymGL", "rbuttGymBL",
        "llegGym", "rlegGym", "lfootGym", "rfootGym",

        "breastChristmasS", "waistChristmasS", "breastChristmasCollarS",
        "breastChristmasM", "waistChristmasM", "breastChristmasCollarM",
        "breastChristmasL", "waistChristmasL", "breastChristmasCollarL",
        "breastChristmasXL", "waistChristmasXL", "breastChristmasCollarXL",
        "headChristmas",
        "luparmChristmasMXL", "ruparmChristmasMXL", "lhandChristmasMLXL", "rhandChristmasMLXL",
        "lloarmChristmasS", "rloarmChristmasS", "lloarmChristmasM", "rloarmChristmasM",
        "lloarmChristmasL", "rloarmChristmasL", "lloarmChristmasXL", "rloarmChristmasXL",
        "lbuttChristmasS", "rbuttChristmasS", "lbuttChristmasM", "rbuttChristmasM",
        "lbuttChristmasLXL", "rbuttChristmasLXL",
        "llegChristmasBoot", "rlegChristmasBoot", "lfootChristmasBoot", "rfootChristmasBoot",
        "llegChristmasM", "rlegChristmasM", "lfootChristmasM", "rfootChristmasM",
        "llegChristmasLXL", "rlegChristmasLXL", "lfootChristmasLXL", "rfootChristmasLXL",
    ],
}};

function ZmodLLClothesSetup(costumeID, character, rm) {
    let images = rm.images.collections.ZmodLLClothesPack;
    let size = character.characterSize;
    switch (costumeID) {
        case "uniform":
            character.waist.addCloth(2, images["waistSkirt" + size]);
            character.waist.clothes[2].bottom += I2W(90);
            character.waist.addCloth(3, images["waistUniform" + size]);
            character.belly.addCloth(3, images["bellyUniform" + size]);
            character.breast.addCloth(3, images["breastUniform" + size]);
            character.breast.clothes[3].top = I2W(-15);
            character.luparm.addCloth(3, images["luparmUniform"]);
            character.ruparm.addCloth(3, images["ruparmUniform"]);
            character.lloarm.addCloth(3, images["lloarmUniform"]);
            character.rloarm.addCloth(3, images["rloarmUniform"]);
            character.lbutt.addCloth(0, images["lbuttSocks" + size]);
            character.rbutt.addCloth(0, images["rbuttSocks" + size]);
            character.lleg.addCloth(0, images["llegSocks"]);
            character.rleg.addCloth(0, images["rlegSocks"]);
            character.lfoot.addCloth(1, images["lfootShoes"]);
            character.rfoot.addCloth(1, images["rfootShoes"]);
            break;
        case "tiger":
            character.waist.addCloth(0, images["waistTiger" + size]);
            character.breast.addCloth(1, images["breastTiger" + size]);
            character.lbutt.addCloth(2, images["lbuttTiger" + (size == "S" ? "S" : "")]); // Only S is different
            character.rbutt.addCloth(3, images["rbuttTiger" + (size == "S" ? "S" : "")]);
            character.lleg.addCloth(2, images["llegTiger"]);
            character.rleg.addCloth(3, images["rlegTiger"]);
            character.lfoot.addCloth(2, images["lfootTiger"]);
            character.rfoot.addCloth(3, images["rfootTiger"]);
            character.luparm.addCloth(4, images["luparmTiger"]);
            character.ruparm.addCloth(5, images["ruparmTiger"]);
            character.lloarm.addCloth(4, images["lloarmTiger"]);
            character.rloarm.addCloth(5, images["rloarmTiger"]);
            character.lhand.addCloth(4, images["lhandTiger"]);
            character.rhand.addCloth(5, images["rhandTiger"]);
            character.head.addCloth(6, images["tigerEars"]);
            break;
        case "gym_r":
        case "gym_g":
        case "gym_b":
            let color = costumeID.slice(-1).toUpperCase();
            character.waist.addCloth(1, images["waistGym" + size]);
            character.breast.addCloth(1, images["breastGym" + color + size]);
            character.belly.addCloth(1, images["bellyGym" + size]);
            character.luparm.addCloth(1, images["luparmGym" + color]);
            character.ruparm.addCloth(1, images["ruparmGym" + color]);
            character.waist.addCloth(0, images["waistGymBottom" + color + size]);
            character.waist.clothes[0].bottom += I2W(5);
            character.lbutt.addCloth(0, images["lbuttGym" + color + size]);
            character.rbutt.addCloth(0, images["rbuttGym" + color + size]);
            character.lleg.addCloth(2, images["llegGym"]);
            character.rleg.addCloth(2, images["rlegGym"]);
            character.lfoot.addCloth(2, images["lfootGym"]);
            character.rfoot.addCloth(2, images["rfootGym"]);
            break;
        case "xmas":
            character.breast.addCloth(0, images["breastChristmas" + size]);
            if (size == "XL") character.breast.clothes[0].bottom += I2W(70);
            character.waist.addCloth(1, images["waistChristmas" + size]);
            character.waist.clothes[1].bottom += I2W(5);
            character.breast.addCloth(2, images["breastChristmasCollar" + size]);
            character.head.addCloth(3, images["headChristmas"]);
            character.head.clothes[3].top -= I2W(100);
            if (size == "M" || size == "XL") {
                character.luparm.addCloth(4, images["luparmChristmasMXL"]);
                character.ruparm.addCloth(4, images["ruparmChristmasMXL"]);
            }
            character.lloarm.addCloth(4, images["lloarmChristmas" + size]);
            character.rloarm.addCloth(4, images["rloarmChristmas" + size]);
            if (size == "M" || size == "L" || size == "XL") {
                character.lhand.addCloth(4, images["lhandChristmasMLXL"]);
                character.rhand.addCloth(4, images["rhandChristmasMLXL"]);
            }
            if (size == "S") {
                character.lbutt.addCloth(1, images["lbuttChristmasS"]);
                character.rbutt.addCloth(1, images["rbuttChristmasS"]);
            } else if (size == "M") {
                character.lbutt.addCloth(5, images["lbuttChristmasM"]);
                character.rbutt.addCloth(5, images["rbuttChristmasM"]);
                character.lleg.addCloth(5, images["llegChristmasM"]);
                character.rleg.addCloth(5, images["rlegChristmasM"]);
                character.lfoot.addCloth(5, images["lfootChristmasM"]);
                character.rfoot.addCloth(5, images["rfootChristmasM"]);
            } else {
                character.lbutt.addCloth(5, images["lbuttChristmasLXL"]);
                character.rbutt.addCloth(5, images["rbuttChristmasLXL"]);
                character.lleg.addCloth(5, images["llegChristmasLXL"]);
                character.rleg.addCloth(5, images["rlegChristmasLXL"]);
                character.lfoot.addCloth(5, images["lfootChristmasLXL"]);
                character.rfoot.addCloth(5, images["rfootChristmasLXL"]);
            }
            if (size == "S" || size == "L") {
                character.lleg.addCloth(6, images["llegChristmasBoot"]);
                character.rleg.addCloth(6, images["rlegChristmasBoot"]);
                character.lfoot.addCloth(6, images["lfootChristmasBoot"]);
                character.rfoot.addCloth(6, images["rfootChristmasBoot"]);
            }
            break;
    }
}


code:
let ZmodLLPack = { Resources: {
    images: [
        "mouth_close_smile", "mouth_close_neutral", "mouth_close_sad", "mouth_close_squiggly", "mouth_close_pouting",
        "mouth_open_small", "mouth_open_medium", "mouth_open_large", "mouth_open_squiggly",
        "mouth_happy_small", "mouth_happy_medium", "mouth_happy_large",
        "mouth_small_o", "mouth_small_triangle", "mouth_orgasm_1", "mouth_orgasm_2",
        "mouth_dead",
        "mouth_clench_weak", "mouth_clench_medium", "mouth_clench_strong", "mouth_clench_squiggly",
        "sweat1", "sweat2", "shadow1", "shadow2", "blush",
        "tears1", "tears2", "tears3", "nose", "nose_bloody",

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

        "luparmExtra", "ruparmExtra", "breastExtraS", "breastExtraM", "breastExtraL", "breastExtraXL",

        "bellySkinS", "bellyContS", "bellyMeatS", "bellyOrgansS", "bellyBoneS",
        "breastSkinS", "breastContS", "breastMeatS", "breastOrgansS", "breastBoneS",
        "neckSkinS", "neckContS", "neckMeatS", "neckOrgansS", "neckBoneS",
        "waistSkinS", "waistContS", "waistMeatS", "waistOrgansS", "waistBoneS",
        "lbuttSkinS", "lbuttContS", "lbuttMeatS",
        "rbuttSkinS", "rbuttContS", "rbuttMeatS",

        "bellySkinM", "bellyContM", "bellyMeatM", "bellyOrgansM", "bellyBoneM",
        "breastSkinM", "breastContM", "breastMeatM", "breastOrgansM", "breastBoneM",
        "neckSkinM", "neckContM", "neckMeatM", "neckOrgansM", "neckBoneM",
        "waistSkinM", "waistContM", "waistMeatM", "waistOrgansM", "waistBoneM",
        "lbuttSkinM", "lbuttContM", "lbuttMeatM",
        "rbuttSkinM", "rbuttContM", "rbuttMeatM",

        "bellySkinL", "bellyContL", "bellyMeatL", "bellyOrgansL", "bellyBoneL",
        "breastSkinL", "breastContL", "breastMeatL", "breastOrgansL", "breastBoneL",
        "neckSkinL", "neckContL", "neckMeatL", "neckOrgansL", "neckBoneL",
        "waistSkinL", "waistContL", "waistMeatL", "waistOrgansL", "waistBoneL",
        "lbuttSkinL", "lbuttContL", "lbuttMeatL",
        "rbuttSkinL", "rbuttContL", "rbuttMeatL",

        "bellySkinXL", "bellyContXL", "bellyMeatXL", "bellyOrgansXL", "bellyBoneXL",
        "breastSkinXL", "breastContXL", "breastMeatXL", "breastOrgansXL", "breastBoneXL",
        "neckSkinXL", "neckContXL", "neckMeatXL", "neckOrgansXL", "neckBoneXL",
        "waistSkinXL", "waistContXL", "waistMeatXL", "waistOrgansXL", "waistBoneXL",
        "lbuttSkinXL", "lbuttContXL", "lbuttMeatXL",
        "rbuttSkinXL", "rbuttContXL", "rbuttMeatXL",

        "lbuttBone", "rbuttBone",
    ],

    bodyCommonDef: {
        faceShadowColor: "#ebc0ad",

        headWidth: I2W(264),
        headTop: I2W(100),
        headControlPoint1: I2W(132),
        headControlPoint2: I2W(400),
        headBottom: I2W(520),
        headNeckJointXPos: I2W(0),
        headNeckJointYPos: I2W(400),

        neckWidth: I2W(112),
        neckHeight: I2W(120),
        neckHeadJointXPos: I2W(0),
        neckHeadJointYPos: I2W(20),
        neckBreastJointXPos: I2W(0),
        neckBreastJointYPos: I2W(110),

        breastWidth: I2W(500),
        breastHeight: I2W(449),
        breastNeckJointXPos: I2W(0),
        breastNeckJointYPos: I2W(15),
        breastBellyJointXPos: I2W(0),
        breastBellyJointYPos: I2W(427),
        breastLuparmJointXPos: I2W(180),
        breastLuparmJointYPos: I2W(45),
        breastRuparmJointXPos: I2W(-180),
        breastRuparmJointYPos: I2W(45),

        bellyWidth     : I2W(300),
        bellyHeight    : I2W(153),
        bellyBreastJointXPos: I2W(0),
        bellyBreastJointYPos: I2W(21),
        bellyWaistJointXPos: I2W(0),
        bellyWaistJointYPos: I2W(127),

        waistBellyJointXPos: I2W(0),
        waistBellyJointYPos: I2W(20),

        llegWidth     : I2W(150),
        llegHeight    : I2W(530),
        llegLbuttJointXPos: I2W(20),
        llegLbuttJointYPos: I2W(50),
        llegLfootJointXPos: I2W(-20),
        llegLfootJointYPos: I2W(495),

        rlegRbuttJointXPos: I2W(-20),
        rlegRbuttJointYPos: I2W(50),
        rlegRfootJointXPos: I2W(20),
        rlegRfootJointYPos: I2W(495),

        lfootWidth     : I2W(130),
        lfootHeight    : I2W(310),
        lfootLlegJointXPos: I2W(0),
        lfootLlegJointYPos: I2W(30),

        rfootWidth     : I2W(130),
        rfootHeight    : I2W(310),
        rfootRlegJointXPos: I2W(0),
        rfootRlegJointYPos: I2W(30),

        luparmWidth      : I2W(124),
        luparmHeight     : I2W(428),
        luparmBreastJointXPos: I2W(20),
        luparmBreastJointYPos: I2W(36),
        luparmLloarmJointXPos: I2W(-5),
        luparmLloarmJointYPos: I2W(400),

        ruparmWidth      : I2W(124),
        ruparmHeight     : I2W(428),
        ruparmBreastJointXPos: I2W(-20),
        ruparmBreastJointYPos: I2W(36),
        ruparmRloarmJointXPos: I2W(5),
        ruparmRloarmJointYPos: I2W(400),

        lloarmWidth     : I2W(80),
        lloarmHeight    : I2W(420),
        lloarmLuparmJointXPos: I2W(5),
        lloarmLuparmJointYPos: I2W(45),
        lloarmLhandJointXPos: I2W(0),
        lloarmLhandJointYPos: I2W(375),

        rloarmWidth     : I2W(80),
        rloarmHeight    : I2W(420),
        rloarmRuparmJointXPos: I2W(-5),
        rloarmRuparmJointYPos: I2W(45),
        rloarmRhandJointXPos: I2W(0),
        rloarmRhandJointYPos: I2W(375),

        lhandWidth     : I2W(128),
        lhandHeight    : I2W(122),
        lhandLloarmJointXPos: I2W(-10),
        lhandLloarmJointYPos: I2W(20),

        rhandWidth     : I2W(128),
        rhandHeight    : I2W(122),
        rhandRloarmJointXPos: I2W(10),
        rhandRloarmJointYPos: I2W(20),
    },

    bodySDef: {
        waistWidth      : I2W(340),
        waistHeight     : I2W(287),
        waistLbuttJointXPos: I2W(92),
        waistLbuttJointYPos: I2W(182),
        waistRbuttJointXPos: I2W(-92),
        waistRbuttJointYPos: I2W(182),

        lbuttWidth     : I2W(200),
        lbuttHeight    : I2W(630),
        lbuttWaistJointXPos: I2W(-20),
        lbuttWaistJointYPos: I2W(110),
        lbuttLlegJointXPos: I2W(10),
        lbuttLlegJointYPos: I2W(600),

        rbuttWidth     : I2W(200),
        rbuttHeight    : I2W(630),
        rbuttWaistJointXPos: I2W(20),
        rbuttWaistJointYPos: I2W(110),
        rbuttRlegJointXPos: I2W(-10),
        rbuttRlegJointYPos: I2W(600),
    },

    bodyMDef: {
        waistWidth      : I2W(376),
        waistHeight     : I2W(301),
        waistLbuttJointXPos: I2W(110),
        waistLbuttJointYPos: I2W(187),
        waistRbuttJointXPos: I2W(-110),
        waistRbuttJointYPos: I2W(187),

        lbuttWidth     : I2W(210),
        lbuttHeight    : I2W(630),
        lbuttWaistJointXPos: I2W(-15),
        lbuttWaistJointYPos: I2W(110),
        lbuttLlegJointXPos: I2W(0),
        lbuttLlegJointYPos: I2W(600),

        rbuttWidth     : I2W(210),
        rbuttHeight    : I2W(630),
        rbuttWaistJointXPos: I2W(15),
        rbuttWaistJointYPos: I2W(110),
        rbuttRlegJointXPos: I2W(0),
        rbuttRlegJointYPos: I2W(600),
    },

    bodyLDef: {
        waistWidth      : I2W(428),
        waistHeight     : I2W(324),
        waistLbuttJointXPos: I2W(125),
        waistLbuttJointYPos: I2W(186),
        waistRbuttJointXPos: I2W(-125),
        waistRbuttJointYPos: I2W(186),

        lbuttWidth     : I2W(210),
        lbuttHeight    : I2W(630),
        lbuttWaistJointXPos: I2W(-5),
        lbuttWaistJointYPos: I2W(110),
        lbuttLlegJointXPos: I2W(0),
        lbuttLlegJointYPos: I2W(600),

        rbuttWidth     : I2W(210),
        rbuttHeight    : I2W(630),
        rbuttWaistJointXPos: I2W(5),
        rbuttWaistJointYPos: I2W(110),
        rbuttRlegJointXPos: I2W(0),
        rbuttRlegJointYPos: I2W(600),
    },

    bodyXLDef: {
        waistWidth      : I2W(428),
        waistHeight     : I2W(324),
        waistLbuttJointXPos: I2W(125),
        waistLbuttJointYPos: I2W(176),
        waistRbuttJointXPos: I2W(-125),
        waistRbuttJointYPos: I2W(176),

        lbuttWidth     : I2W(210),
        lbuttHeight    : I2W(630),
        lbuttWaistJointXPos: I2W(-10),
        lbuttWaistJointYPos: I2W(110),
        lbuttLlegJointXPos: I2W(0),
        lbuttLlegJointYPos: I2W(600),

        rbuttWidth     : I2W(210),
        rbuttHeight    : I2W(630),
        rbuttWaistJointXPos: I2W(10),
        rbuttWaistJointYPos: I2W(110),
        rbuttRlegJointXPos: I2W(0),
        rbuttRlegJointYPos: I2W(600),
    },
}};

// Base class for each LL character
// Size is specified in further subclasses
class ZmodLLPackCharacter extends Character {
    constructor(params, rm, style, pose, size) {
        super(params, rm, style, pose);
        this.bodyFullDef = ZmodLLPackGetDef(rm.images.collections.ZmodLLPack, size);
        this.characterSize = size;
    }

    initExtraParts() {
        this.breast.setExtra(this.def["breastExtraImage"]);
        this.luparm.setExtra(this.def["luparmExtraImage"]);
        this.ruparm.setExtra(this.def["ruparmExtraImage"]);
        this.extraParts = [this.luparm, this.ruparm, this.breast];
    }
}

class ZmodLLPackCharacterS extends ZmodLLPackCharacter {
    constructor(params, rm, style, pose) {
        super(params, rm, style, pose, "S");
        this.lnippleRegion = circleRegion(I2W(102), I2W(269), I2W(45));
        this.rnippleRegion = circleRegion(I2W(-102), I2W(269), I2W(45));
        this.pussyRegion = circleRegion(I2W(0), I2W(268), I2W(45));
    }
}

class ZmodLLPackCharacterM extends ZmodLLPackCharacter {
    constructor(params, rm, style, pose) {
        super(params, rm, style, pose, "M");
        this.lnippleRegion = circleRegion(I2W(103), I2W(284), I2W(45));
        this.rnippleRegion = circleRegion(I2W(-103), I2W(284), I2W(45));
        this.pussyRegion = circleRegion(I2W(0), I2W(280), I2W(45));
    }
}

class ZmodLLPackCharacterL extends ZmodLLPackCharacter {
    constructor(params, rm, style, pose) {
        super(params, rm, style, pose, "L");
        this.lnippleRegion = circleRegion(I2W(116), I2W(293), I2W(45));
        this.rnippleRegion = circleRegion(I2W(-116), I2W(293), I2W(45));
        this.pussyRegion = circleRegion(I2W(0), I2W(289), I2W(45));
    }
}

class ZmodLLPackCharacterXL extends ZmodLLPackCharacter {
    constructor(params, rm, style, pose) {
        super(params, rm, style, pose, "XL");
        this.lnippleRegion = circleRegion(I2W(137), I2W(299), I2W(45));
        this.rnippleRegion = circleRegion(I2W(-137), I2W(299), I2W(45));
        this.pussyRegion = circleRegion(I2W(0), I2W(298), I2W(45));
    }
}

function ZmodLLPackGetDef(LLimages, size) {
    let bodySpecDef = ZmodLLPack.Resources["body" + size + "Def"];
    let bodyFullDef = Object.assign({}, ZmodLLPack.Resources.bodyCommonDef, bodySpecDef); // Merging size dependent and common definitions
    bodyFullDef.noseImage = LLimages.nose;
    bodyFullDef.noseBloodyImage = LLimages.nose_bloody;
    bodyFullDef.mouth_close_smile = LLimages.mouth_close_smile;
    bodyFullDef.mouth_close_neutral = LLimages.mouth_close_neutral;
    bodyFullDef.mouth_close_sad = LLimages.mouth_close_sad;
    bodyFullDef.mouth_close_squiggly = LLimages.mouth_close_squiggly;
    bodyFullDef.mouth_close_pouting = LLimages.mouth_close_pouting;
    bodyFullDef.mouth_open_small = LLimages.mouth_open_small;
    bodyFullDef.mouth_open_medium = LLimages.mouth_open_medium;
    bodyFullDef.mouth_open_large = LLimages.mouth_open_large;
    bodyFullDef.mouth_open_squiggly = LLimages.mouth_open_squiggly;
    bodyFullDef.mouth_happy_small = LLimages.mouth_happy_small;
    bodyFullDef.mouth_happy_medium = LLimages.mouth_happy_medium;
    bodyFullDef.mouth_happy_large = LLimages.mouth_happy_large;
    bodyFullDef.mouth_small_o = LLimages.mouth_small_o;
    bodyFullDef.mouth_small_triangle = LLimages.mouth_small_triangle;
    bodyFullDef.mouth_orgasm_1 = LLimages.mouth_orgasm_1;
    bodyFullDef.mouth_orgasm_2 = LLimages.mouth_orgasm_2;
    bodyFullDef.mouth_dead = LLimages.mouth_dead;
    bodyFullDef.mouth_clench_weak = LLimages.mouth_clench_weak;
    bodyFullDef.mouth_clench_medium = LLimages.mouth_clench_medium;
    bodyFullDef.mouth_clench_strong = LLimages.mouth_clench_strong;
    bodyFullDef.mouth_clench_squiggly = LLimages.mouth_clench_squiggly;

    bodyFullDef.blushImage = LLimages.blush;
    bodyFullDef.sweat1Image = LLimages.sweat1;
    bodyFullDef.sweat2Image = LLimages.sweat2;
    bodyFullDef.shadow1Image = LLimages.shadow1;
    bodyFullDef.shadow2Image = LLimages.shadow2;
    bodyFullDef.tears1Image = LLimages.tears1;
    bodyFullDef.tears2Image = LLimages.tears2;
    bodyFullDef.tears3Image = LLimages.tears3;

    bodyFullDef.breastExtraImage = LLimages["breastExtra" + size];
    bodyFullDef.luparmExtraImage = LLimages["luparmExtra"];
    bodyFullDef.ruparmExtraImage = LLimages["ruparmExtra"];
    let types = ["Skin", "Cont", "Meat", "Bone"];
    for (const type of types) {
        bodyFullDef["head" + type + "Image"] = LLimages["head" + type];
        bodyFullDef["lleg" + type + "Image"] = LLimages["lleg" + type];
        bodyFullDef["rleg" + type + "Image"] = LLimages["rleg" + type];
        bodyFullDef["lfoot" + type + "Image"] = LLimages["lfoot" + type];
        bodyFullDef["rfoot" + type + "Image"] = LLimages["rfoot" + type];
        bodyFullDef["luparm" + type + "Image"] = LLimages["luparm" + type];
        bodyFullDef["ruparm" + type + "Image"] = LLimages["ruparm" + type];
        bodyFullDef["lloarm" + type + "Image"] = LLimages["lloarm" + type];
        bodyFullDef["rloarm" + type + "Image"] = LLimages["rloarm" + type];
        bodyFullDef["lhand" + type + "Image"] = LLimages["lhand" + type];
        bodyFullDef["rhand" + type + "Image"] = LLimages["rhand" + type];

        bodyFullDef["belly" + type + "Image"] = LLimages["belly" + type + size];
        bodyFullDef["breast" + type + "Image"] = LLimages["breast" + type + size];
        bodyFullDef["neck" + type + "Image"] = LLimages["neck" + type + size];
        bodyFullDef["waist" + type + "Image"] = LLimages["waist" + type + size];
        bodyFullDef["lbutt" + type + "Image"] = LLimages["lbutt" + type + (type == "Bone" ? "" : size)]; // No separate bone image for butts
        bodyFullDef["rbutt" + type + "Image"] = LLimages["rbutt" + type + (type == "Bone" ? "" : size)];
    }
    bodyFullDef["neckOrgansImage"] = LLimages["neckOrgans" + size];
    bodyFullDef["breastOrgansImage"] = LLimages["breastOrgans" + size];
    bodyFullDef["bellyOrgansImage"] = LLimages["bellyOrgans" + size];
    bodyFullDef["waistOrgansImage"] = LLimages["waistOrgans" + size];
    return bodyFullDef;
}


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

    static creator = "zmod";
    static fullName = "Beta Laboratory";
    static characterCount = 2;
    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(1050),
            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(2700),
            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 CircWheel extends TMap {
    static Resources = {
        images: [
            // Experimental webm support
            "wheel.webp", "wheel_back.webp", "bg.jpg",
        ],
        sounds: [],
    };

    static creator = "zmod";
    static fullName = "Circus wheel";
    static characterCount = 1;
    static depends = ["Nail"];

    constructor(params, rm) {
        super(params, rm);
        this.backgroundImage = createCanvas(this.images["wheel.webp"]);
        this.width = I2W(3840);
        this.height = I2W(2688);
        this.wheelRadius = I2W(1182);

        let tableShape = new b2CircleShape();
        tableShape.SetRadius(this.wheelRadius / SCALE);

        let fd = new b2FixtureDef;
        fd.shape = tableShape;
        fd.filter.categoryBits = NO_COLL.categoryBits;
        fd.filter.maskBits = NO_COLL.maskBits;
        let bd = new b2BodyDef();
        bd.type = b2Body.b2_staticBody;
        bd.position.x = this.width / 2 / SCALE;
        bd.position.y = this.height / 2 / SCALE;
        bd.ang = D2R(0);
        this.wheelBody = this.world.CreateBody(bd);
        this.wheelBody.CreateFixture(fd);
    }

    init() {
        super.init();
        this.createBorders();

        // Fixing the tips of limbs
        let nailReferenceList = [
            [this.characters[0].lfoot, I2W(10), I2W(180)],
            [this.characters[0].rfoot, I2W(-10), I2W(180)],
            [this.characters[0].lhand, I2W(0), I2W(35)],
            [this.characters[0].rhand, I2W(0), I2W(35)]
        ];
        let nail = this.tools["Nail"];
        for (const [part, xMargin, yMargin] of nailReferenceList) {
            let b = part.box2dBody;
            let bang = b.GetAngle();
            let posx = b.GetPosition().x*SCALE + xMargin*Math.cos(-bang) + yMargin*Math.sin(-bang);
            let posy = b.GetPosition().y*SCALE - xMargin*Math.sin(-bang) + yMargin*Math.cos(-bang);
            nail.click(posx, posy, true);
        }
    }

    update() {
        this.wheelBody.SetAngle(this.wheelBody.GetAngle() + D2R(0.5));
    }

    renderBackround(ctx) {
        ctx.drawImage(this.images["bg.jpg"], 0, 0);
        ctx.drawImage(this.images["wheel_back.webp"], 0, 0);
        ctx.save();
        ctx.translate(this.width / 2, this.height / 2);
        ctx.rotate(this.wheelBody.GetAngle());
        ctx.drawImage(this.backgroundImage, -this.width / 2, -this.height/2);
        ctx.restore();
    }

    addWallFixation(body, bx, by) {
        let worldPoint = body.GetWorldPoint(new b2Vec2(bx, by));
        let wheelPoint = this.wheelBody.GetLocalPoint(worldPoint);
        let wpxDiff = worldPoint.x * SCALE - this.width / 2;
        let wpyDiff = worldPoint.y * SCALE - this.height / 2;
        if (Math.pow(wpxDiff, 2) + Math.pow(wpyDiff, 2) <= Math.pow(this.wheelRadius, 2)) { // Circle detection
            let joint = createRevoluteJoint(this.params.world, body, this.wheelBody, bx, by, wheelPoint.x, wheelPoint.y);
            return joint;
        } else {
            return false;
        }
    }

    drawOnBackground(image, x, y, ang, {gco="source-atop", alpha=1} = {}) {
        let ctx = this.backgroundImage.getContext("2d");
        let wheelPoint = this.wheelBody.GetLocalPoint(new b2Vec2(x / SCALE, y / SCALE));
        let wheelAng = this.wheelBody.GetAngle();
        ctx.save();
        ctx.globalCompositeOperation = gco;
        ctx.globalAlpha = alpha;
        ctx.translate(wheelPoint.x * SCALE + this.width / 2, wheelPoint.y * SCALE + this.height / 2);
        ctx.rotate(ang - wheelAng); // Maybe +wheelAng
        ctx.drawImage(image, -image.width/2, -image.height/2);
        ctx.restore();
    }

    getPoseInfo() {
        return [{
            headXPos: I2W(1920),
            headYPos: I2W(100),

            neckHeadJointAngle: D2R(0),
            breastNeckJointAngle: D2R(0),
            bellyBreastJointAngle: D2R(0),
            waistBellyJointAngle: D2R(0),
            lbuttWaistJointAngle: D2R(-28),
            rbuttWaistJointAngle: D2R(28),
            llegLbuttJointAngle: D2R(0),
            rlegRbuttJointAngle: D2R(-0),
            lfootLlegJointAngle: D2R(-12),
            rfootRlegJointAngle: D2R(12),
            luparmBreastJointAngle: D2R(-75),
            ruparmBreastJointAngle: D2R(75),
            lloarmLuparmJointAngle: D2R(-35),
            rloarmRuparmJointAngle: D2R(35),
            lhandLloarmJointAngle: D2R(0),
            rhandRloarmJointAngle: D2R(0),
        }];
    }
}


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

    static creator = "zmod";
    static fullName = "Dead hills";
    static characterCount = 1;
    static depends = ["Nail"];

    constructor(params, rm) {
        super(params, rm);
        this.backgroundImage = createCanvas(this.images["crux"]);
        this.width = I2W(3000);
        this.height = I2W(3000);
        this.renderShadow = false;
    }

    init() {
        super.init();

        let characters = this.characters;

        // Fixing the tips of limbs
        let nailReferenceList = [
            [characters[0].lfoot, I2W(10), I2W(180)],
            [characters[0].rfoot, I2W(-10), I2W(180)],
            [characters[0].lhand, I2W(0), I2W(35)],
            [characters[0].rhand, I2W(0), I2W(35)]
        ];
        let nail = this.tools["Nail"];
        for (const [part, xMargin, yMargin] of nailReferenceList) {
            let b = part.box2dBody;
            let bang = b.GetAngle();
            let posx = b.GetPosition().x*SCALE + xMargin*Math.cos(-bang) + yMargin*Math.sin(-bang);
            let posy = b.GetPosition().y*SCALE - xMargin*Math.sin(-bang) + yMargin*Math.cos(-bang);
            nail.click(posx, posy, true);
        }
    }

    renderBackround(ctx) {
        ctx.drawImage(this.images["bg.jpg"], 0, 0);
        ctx.drawImage(this.backgroundImage, 0, 0);
    }

    addWallFixation(body, bx, by) {
        let worldPoint = body.GetWorldPoint(new b2Vec2(bx, by));
        let wpx = worldPoint.x * SCALE;
        let wpy = worldPoint.y * SCALE;
        if ((wpx > I2W(1348) && wpx < I2W(1652) && wpy > I2W(450)) || // Vertical bar
            (wpx > I2W(436) && wpx < I2W(2564) && wpy > I2W(706) && wpy < I2W(940))) { // Horizontal bar
            let joint = createRevoluteJoint(this.params.world, body, this.wall, bx, by, worldPoint.x, worldPoint.y);
            return joint;
        } else {
            return false;
        }
    }

    getPoseInfo() {
        return [{
            headXPos: I2W(1500),
            headYPos: I2W(500),

            neckHeadJointAngle: D2R(0),
            breastNeckJointAngle: D2R(0),
            bellyBreastJointAngle: D2R(0),
            waistBellyJointAngle: D2R(0),
            lbuttWaistJointAngle: D2R(-21),
            rbuttWaistJointAngle: D2R(21),
            llegLbuttJointAngle: D2R(40),
            rlegRbuttJointAngle: D2R(-40),
            lfootLlegJointAngle: D2R(-7),
            rfootRlegJointAngle: D2R(7),
            luparmBreastJointAngle: D2R(-90),
            ruparmBreastJointAngle: D2R(90),
            lloarmLuparmJointAngle: D2R(-35),
            rloarmRuparmJointAngle: D2R(35),
            lhandLloarmJointAngle: D2R(0),
            rhandRloarmJointAngle: D2R(0),
        }];
    }
}


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

    static creator = "granony, zmod";
    static fullName = "Default Map";
    static characterCount = 1;
    static depends = ["Nail"];

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

    init() {
        super.init();
        this.createBorders(false, true, true, true); // No top border

        let characters = this.characters;

        // Fixing the tips of limbs
        let nailReferenceList = [
            [characters[0].lfoot, I2W(10), I2W(100)],
            [characters[0].rfoot, I2W(-10), I2W(100)],
            [characters[0].lfoot, I2W(10), I2W(180)],
            [characters[0].rfoot, I2W(-10), I2W(180)],
            [characters[0].lhand, I2W(0), I2W(35)],
            [characters[0].rhand, I2W(0), I2W(35)]
        ];
        let nail = this.tools["Nail"];
        for (const [part, xMargin, yMargin] of nailReferenceList) {
            let b = part.box2dBody;
            let bang = b.GetAngle(); // The angle of b2 is clockwise
            let posx = b.GetPosition().x*SCALE + xMargin*Math.cos(-bang) + yMargin*Math.sin(-bang);
            let posy = b.GetPosition().y*SCALE - xMargin*Math.sin(-bang) + yMargin*Math.cos(-bang);
            nail.click(posx, posy, true);
        }

        // Fixing neck (this should be redone later in a more elegant manner)
        let fixationReferenceList = [
            [characters[0].neck, I2W(-20), I2W(90)],
            [characters[0].neck, I2W(20), I2W(90)],
        ];
        for (const [part, xpos, ypos] of fixationReferenceList) {
            this.addWallFixationItem(part, xpos, ypos);
        }
    }

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

        ctx.drawImage(this.images["rope_back"],
            I2W(1920)-this.images["rope_back"].width/2, this.characters[0].neck.box2dBody.GetPosition().y*SCALE-I2W(1050));
    }

    renderBetweenBodyAndHead(ctx) {
        let hang = this.characters[0].head.box2dBody.GetAngle();
        ctx.save();
        ctx.translate(this.characters[0].head.box2dBody.GetPosition().x*SCALE-I2W(352)*Math.sin(hang),
                        this.characters[0].head.box2dBody.GetPosition().y*SCALE+I2W(352)*Math.cos(hang));
        ctx.rotate(hang);
        ctx.drawImage(this.images["rope_front"], -this.images["rope_front"].width/2, 0);
        ctx.restore();
    }

    getPoseInfo() {
        return [{
            headXPos: I2W(1920),
            headYPos: I2W(-60),

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


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

    static creator = "zmod";
    static fullName = "Delta Laboratory";
    static characterCount = 4;
    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);
    }

    // TODO reduce copypasta
    getPoseInfo() {
        return [{
            headXPos: I2W(480),
            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(1440),
            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(2420),
            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(3380),
            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),
        },
        ];
    }
}