code:
/**
 * Component for selecting the outfit for the current character
 * Stores its own state (clothID) and has some advanced functionality for preserving the cloth ID on character changes
 */
class CharacterClothList extends preact.Component {

    constructor(props) {
        super(props);
        let cloths = null;
        cloths = eval(this.props.characterID).costumes;

        this.state = {clothID: cloths[0][0], characterID: this.props.characterID, cloths: cloths};
        this.props.params.styles[this.props.charaIndex].costume = this.state.clothID;
    }

    onChange = (e) => {
        this.setState({clothID: e.target.value});
    }

    componentDidUpdate(prevProps, prevState) {
        if (this.state.clothID !== prevState.clothID) {
            this.props.params.styles[this.props.charaIndex].costume = this.state.clothID;
        }
    }

    static getDerivedStateFromProps(nextProps, prevState) {
        if (nextProps.characterID !== prevState.characterID) {
            let cloths = null;
            cloths = eval(nextProps.characterID).costumes;

            // Only update clothID only when needed
            // For example if we have two characters:

            // Character 1      <-- Selected
            //      Nude
            //      Swimsuit    <-- Selected
            //      Uniform

            // Character 2
            //      Casual
            //      Nude
            //      Lingerie

            // And we switch to Character 2, then obviously the clothID should be reset, Swimsuit does not exist there
            // So the first item is picked (Casual)
            // But if the current outfit has a matching pair in the next character (Nude for example) then clothID won't be updated
            let doesClothExist = false;
            for (const cloth of cloths) {
                let id = cloth[0];
                if (id == prevState.clothID) {
                    doesClothExist = true;
                    break;
                }
            }
            let update = { characterID: nextProps.characterID, cloths: cloths };
            if (!doesClothExist) {
                update.clothID = cloths[0][0];
            }

            return update;
       }
       else return null;
     }

    render () {
        return (html`

            <select value=${this.state.clothID} onChange=${this.onChange}>
                ${this.state.cloths.map((clothEntry) => {
                    const clothID = clothEntry[0];
                    const clothName = clothEntry[1];
                    return html`<option key=${clothID} value=${clothID}>${clothName}</option>`;
                })}
            </select>

        `);
    }
}


code:
/**
 * Component for character options
 * Stores its own state (optionBools) which are single checkboxes independent of each other
 */
class CharacterOptions extends preact.Component {

    constructor(props) {
        super(props);
        let options = null;
        options = eval(this.props.characterID).options;

        let optionBools = Object.assign({}, ...options.map((opt) => ({[opt[0]]: false})));

        this.state = {optionBools: optionBools, characterID: this.props.characterID, options: options};
        this.props.params.styles[this.props.charaIndex].options = this.state.optionBools;
    }

    componentDidUpdate(prevProps, prevState) {
        if (this.state.optionBools !== prevState.optionBools) {
            this.props.params.styles[this.props.charaIndex].options = this.state.optionBools;
        }
    }

    onChange = (id) => {
        this.setState(state => {
            // Updating a dictionary: we have to recreate the whole thing
            const optionBools = {};
            Object.keys(state.optionBools).map((currentID) => {
                if (currentID === id) {
                    optionBools[currentID] = !state.optionBools[currentID]; // Only the clicked option is changed
                } else {
                    optionBools[currentID] = state.optionBools[currentID];
                }
            });
            return {optionBools: optionBools};
        });
    }

    static getDerivedStateFromProps(nextProps, prevState) {
        if (nextProps.characterID !== prevState.characterID) {
            let options = null;
            options = eval(nextProps.characterID).options;

            let optionBools = Object.assign({}, ...options.map((opt) => ({[opt[0]]: false})));
            // TODO something similar as in CharacterClothList
            return {optionBools: optionBools, characterID: nextProps.characterID, options: options};
        }
        else return null;
    }

    render() {
        return (html`

            ${this.state.options.map((option, index) => {
                let id = option[0];
                let name = option[1];
                return (html`
                    ${name}
                        <input type="checkbox" id=${`option-checkbox-${this.props.charaIndex}-${index}`} name=${id} value=${id}
                            checked=${this.state.optionBools[id]} onChange=${() => this.onChange(id)} />

                `);
            })}

        `);
    }
}

repo_name: Tomajc/torture

license: mit


code:
/**
 * Component for character selection
 * Contains a list, which has as many elements as the map allows
 */
class CharacterPickScreen extends preact.Component {

    constructor(props) {
        super(props);
        this.initParams();
        this.state = {characterIDs: characterIDs};
    }

    initParams() {
        this.props.params.characterIDs = Array(this.props.characterCount);
        this.props.params.styles = Array(this.props.characterCount);
        for (let i = 0; i < this.props.characterCount; i++) {
            this.props.params.styles[i] = {};
        }
    }

    componentDidUpdate(prevProps) {
        if (this.props.characterCount !== prevProps.characterCount) {
            this.initParams();
        }
    }

    back = () => {
        this.props.onBack();
    }

    start = () => {
        this.props.onStart();
    }

    randomize = () => {
        // Shuffles the characterIDs array
        let newCharacterIDs = this.state.characterIDs
            .map(value => ({ value, sort: Math.random() }))
            .sort((a, b) => a.sort - b.sort)
            .map(({ value }) => value);
        this.setState({characterIDs: newCharacterIDs});
    }

    render() {
        return (html`

Select characters

            ${Array.from({length: this.props.characterCount}, (_, index) =>{
                return (html`

                    <${CharacterSingleElement} params=${this.props.params}
                        characterID=${this.state.characterIDs[index % this.state.characterIDs.length]}
                        charaIndex=${index}/>

                `);
            })}

            <button onClick=${this.back}>Back</button>
            <button onClick=${this.randomize}>Randomize</button>
            <button onClick=${this.start}>Start</button>

        `);
    }
}


code:
/**
 * Component which acts as a wrapper around the character selection dropdown
 * State (characterID) is handled in CharacterSingleElement
 */
class CharacterSelectList extends preact.Component {

    constructor(props) {
        super(props);
    }

    onChange = e => {
        this.props.onCharacterChange(e.target.value);
    }

    render () {
        const characterID = this.props.characterID;
        return (html`

            <select value=${characterID} onChange=${this.onChange}>
                ${characterIDs.map((chara) => {
                    let name = eval(chara).fullName;
                    return html`<option key=${chara} value=${chara}>${name}</option>`;
                })}
            </select>

        `);
    }
}


code:
/**
 * Component for a single character
 * Contains ui elements for changing the character, her clothes and ticking her additional options on and off
 * This component stores the character ID in its state
 */
class CharacterSingleElement extends preact.Component {

    constructor(props) {
        super(props);
        this.state = {characterID: this.props.characterID}; // this is quite ugly
        this.props.params.characterIDs[this.props.charaIndex] = this.props.characterID;
    }

    handleCharacterChange = (value) => {
        this.props.params.characterIDs[this.props.charaIndex] = value;
        this.setState({characterID: value});
    }

    componentDidUpdate(prevProps) {
        if (this.props.characterID !== prevProps.characterID) {
            this.props.params.characterIDs[this.props.charaIndex] = this.props.characterID;
            this.setState({characterID: this.props.characterID});
        }
    }

    render() {
        return (html`

${this.props.charaIndex + 1}

            <${CharacterSelectList} characterID=${this.state.characterID} onCharacterChange=${this.handleCharacterChange}/>

            <${CharacterClothList} characterID=${this.state.characterID} charaIndex=${this.props.charaIndex} params=${this.props.params}/>
            <${CharacterOptions} characterID=${this.state.characterID} charaIndex=${this.props.charaIndex} params=${this.props.params}/>
        `);
    }
}


code:
/**
 * Component to handle map selection
 * Maps are listed in a scrollable box
 * The ID of the currently selected map is stored in MenuGUI
 */
class MapPickScreen extends preact.Component {

    constructor(props) {
        super(props);
    }

    onChange = e => {
        this.props.onMapChange(e.target.id);
    }

    back = () => {
        this.props.onBack();
    }

    next = () => {
        this.props.onNext();
    }

    render() {
        return(html`

Selected map: ${this.props.mapName}

            ${mapIDs.map((map) => {
                if (this.props.selectedMap === map) {
                    return (html`<img id=${map} class="selected" src="mods/maps/${map}/preview.png" onClick=${this.onChange}/>`);
                }
                return (html`<img id=${map} src="mods/maps/${map}/preview.png" onClick=${this.onChange}/>`);
            })}

            <button onClick=${this.back}>Back</button>
            <button disabled=${this.props.selectedMap === ""} onClick=${this.next}>Next</button>

        `);
    }
}


code:
/**
 * Class which handles the main map and character picking gui element
 * Has 4 states (title, map, character, game) but only map and character has meaningful ui elements
 */
class MenuGUI extends preact.Component {

    constructor(props) {
        super(props);
        this.props.titleScene.setClickStartCallback(() => this.guiMap());
        this.state = {currentGUI: "title", mapID: ""}
        this.characterCount = 0;
        this.mapName = "";
    }

    // Current map info is lifted up to this component
    handleMapChange = (value) => {
        this.characterCount = eval(value).characterCount;
        this.mapName = eval(value).fullName;
        this.props.params.mapID = value;
        this.setState({mapID: value});
    }

    guiTitle = () => {
        jQuery("#menu_gui").addClass("transparent");
        this.props.onTitle();
        this.setState({currentGUI: "title"});
    }

    guiMap = () => {
        jQuery("#menu_gui").removeClass("transparent");
        this.setState({currentGUI: "map"});
    }

    guiCharacter = () => {
        this.setState({currentGUI: "character"});
    }

    start = () => {
        let dom = jQuery("#game")[0];
        dom.dispatchEvent(createChangeSceneEvent("play"));
        jQuery("#menu_gui").addClass("transparent");
        this.setState({currentGUI: "game"}); // render nothing
    }

    // Adds a callback, so that the outside TitleScene (which is not a preact component, and passed as a prop)
    //  can call guiMap without resorting to ugly hacks
    // Preact does not unconditionally call constructor on createElement,
    //  so componentDidUpdate must be used alongside the constructor
    componentDidUpdate(prevProps) {
        if (prevProps.titleScene != this.props.titleScene) {
            this.props.titleScene.setClickStartCallback(() => this.guiMap());
        }
    }

    render() {
        const currentGUI = this.state.currentGUI;
        const mapID = this.state.mapID;
        /*if (currentGUI == "title") {
            return (html`
                <button onClick=${this.guiMap}>Start</button>
            `)
        } else*/ if (currentGUI == "map") {
            return (html`
                <${MapPickScreen} selectedMap=${mapID} mapName=${this.mapName} onMapChange=${this.handleMapChange} onBack=${this.guiTitle} onNext=${this.guiCharacter}/>
            `)
        } else if (currentGUI == "character") {
            return (html`
                <${CharacterPickScreen} characterCount=${this.characterCount} params=${this.props.params} onBack=${this.guiMap} onStart=${this.start}/>
            `)
        } else {
            return null;
        }
    }

}


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">

Torture Chamber

Version ...

You cannot play this game if you are under 18 years of age.

Play the game

Manual for players

Manual for modders

Common practices

Collection mods

Character mods

Character poser tool

Face editor tool (limited functionality)

Tool mods

Map mods

Manual for developers

License

Authors

granony

zmod


code:
// DEPRECATED: use BrowRenderer.js instead
const fs = require('fs');
const { createCanvas, loadImage } = require('canvas');
const Vec2D = require('vector2d');
require('canvas-5-polyfill')

function d2r(degrees) {
    var pi = Math.PI;
    return degrees * (pi/180);
}

const width = 800;
const height = 800;
const w2 = width / 2
const h2 = height / 2

var p = new Path2D(
    "M0 43.7841L29.0197 0C86.6675 9.35515 126.379 17.6707 148.153 24.9468C203.618 43.4805 241.502 60.3296 262.704 72.8037C296.26 92.5455 341.394 121.057 406.784 186.337C411.115 190.66 421.298 201.012 437.331 217.393C372.673 162.873 325.156 126.046 294.779 106.915C252.46 80.2617 217.445 69.1171 197.537 61.094C156.97 44.7452 126.165 38.0759 107.933 34.1109C86.8186 29.5191 68.6953 27.5314 53.9664 29.5288C34.4345 32.1775 8.48528 45.4811 0 43.7841Z"
)

function makeImage(name, color, hp, emotion, direction) {
    var canvas = createCanvas(width, height)
    var ctx = canvas.getContext("2d")

    var invert = (direction === "r")

    var rot = 0
    var tr_x = 0
    var tr_y = 0
    var yfl = 1
    switch (emotion) {
        case "ashamed_strong":
            yfl = -1
            switch (hp) {
                case "A": rot = 20; break;
                case "B": rot = 10; break;
                case "C": rot = 0; break;
                case "D": rot = -10; break;
            }
            break
        case "ashamed_weak":
            yfl = -1
            switch (hp) {
                case "A": rot = 25; break;
                case "B": rot = 15; break;
                case "C": rot = 5; break;
                case "D": rot = -5; break;
            }
            break
        case "normal_1":
            switch (hp) {
                case "A": rot = 0; break;
                case "B": rot = 8; break;
                case "C": rot = 16; break;
                case "D": rot = 24; break;
            }
            break
        case "normal_2":
            tr_y = 20
            switch (hp) {
                case "A": rot = 0; break;
                case "B": rot = 8; break;
                case "C": rot = 16; break;
                case "D": rot = 24; break;
            }
            break
        case "normal_3":
            tr_y = 10
            switch (hp) {
                case "A": rot = 0; break;
                case "B": rot = 8; break;
                case "C": rot = 16; break;
                case "D": rot = 24; break;
            }
            break
        case "surprised_strong":
            switch (hp) {
                case "A": rot = -15; break;
                case "B": rot = -7; break;
                case "C": rot = 0; break;
                case "D": rot = 7; break;
            }
            break
        case "surprised_weak":
            switch (hp) {
                case "A": rot = -10; break;
                case "B": rot = -5; break;
                case "C": rot = 0; break;
                case "D": rot = 5; break;
            }
            break
        case "pain_strong":
            yfl = -1
            switch (hp) {
                case "A": rot = 15; break;
                case "B": rot = 7; break;
                case "C": rot = 0; break;
                case "D": rot = 7; break;
            }
            break
        case "pain_weak":
            yfl = -1
            switch (hp) {
                case "A": rot = 10; break;
                case "B": rot = 5; break;
                case "C": rot = 0; break;
                case "D": rot = 5; break;
            }
            break
    }

    if (hp === "E") {
        rot = 0
    }

    ctx.translate(400-228, 100+400-76)
    ctx.rotate(d2r(-25))
    var s = 0.2

    function drawPath(ctx, p, color) {
        ctx.beginPath()
        for (var i = 0; i < p.ops_.length; i++) {
            switch (p.ops_[i].type) {
                case "moveTo":
                    ctx.moveTo(...p.ops_[i].args)
                    break
                case "bezierCurveTo":
                    ctx.bezierCurveTo(...p.ops_[i].args)
                    break
                case "lineTo":
                    ctx.lineTo(...p.ops_[i].args)
                    break
                case "closePath":
                    ctx.closePath()
                    break
            }
        }

        ctx.fillStyle = color
        ctx.lineWidth = 5
        ctx.fill()
        ctx.globalAlpha = 0.5
        ctx.stroke()
    }

    drawPath(ctx, p, color)

    var finalCanvas = createCanvas(width, height)
    var finalCtx = finalCanvas.getContext("2d")

    finalCtx.translate(w2, h2)

    finalCtx.scale(1, yfl)
    finalCtx.rotate(d2r(rot))
    finalCtx.translate(-w2, -h2)
    /*finalCtx.translate((350 + tr_x), (-50 + tr_y))*/
    finalCtx.drawImage(canvas, 0, 0)

    var finalfinalCanvas = createCanvas(width, height)
    var finalfinalCtx = finalfinalCanvas.getContext("2d")

    finalfinalCtx.translate(w2, h2)
    finalfinalCtx.scale(s * (invert ? -1 : 1), s)
    finalfinalCtx.translate(-w2, -h2)
    finalfinalCtx.translate((350 + tr_x), (-50 + tr_y))
    finalfinalCtx.drawImage(finalCanvas, 0, 0)

    const buffer = finalfinalCanvas.toBuffer('image/png');
    var printhp = hp === "" ? "" : "_" + hp
    var printemo = emotion === "" ? "" : "_" + emotion
    var path = 'game/mods/characters/' + name + '/images/' + direction + 'brow' + printhp + printemo+ '.png'
    console.log(path)
    fs.writeFileSync(path, buffer);
}

var HonokaConfig = {"name": "Honoka", "color": "#D07F31"}
var KotoriConfig = {"name": "Kotori", "color": "#b9a587"}
var UmiConfig = {"name": "Umi", "color": "#3f4064"}
var RinConfig = {"name": "Rin", "color": "#e78757"}
var MakiConfig = {"name": "Maki", "color": "#c03434"}
var HanayoConfig = {"name": "Hanayo", "color": "#b48b2a"}
var NicoConfig = {"name": "Nico", "color": "#444444"}
var EliConfig = {"name": "Eli", "color": "#ebd371"}
var NozomiConfig = {"name": "Nozomi", "color": "#574b71"}

var TsubasaConfig = {"name": "Tsubasa", "color": "#b17744"}
var AnjuConfig = {"name": "Anju", "color": "#ba4405"}
var ErenaConfig = {"name": "Erena", "color": "#61223c"}

var YoshikoConfig = {"name": "Yoshiko", "color": "#1b415d"}
var HanamaruConfig = {"name": "Hanamaru", "color": "#c18e61"}
var YouConfig = {"name": "You", "color": "#a99181"}
var ChikaConfig = {"name": "Chika", "color": "#e68556"}
var KananConfig = {"name": "Kanan", "color": "#2d3261"}
var MariConfig = {"name": "Mari", "color": "#f1dd88"}

//var confs = [HonokaConfig,KotoriConfig,UmiConfig,RinConfig,MakiConfig,HanayoConfig,NicoConfig,EliConfig,NozomiConfig,TsubasaConfig,AnjuConfig,ErenaConfig,YoshikoConfig,HanamaruConfig,YouConfig,ChikaConfig,KananConfig,MariConfig]
var confs = [MariConfig]

var hps = ["A", "B", "C", "D"]
var dirs = ["l", "r"]

//makeImage(conf, "A", "normal_1", "l")
confs.forEach(conf => {
    var name = conf["name"]
    var color = conf["color"]
    dirs.forEach(dir => {

        hps.forEach(hp => {
            makeImage(name, color, hp, "ashamed_weak", dir)
            makeImage(name, color, hp, "ashamed_strong", dir)
            makeImage(name, color, hp, "normal_1", dir)
            makeImage(name, color, hp, "normal_2", dir)
            makeImage(name, color, hp, "normal_3", dir)
            makeImage(name, color, hp, "pain_weak", dir)
            makeImage(name, color, hp, "pain_strong", dir)
            makeImage(name, color, hp, "surprised_weak", dir)
            makeImage(name, color, hp, "surprised_strong", dir)
        })
        makeImage(name, color, "E", "", dir)
    })
})


code:
<!DOCTYPE html>

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

            <input type="file" style="visibility: hidden" id="game-files" webkitdirectory directory multiple title="Select Game Folder"/>
            Select Game Folder

                <canvas id="posercanvas">
                    html5 canvas required - please download a compatible browser
                </canvas>

Index


code:
const DEBUG = false;
const TEXTURE_SCALE = 1;
const SCALE    = 200 * TEXTURE_SCALE;
const TIMESTEP = 1000/30;
const CHARACTER = "Honoka";

const WIDTH = 500*TEXTURE_SCALE;
const HEIGHT = 500*TEXTURE_SCALE;

let characterIDs = [CHARACTER];
let mapIDs = [];
let toolIDs = [];
let collectionIDs = ["ZmodLLPack", "ZmodLLClothesPack"];

let scripts = [];
for (const id of collectionIDs) scripts.push("../../game/mods/collections/" + id + "/" + id + ".js");
for (const id of characterIDs) scripts.push("../../game/mods/characters/" + id + "/" + id + ".js");
for (const id of mapIDs) scripts.push("../../game/mods/maps/" + id + "/" + id + ".js");
for (const id of toolIDs) scripts.push("../../game/mods/tools/" + id + "/" + id + ".js");

let scriptLoader, rm, dom, chara, ctx;

// Loads scripts upon page load
jQuery(document).ready(function() {
    let canvas = jQuery("#posercanvas")[0];
    canvas.setAttribute("width", WIDTH);
    canvas.setAttribute("height", HEIGHT);
    ctx = canvas.getContext("2d");

    dom = jQuery("#game")[0];
    scriptLoader = new ScriptLoader(scripts, false);
    dom.addEventListener("load_scripts_complete", onLoadScripts);
    scriptLoader.load();
});

// Loads resources upon scrips load
function onLoadScripts() {
    rm = new ResourceManager();
    rm.gamePath = "../../game/";

    let files = jQuery("#game-files")[0];
    files.addEventListener("change", e => rm.setFiles(e.target.files));

    dom.addEventListener("load_resources_complete", onLoadResources);
    rm.load();
}
let world;

const referenceImage = new Image();

// Creates a world and spawns a single character
function onLoadResources() {
    world = new b2World(new b2Vec2(0, 0), false);
    let params = {}
    params.world = world;
    params.particleManager = {addEmitter: ()=>{}};

    let style = {costume: "nude", options: {}};
    let pose = {
        headXPos: I2W(250),
        headYPos: I2W(-30),

        neckHeadJointAngle: D2R(0),
        breastNeckJointAngle: D2R(0),
        bellyBreastJointAngle: D2R(0),
        waistBellyJointAngle: D2R(0),
        lbuttWaistJointAngle: D2R(-24),
        rbuttWaistJointAngle: D2R(24),
        llegLbuttJointAngle: D2R(-2),
        rlegRbuttJointAngle: D2R(2),
        lfootLlegJointAngle: D2R(-4),
        rfootRlegJointAngle: D2R(4),
        luparmBreastJointAngle: D2R(-115),
        ruparmBreastJointAngle: D2R(115),
        lloarmLuparmJointAngle: D2R(-15),
        rloarmRuparmJointAngle: D2R(15),
        lhandLloarmJointAngle: D2R(-10),
        rhandRloarmJointAngle: D2R(10),
    };
    eval("chara = new " + CHARACTER + "(params, rm, style, pose)");

    /*let transformXSlider = jQuery("#transformX")[0];
    transformXSlider.oninput = (e) => { chara.eyeRenderer.globalTranslateX = parseInt(e.target.value); }
    transformXSlider.min = -100;
    transformXSlider.max = 600;
    let transformYSlider = jQuery("#transformY")[0];
    transformYSlider.oninput = (e) => { chara.eyeRenderer.globalTranslateY = parseInt(e.target.value); }
    transformYSlider.min = -100;
    transformYSlider.max = 600;
    let rotateSlider = jQuery("#rotation")[0];
    rotateSlider.oninput = (e) => { chara.eyeRenderer.globalRotate = D2R(parseInt(e.target.value)); }
    rotateSlider.min = 0;
    rotateSlider.max = 360;
    let scaleSlider = jQuery("#scale")[0];
    scaleSlider.min = 0;
    scaleSlider.max = 2;
    scaleSlider.step = 0.01;
    scaleSlider.oninput = (e) => { chara.eyeRenderer.globalScale = parseFloat(e.target.value); }

    for (const key of chara.browRenderer.keys) {
        let input = document.createElement("input");
        input.type = "range";
        input.min = 0;
        input.max = 1;
        input.step = 0.01;
        input.oninput = (e) => { chara.browRenderer.setMorph(key, parseFloat(e.target.value)); };
        jQuery("#contents")[0].appendChild(input); // put it into the DOM
    }

    chara.browRenderer.resetStates();*/

    referenceImage.src = "reference.png";

    window.setInterval(render, TIMESTEP);
}
// Render loop
function render() {
    clearCtx(ctx);
    chara.updateHeadCanvas();
    chara.renderBackHair(ctx);
    chara.neck.render(ctx, BodyPart.SKIN);
    chara.neck.render(ctx, BodyPart.CONTOUR);
    chara.breast.render(ctx, BodyPart.SKIN);
    chara.breast.render(ctx, BodyPart.CONTOUR);
    chara.eyeRenderer.update();
    chara.renderHead(ctx);
    let eyeCtx = chara.head.eyeCanvas.getContext("2d");
    clearCtx(eyeCtx);
    chara.eyeRenderer.render(eyeCtx);

    ctx.save();
    ctx.globalAlpha = 0.5;
    //ctx.drawImage(referenceImage, 0, 0);
    ctx.restore();

    //chara.browRenderer.render(ctx);
}


code:
#!/usr/bin/perl
=pod
Flop eyes and brows images for left-side to those for right-side.
=cut
use utf8;
use strict;

my @srcs;
push @srcs, <./base/leye*.png>;
push @srcs, <./base/lbrow*.png>;
foreach my $src (@srcs){
  my $to = $src;
  $to =~s{base/l}{base/r};
  print "$src=>$to\n";
  system "convert -flop $src $to";
}


code:
#poser {
    display: flex;
    flex-direction: row
}

#poser div {
    margin-right: 20px;
}

.slidecontainer input {
    display: block;
    width: 250px;
}

input {
    margin-bottom: 10px;
}

button {
    width: 100%;
    height: 70px;
    font-size: 20px;
    margin-bottom: 10px;
}

textarea {
    display: block;
    border: 1px solid #0080B0;
}


code:
<!DOCTYPE html>

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

            <input type="file" style="visibility: hidden" id="game-files" webkitdirectory directory multiple title="Select Game Folder"/>
            Select Game Folder

                    <canvas id="posercanvas">
                        html5 canvas required - please download a compatible browser
                    </canvas>

                    <input type="range" class="slider" id="headNeckAng">
                    <input type="range" class="slider" id="neckBreastAng">
                    <input type="range" class="slider" id="breastBellyAng">
                    <input type="range" class="slider" id="bellyWaistAng">

                    <input type="range" class="slider" id="waistLbuttAng">
                    <input type="range" class="slider" id="waistRbuttAng">
                    <input type="range" class="slider" id="lbuttLlegAng">
                    <input type="range" class="slider" id="rbuttRlegAng">
                    <input type="range" class="slider" id="llegLfootAng">
                    <input type="range" class="slider" id="rlegRfootAng">

                    <input type="range" class="slider" id="breastLuparmAng">
                    <input type="range" class="slider" id="breastRuparmAng">
                    <input type="range" class="slider" id="luparmLloarmAng">
                    <input type="range" class="slider" id="ruparmRloarmAng">
                    <input type="range" class="slider" id="lloarmLhandAng">
                    <input type="range" class="slider" id="rloarmRhandAng">

                    <button onclick="generatePoseInfo()">Generate Pose</button>
                    <textarea id="poseinfo" rows="25" cols="50"></textarea>

Index


code:
const DEBUG = false;
const TEXTURE_SCALE = 0.33;
const SCALE    = 200 * TEXTURE_SCALE;
const TIMESTEP = 1000/30;
const CHARACTER = "Honoka";

const WIDTH = 2400*TEXTURE_SCALE;
const HEIGHT = 2600*TEXTURE_SCALE;

let characterIDs = [CHARACTER];
let mapIDs = [];
let toolIDs = [];
let collectionIDs = ["ZmodLLPack", "ZmodLLClothesPack"];

let scripts = [];
for (const id of collectionIDs) scripts.push("../../game/mods/collections/" + id + "/" + id + ".js");
for (const id of characterIDs) scripts.push("../../game/mods/characters/" + id + "/" + id + ".js");
for (const id of mapIDs) scripts.push("../../game/mods/maps/" + id + "/" + id + ".js");
for (const id of toolIDs) scripts.push("../../game/mods/tools/" + id + "/" + id + ".js");

let scriptLoader, rm, dom, chara, ctx;

// Loads scripts upon page load
jQuery(document).ready(function() {
    let canvas = jQuery("#posercanvas")[0];
    canvas.setAttribute("width", WIDTH);
    canvas.setAttribute("height", HEIGHT);
    ctx = canvas.getContext("2d");

    dom = jQuery("#game")[0];
    scriptLoader = new ScriptLoader(scripts, false);
    dom.addEventListener("load_scripts_complete", onLoadScripts);
    scriptLoader.load();
});

// Loads resources upon scrips load
function onLoadScripts() {
    rm = new ResourceManager();
    rm.gamePath = "../../game/";

    let files = jQuery("#game-files")[0];
    files.addEventListener("change", e => rm.setFiles(e.target.files));

    dom.addEventListener("load_resources_complete", onLoadResources);
    rm.load();
}
let world;

parents = ["head", "neck", "breast", "belly", "waist", "waist", "lbutt", "rbutt",
        "lleg", "rleg", "breast", "breast", "luparm", "ruparm", "lloarm", "rloarm"];
parts = ["neck", "breast", "belly", "waist", "lbutt", "rbutt", "lleg", "rleg",
        "lfoot", "rfoot", "luparm", "ruparm", "lloarm", "rloarm", "lhand", "rhand"];

// Creates a world and spawns a single character
function onLoadResources() {
    world = new b2World(new b2Vec2(0, 0), false);
    let params = {}
    params.world = world;
    params.particleManager = {addEmitter: ()=>{}};

    let style = {costume: "nude", options: {}};
    let pose = {
        headXPos: I2W(1200),
        headYPos: I2W(150),

        neckHeadJointAngle: D2R(0),
        breastNeckJointAngle: D2R(0),
        bellyBreastJointAngle: D2R(0),
        waistBellyJointAngle: D2R(0),
        lbuttWaistJointAngle: D2R(-24),
        rbuttWaistJointAngle: D2R(24),
        llegLbuttJointAngle: D2R(-2),
        rlegRbuttJointAngle: D2R(2),
        lfootLlegJointAngle: D2R(-4),
        rfootRlegJointAngle: D2R(4),
        luparmBreastJointAngle: D2R(-115),
        ruparmBreastJointAngle: D2R(115),
        lloarmLuparmJointAngle: D2R(-15),
        rloarmRuparmJointAngle: D2R(15),
        lhandLloarmJointAngle: D2R(-10),
        rhandRloarmJointAngle: D2R(10),
    };
    eval("chara = new " + CHARACTER + "(params, rm, style, pose)");

    for (let i = 0; i < 16; i++) {
        setupSlider(parents[i], parts[i]);
    }

    window.setInterval(render, TIMESTEP);
}

let currentName = "";
let slider = undefined;

// Callback for slider, updates the respective body parts and labels
function sliderCallback(parName, name, angle) {
    eval('chara.' + name + '.bodySetAngle(' + angle + ');');
    eval('jQuery("label[for=\'' + parName + cap1st(name) + 'Ang\']").text("' + cap1st(parName) + '-' + name + ' angle: ' + R2D(angle) + '°");');
    currentName = name;
}

// Setting minimum, maximum and current value, intializes callback
function setupSlider(parName, name) {
    eval('slider = jQuery("#' + parName + cap1st(name) + 'Ang")[0];');
    eval('slider.min = R2D(chara.def.' + parName.toUpperCase() + '_' + name.toUpperCase() + '_MIN_ANGLE);');
    eval('slider.max = R2D(chara.def.' + parName.toUpperCase() + '_' + name.toUpperCase() + '_MAX_ANGLE);');
    eval('slider.value = R2D(chara.def.' + name + cap1st(parName) + 'JointAngle);');
    eval('slider.oninput = (e) => { sliderCallback(parName, name, D2R(e.target.value)); };');
    eval('jQuery("label[for=\'' + parName + cap1st(name) + 'Ang\']").text("' + cap1st(parName) + '-' + name + ' angle: ' + eval('R2D(chara.def.' + name + cap1st(parName) + 'JointAngle);') + '°");');
}

function generatePoseInfo() {
    let textarea = jQuery('#poseinfo')[0];
    txt = "headXPos: I2W(1200),\nheadYPos: I2W(0),\n\n";
    for (let i = 0; i < 16; i++) {
        eval('slider = jQuery("#' + parents[i] + cap1st(parts[i]) + 'Ang")[0];');
        txt += parts[i] + cap1st(parents[i]) + "JointAngle: D2R(" + slider.value + "),\n";
    }
    textarea.value = txt;
}

// Render loop
function render() {
    clearCtx(ctx);
    chara.updateHeadCanvas();
    chara.renderBackHair(ctx);
    chara.renderBody(ctx);
    chara.renderHead(ctx);

    // Draw helper lines to show limits visually
    if ("" != currentName) {
        ctx.lineWidth = 4;
        let bodyPart = eval('chara.' + currentName);
        let ang = bodyPart.box2dBody.GetAngle();
        let mina = bodyPart.jointMINA + bodyPart.parent.box2dBody.GetAngle();
        let maxa = bodyPart.jointMAXA + bodyPart.parent.box2dBody.GetAngle();
        let l = 100;
        let posx = bodyPart.box2dBody.GetPosition().x * SCALE + bodyPart.jointXC * Math.cos(ang) - bodyPart.jointYC * Math.sin(ang);
        let posy = bodyPart.box2dBody.GetPosition().y * SCALE + bodyPart.jointYC * Math.cos(ang) + bodyPart.jointXC * Math.sin(ang);

        ctx.strokeStyle = "#D0000070"
        ctx.beginPath();
        ctx.moveTo(posx, posy);
        ctx.lineTo(posx - Math.sin(mina) * l*2/3, posy + Math.cos(mina) * l*2/3);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(posx, posy);
        ctx.lineTo(posx - Math.sin(maxa) * l*2/3, posy + Math.cos(maxa) * l*2/3);
        ctx.stroke();

        ctx.fillStyle = "#D0000030";
        ctx.beginPath();
        ctx.moveTo(posx, posy);
        ctx.arc(posx, posy, l/3, mina + Math.PI/2, maxa + Math.PI/2);
        ctx.moveTo(posx, posy);
        ctx.stroke();
        ctx.fill();

        ctx.strokeStyle = "#50C000B0"
        ctx.beginPath();
        ctx.moveTo(posx, posy);
        ctx.lineTo(posx - Math.sin(ang) * l, posy + Math.cos(ang) * l);
        ctx.stroke();
    }

    // Do not step world; physics would mess the whole pose up
    //world.Step(1/30, 20, 10);
}


code:
#!/usr/bin/perl
=pod
  Change color of brows.
  convert is a command of ImageMagick.
  convert -modulate brightness[,saturation,hue]
=cut
use utf8;
use strict;

my @srcs = <./base/lbrow_blue*.png>;
foreach my $src (@srcs){
  my $to;

  $to = $src;
  $to =~s/blue/gold/;
  print "$src=>$to\n";
  system "convert -modulate 100,100,205 $src $to";

  $to = $src;
  $to =~s/blue/black/;
  print "$src=>$to\n";
  system "convert -modulate 0,100,0 $src $to";
}


code:
#!/usr/bin/perl
=pod
  Change color of eyes.
  convert is a command of ImageMagick.
  convert -modulate brightness[,saturation,hue]
=cut
use utf8;
use strict;
use File::Copy;

my @srcs = <./base/leye_blue*.png>;
foreach my $src (@srcs){
  next if($src=~/open/);
  my $to;
  $to = $src;
  $to =~s/blue/gold/;
  print "$src=>$to\n";
  system "convert -modulate 100,100,205 $src $to"; # blue to gold

  $to = $src;
  $to =~s/blue/red/;
  print "$src=>$to\n";
  system "convert -modulate 100,100,180 $src $to"; # blue to red

  $to = $src;
  $to =~s/blue/black/;
  print "$src=>$to\n";
  system "convert -modulate 100,0,180 $src $to"; # blue to black
}

# leye_*open.png, which do not have pupil, but have red blood, are same.
copy "leye_blue_open.png", "leye_gold_open.png";
copy "leye_blue_open.png", "leye_red_open.png";
copy "leye_blue_open.png", "leye_black_open.png";


code:
#!/usr/bin/perl
use warnings FATAL => 'all';
use utf8;
use strict;

my @srcs;
push @srcs, <../game/system/images/base/*.png>;
push @srcs, <../game/mods/characters/*/images/*.png>;
push @srcs, <../game/mods/tools/*/images/*.png>;
push @srcs, <../game/system/images/ui/rope*.png>;
push @srcs, <../game/system/images/ui/title_logo.png>;
push @srcs, <../game/system/images/ui/background_tile.png>;
push @srcs, <../game/system/images/ui/camera.png>;
push @srcs, <../game/system/images/ui/exit.png>;
push @srcs, <../game/system/images/ui/speaker.png>;
push @srcs, <../game/system/images/ui/speaker2.png>;
foreach my $src (@srcs){
    my $param = $ARGV[0] or die "No scaling parameter given";
    my $cmd = "convert $src -resize ${param}00% $src";
    print "$cmd\n";
    system $cmd;
}


code:
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain

path: .gitattributes

license: unknown


code:
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb
/Source/Patches/PawnAnimationPatches/HarmonyPatch_Pawn_DrawTracker.cs
/Source/Patches/PawnAnimationPatches/HarmonyPatch_PawnRotation.cs
/Source/Patches/PawnAnimationPatches/HarmonyPatch_PawnRenderer.cs
/Source/Patches/OtherModPatches/HarmonyPatch_ShowHairWithHats.cs
/Source/Patches/OtherModPatches/HarmonyPatch_FacialAnimation.cs
/Source/Patches/OtherModPatches/HarmonyPatch_DontShaveYourHead.cs
/Source/Patches/OtherModPatches/HarmonyPatch_CSL.cs
/Source/Patches/OtherModPatches/HarmonyPatch_AlienRace.cs
/Source/Patches/ThingAnimationPatches/HarmonyPatch_ThingDrawAt.cs
/Defs/AnimationDefs/Animations_SexToys.xml
/1.5/Defs/AnimationDefs/BasicBestiality/HumanRotatedOffset.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality4v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality3v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality2v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/BasicBestiality1v1.xml
/1.5/Defs/AnimationDefs/BasicBestiality/!BasicBestiality.xml
/1.5/Defs/TestDoNotPush
/1.5/Textures/AnimationProps/BEV

path: .gitignore