code:
/* Hair: base is red */

@include make_hair('black') {
    -webkit-filter: hue-rotate(0deg) saturate(0%) brightness(40%) contrast(100%);
    filter: hue-rotate(0deg) saturate(0%) brightness(40%) contrast(100%);
}

@include make_hair('platinumblond') {
    -webkit-filter: hue-rotate(57deg) saturate(250%) brightness(180%) contrast(60%) sepia(0.75);
    filter: hue-rotate(57deg) saturate(250%) brightness(180%) contrast(60%) sepia(0.75);
}

@include make_hair('blond') {
    -webkit-filter: hue-rotate(60deg) saturate(130%) brightness(135%) contrast(100%);
    filter: hue-rotate(60deg) saturate(130%) brightness(135%) contrast(100%);
}

@include make_hair('blue') {
    -webkit-filter: hue-rotate(210deg) saturate(200%) brightness(60%) contrast(150%);
    filter: hue-rotate(210deg) saturate(200%) brightness(60%) contrast(150%);
}

@include make_hair('brown') {
    -webkit-filter: hue-rotate(40deg) saturate(100%) brightness(80%) contrast(100%);
    filter: hue-rotate(40deg) saturate(100%) brightness(80%) contrast(100%);
}

@include make_hair('lightbrown') {
    -webkit-filter: hue-rotate(40deg) saturate(100%) brightness(100%) contrast(110%);
    filter: hue-rotate(40deg) saturate(100%) brightness(100%) contrast(110%);
}

@include make_hair('strawberryblond') {
    -webkit-filter: hue-rotate(30deg) saturate(110%) brightness(110%) contrast(100%);
    filter: hue-rotate(30deg) saturate(110%) brightness(110%) contrast(100%);
}

@include make_hair('ginger') {
    -webkit-filter: hue-rotate(55deg) saturate(200%) brightness(110%) contrast(100%);
    filter: hue-rotate(55deg) saturate(200%) brightness(110%) contrast(100%);
}

@include make_hair('green') {
    -webkit-filter: hue-rotate(90deg) saturate(100%) brightness(100%) contrast(100%);
    filter: hue-rotate(90deg) saturate(100%) brightness(100%) contrast(100%);
}

@include make_hair('teal') {
    -webkit-filter: hue-rotate(165deg) saturate(100%) brightness(100%) contrast(100%);
    filter: hue-rotate(165deg) saturate(100%) brightness(100%) contrast(100%);
}

@include make_hair('pink') {
    -webkit-filter: hue-rotate(-20deg) saturate(70%) brightness(130%) contrast(100%);
    filter: hue-rotate(-20deg) saturate(70%) brightness(130%) contrast(100%);
}

@include make_hair('hotpink') {
    -webkit-filter: hue-rotate(320deg) saturate(100%) brightness(120%) contrast(100%);
    filter: hue-rotate(320deg) saturate(100%) brightness(120%) contrast(100%);
}

@include make_hair('softpink') {
    -webkit-filter: hue-rotate(0deg) saturate(30%) brightness(190%) contrast(100%);
    filter: hue-rotate(0deg) saturate(30%) brightness(190%) contrast(100%);
}

@include make_hair('purple') {
    -webkit-filter: hue-rotate(-80deg) saturate(110%) brightness(60%) contrast(200%);
    filter: hue-rotate(-80deg) saturate(110%) brightness(60%) contrast(200%);
}

@include make_hair('red') {
    -webkit-filter: hue-rotate(0deg) saturate(100%) brightness(100%) contrast(100%);
    filter: hue-rotate(0deg) saturate(100%) brightness(100%) contrast(100%);
}

@include make_hair('crimson') {
    -webkit-filter: hue-rotate(0deg) saturate(155%) brightness(75%) contrast(135%);
    filter: hue-rotate(0deg) saturate(155%) brightness(75%) contrast(135%);
}

@include make_hair('white') {
    -webkit-filter: hue-rotate(0deg) saturate(0%) brightness(160%) contrast(100%);
    filter: hue-rotate(0deg) saturate(0%) brightness(160%) contrast(100%);
}

@include make_hair('snowwhite') {
    -webkit-filter: hue-rotate(0deg) saturate(0%) brightness(190%) contrast(100%);
    filter: hue-rotate(0deg) saturate(0%) brightness(190%) contrast(100%);
}


code:
@each $key, $value in $layers {
    .layer-#{$key} {
        z-index: $value;
    }
}

repo_name: warawanaineko/degrees


code:
#map-display-container {
    width: 100%;
    position: relative;

    img {
        display: block;
    }

    #background-layer {
        z-index: -1;
    }

    .location-single {
        position: absolute;
        transition: margin-top $mapLocTransition;
        margin-top: 0;

        .location-light {
            position: absolute;
            top: 0;
            left: 0;
        }

        .location-active-trigger {
            position: absolute;
            z-index: 1;
        }

        &.link-active {
            &.location-is-hovered {
                margin-top: -10px;
            }

            // DEV
            border: solid 3px blue;
        }
    }
}


code:
@mixin make_meter($selector, $color) {
    .#{unquote($selector)} {
        position: absolute;
        left: 0;
        top: 0;
        height: 0.3em;
        background-color: $color;
        z-index: -10;
    }
}

@mixin make_hair($selector) {
    .hair-#{unquote($selector)} {
        .colour-hair {
            @content;
        }
    }
}

@mixin make_eye($selector) {
    .eye-#{unquote($selector)} {
        .colour-eye {
            @content;
        }
    }
}

@mixin make_clothing($selector) {
    .upper-#{unquote($selector)} {
        .colour-upper {
            @content;
        }
    }

    .lower-#{unquote($selector)} {
        .colour-lower {
            @content;
        }
    }

    .under-#{unquote($selector)} {
        .colour-under {
            @content;
        }
    }
}

@mixin make_animation($identifier, $steps, $time, $pace: "", $overrideSteps: false) {
    $class: unquote("anim-") + unquote($identifier) + "-" + $steps + unquote("f");

    @if $pace != "" {
        $class: $class + unquote("-") + unquote($pace);
    }

    @if $overrideSteps != false {
        $steps: $overrideSteps;
    }

    .#{$class} {
        -webkit-animation-duration: $time;
        -moz-animation-duration: $time;
        -o-animation-duration: $time;
        animation-duration: $time;

        -webkit-animation-timing-function: steps($steps);
        -moz-animation-timing-function: steps($steps);
        -o-animation-timing-function: steps($steps);
        animation-timing-function: steps($steps);

        -webkit-animation-iteration-count: infinite;
        -moz-animation-iteration-count: infinite;
        -o-animation-iteration-count: infinite;
        animation-iteration-count: infinite;

        -webkit-animation-name: anim-default;
        -moz-animation-name: anim-default;
        -o-animation-name: anim-default;
        animation-name: anim-default;
    }
}


code:
.macro-numberslider {
    display: inline-block;

    input {
        width: 3em;
        text-align: center;

        -webkit-appearance: none;
        cursor: pointer;
        height: 1.25em;
        min-width: 20em;
        vertical-align: middle;

        &:focus {
            outline: none;
        }

        &::-webkit-slider-runnable-track, &::-moz-range-track {
            background: #222;
            border: 1px solid #444;
            border-radius: 0;
            cursor: pointer;
            height: 10px;
            width: 100%;

            &:focus {
                background: #222;
            }
        }

        &::-webkit-slider-thumb, &::-moz-range-thumb {
            -webkit-appearance: none;
            background: #35a;
            border: 1px solid #57c;
            border-radius: 0;
            cursor: pointer;
            height: 18px;
            margin-top: -5px;
            width: 33px;
        }

        &::-ms-track {
            background: transparent;
            border-color: transparent;
            color: transparent;
            cursor: pointer;
            height: 10px;
            width: 99%; /* fallback for MS browsers without support for calc() */
            width: calc(100% - 1px);
        }

        &::-ms-thumb {
            background: #35a;
            border: 1px solid #57c;
            border-radius: 0;
            cursor: pointer;
            height: 16px;
            width: 33px;
        }

        &::-ms-fill-lower {
            background: #222;
            border: 1px solid #444;
            border-radius: 0;
        }

        &::-ms-fill-upper {
            background: #222;
            border: 1px solid #444;
            border-radius: 0;
        }
    }

    button {
        font-family: "tme-fa-icons";
        font-style: normal;
        font-weight: normal;
        font-variant: normal;
        text-transform: none;
        line-height: normal;
        speak: none;
        padding: 0.4em 0.6em;

        &[id^="numberbox-minus"] {
            margin-right: 0.4em;
        }

        &[id^="numberbox-plus"] {
            margin-left: 0.4em;
        }
    }

    span {
        display: inline-block;
        /*font-size: 1.25em;*/
        margin-left: 0.25em;
        min-width: 3em;
        text-align: center;
        vertical-align: middle;
    }
}


code:
#ui-bar {
    z-index: 300;
}

#ui-bar-toggle {
    z-index: 500;
}

body {
    &.has-images {
        header {
            &#title {
                display: none;
            }
        }

        #story-caption {
            /* 256 px: images
	        2.5 em: ui-bar-body margin-top */
            margin-top: calc(256px - 2.5em);
        }
    }
}

.hide {
    a {
        color: $white;
        text-decoration: none !important;

        &:hover {
            color: $white;
        }
    }
}

.general {
    opacity: 0.99;
}

#statbar {
    position: relative;
    left: 0;
    top: 9px;
    z-index: 0;
}

#img {
    height: 256px;
    width: 256px;
    overflow: hidden;

    position: fixed;
    top: 0;
    left: 0;

    & > * {
        position: absolute;
        top: 0;
        left: 0;
    }
}

#daystate {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 10;
}

#weather {
    position: fixed;
    left: 0;
    top: 64px;
    z-index: 10;
}

#location {
    position: fixed;
    left: 0;
    top: 128px;
    z-index: 10;
}

#divmap {
    position: relative;
    display: inline-block;
    height: 256px;
    width: 256px;

}

#map {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 10;
}

#maparrow {
    position: absolute;
    left: 0;
    top: 0;
    z-index: 20;
}

.meter {
    position: relative;
    height: 0.3em;
    width: 100%;
    background-color: gray;
    text-align: center;
    z-index: -10;
}

@include make_meter('redbar', $red);
@include make_meter('pinkbar', $pink);
@include make_meter('purplebar', $purple);
@include make_meter('bluebar', $blue);
@include make_meter('lbluebar', $light-blue);
@include make_meter('tealbar', $teal);
@include make_meter('greenbar', $green);
@include make_meter('goldbar', $gold);


code:
$red: #EC3535;
$pink: #E40081;
$purple: #AA4BC8;
$blue: #4372FF;
$light-blue: #559BC0;
$teal: #08C1C1;
$green: #38B20A;
$platinum: #FEF593;
$gold: #FFD700;
$orange: #f28500;
$strawberry: #F08C51;
$tangerine: #f28500;
$brown: #aa7243;
$light-brown: #C5793A;
$gray: #929292;
$light-gray: #B7B7B7;

$white: #eee;

$action-brat: #B75D41;
$action-meek: #C44B8B;
$action-def: #D20000;
$action-sub: #AA4BC8;
$action-lewd: #E04880;

$mapLocTransition: 0.3s;

$layers: (
    'bg': 0,
    'backhair': 10,
    'base': 20,
    'hirsute': 21,
    'eyes': 30,
    'sclera': 35,
    'mouth': 40,
    'blush': 50,
    'tears': 55,
    'hair': 60,
    'brow': 62,
    'lashes': 64,
    'under': 75,
    'legsacc': 80,
    'feetacc': 85,
    'lower': 90,
    'upper': 95,
    'collar': 100,
    'rightarm': 105,
    'leftarm': 105,
    'rightarmclothes': 110,
    'leftarmclothes': 110,
    'faceacc': 125,
    'headacc': 130
);


code:
.wetstage2 {
    opacity: 0.90;
    filter: brightness(0.95);
}

.wetstage3 {
    opacity: 0.75;
    filter: brightness(0.90);
}


code:
#divsex {
    position: relative;
    display: inline-block;
    height: 256px;
    width: 768px;

    & > * {
        position: absolute;
        left: 256px;
        top: 0;
    }
}

#divandroidsex {
    position: relative;
    display: inline-block;
    height: 256px;
    width: 256px;
    left: -256px;

    & > * {
        position: absolute;
        left: 256px;
        top: 0;
    }
}

#closepenis {
    &, #divsex & {
        position: absolute;
        left: 321px;
        top: -30px;
        z-index: 250;
    }
}

#closeanus {
    &, #divsex & {
        position: absolute;
        left: 451px;
        top: -30px;
        z-index: 250;
    }
}

#closevagina {
    &, #divsex & {
        position: absolute;
        left: 386px;
        top: -30px;
        z-index: 250;
    }
}

#divandroidxray {
    position: relative;
    display: inline-block;
    height: 256px;
    width: 256px;
}

#xrayvaginal {
    &, #divsex & {
        position: absolute;
        left: 0;
        top: 0;
        z-index: 3;
    }
}

#xrayvaginalcum {
    &, #divsex & {
        position: absolute;
        left: 0;
        top: 0;
        z-index: 4;
    }
}

#xraypenile {
    &, #divsex & {
        position: absolute;
        left: 0;
        top: 0;
        z-index: 2;
    }
}

#xrayanal {
    &, #divsex & {
        position: absolute;
        left: 512px;
        top: 0;
        z-index: 1;
    }
}

#xrayandroidanal {
    &, #divsex & {
        position: relative;
        left: 0;
        top: -150px;
        z-index: 1;
    }
}


code:
@import "mixins";
@import "variables";

@import "colors";

@import "ui";
@import "numberbox";
@import "wet";
@import "layers";
@import "xray";
@import "hair";
@import "eyes";
@import "clothing";

@import "animation_base";
@import "animation";

@import "map";


code:
export default [{
    type: 'outfit',
    states: {
        100: {
            upper: 'img/upperclothes/sundressupper/sundressupperred.png'
        }
    }
}];


code:
export default [{
    type: 'upper',
    id: 't_shirt',
    name: 'T-Shirt',
    states: {
        100: {
            sprite: 'GOOD_SPRITE'
        },
        70: {
            sprite: 'TORN_WHATEVER'
        },
        //[...]
    },
    variants: {
        'red': {
            class: 'red'
        },
        'blue': {
            class: 'red'
        },
        'black_with_heart': {
            states: {
                100: {
                    sprite: 'GOOD_SPRITE_WITH_HEART'
                },
                70: {
                    sprite: 'TORN_WHATEVER_WITH_HEART'
                },
            },
            combat: {
                'hips': 'special',
                'neck': 'sprite',
                'thigs': 'goes',
                'tummy': 'here'
            }
        }
    },
    combat: {
        'hips': 'whatever',
        'neck': 'sprite',
        'thigs': 'sprite',
        'tummy': 'sprite'
    },
    integrity: {
        max: 100,
        current: 100 // or saved
    },
    fabric_strength: 100, // dmg - fabric strength (min 1 dmg?)
    reveal: 100
}];


code:
class Clothing {

}

export default Clothing;


code:
import {checkForDaylightExposure} from "./PlayerStates";
import {colourContainerClasses} from "../Macros/Misc";
import "../Macros/Numberpool";
import {upperIntegrity, lowerIntegrity, underIntegrity} from "../Macros/Clothing";
import {macroSetup} from "./Utilities";
import GameMap from "./GameMap";
import "./Numberify";
import {ImportDefaultGameState, GetNpc} from "../Macros/GameState";
import "../Update/SaveUpdater";

class Game {
    constructor() {
        this.initState();
        this.initMacros();
        this.initTwineHelper();

        console.log("Degrees of Lewdity initialized.");
    }

    initMacros() {
        macroSetup('display_map', GameMap);

        macroSetup('underintegrity', underIntegrity, true, true);
        macroSetup('lowerintegrity', lowerIntegrity, true, true);
        macroSetup('upperintegrity', upperIntegrity, true, true);
        macroSetup('init_game_state', ImportDefaultGameState, false, true);
    }

    initState() {
        Config.history.controls = false;
        Config.history.maxStates = 20;

        State.initPRNG();

        $(document).ready(() => {
            $('#sidetooltip').appendTo('body');
        });
    }

    initTwineHelper() {
        window.DoL = {
            colourContainerClasses: colourContainerClasses,
            daylightExposure: checkForDaylightExposure
        };
    }

    static getGameVersion() {
        return '1.25.0';
    }
}

export default Game;


code:
import Maps from "../Maps/All";
import {get} from "./Utilities";
import _ from 'lodash';

class GameMap {
    macro = null;
    map = null;
    locationID = null;

    location = null;

    renderedLocations = {};

    constructor(macro) {
        this.macro = macro;
        const [locationID, mapID] = this.macro.args;

        this.map = GameMap.getMapById(mapID);
        this.locationID = locationID;
        if (this.map === null)
            this.macro.error(`Invalid map id specified (${mapID}) or default map is not set!`);

        this.validateLocation();
    }

    validateLocation() {
        if (typeof this.locationID !== "string")
            this.macro.error("Invalid location id type");

        for (let m of this.map.map.locations) {
            if (this.locationID === m.id) {
                this.location = m;
                break;
            }
        }

        if (this.location === null)
            this.macro.error(`Invalid location id specified (${this.locationID}), not found in map (${this.map.mapID})`);
    }

    static getDefaultMap() {
        for (let m of Maps)
            if (typeof m.default !== "undefined" && m.default)
                return m;

        return null;
    }

    static getMapById(mapID) {
        if (typeof mapID === "undefined")
            return GameMap.getDefaultMap();

        for (let m of Maps)
            if (m.mapID === mapID)
                return m;

        return null;
    }

    renderLocation(loc, map) {
        const location = $('<div>').attr('id', 'location-' + loc.id).css({
            left: loc.x,
            top: loc.y
        }).addClass('location-single');

        $(location).append(
            $('<img>').addClass('location-sprite').attr('src', get(loc.sprite))
        );

        if (typeof loc.light !== "undefined")
            $(location).append(
                $('<img>').addClass('location-light').attr('src', get(loc.light))
            );

        if (typeof loc.active === "undefined") {
            $(location).append(
                $('<div>').addClass('location-active-trigger').css({
                    left: 0,
                    top: 0,
                    width: '100%',
                    height: '100%'
                })
            );
        }
        else {
            const active = get(loc.active);

            const left = active.x;
            const top = active.y;
            const width = active.width;
            const height = active.height;

            $(location).append(
                $('<div>').addClass('location-active-trigger').addClass('location-active-trigger-custom').css({
                    left,
                    top,
                    width,
                    height
                })
            );
        }

        if (this.locationID === loc.id) {
            $(location).addClass('location-current');
        }

        this.renderedLocations[loc.id] = location;

        $(map).append(location);
    }

    attachLinks() {
        let currentLocation = null;
        for (let l of this.map.map.locations) {
            if (this.locationID === l.id) {
                currentLocation = l;
                break;
            }
        }

        let links = get(currentLocation.links);
        let locations = _.keyBy(_.cloneDeep(this.map.map.locations), o => o.id);

        for (let link of links) {
            if (typeof this.renderedLocations[link] === "undefined") {
                console.warn(`Unknown selected for the current location (${link})`);
                continue;
            }

            if (typeof locations[link].canBeVisited !== "undefined") {
                if (!get(locations[link].canBeVisited)) {
                    delete locations[link];
                    continue;
                }
            }

            $(this.renderedLocations[link]).addClass('link-active');

            const activeTrigger = $(this.renderedLocations[link]).find('.location-active-trigger');

            $(activeTrigger).on('click', () => {
                const myLocation = _.keyBy(this.map.map.locations, o => o.id)[link];
                if (typeof myLocation.passage === "undefined") {
                    console.warn(`Passage for link ${link} is not defined.`);
                    return;
                }

                const passage = get(myLocation.passage);
                if (typeof passage !== "string") {
                    console.error(`Passage returned for link ${link} is not a string.`);
                    return;
                }

                const timePass = get(myLocation.timePass);
                if (typeof timePass === "number")
                    Wikifier.wikifyEval(`<<pass ${timePass}>>`);

                Engine.play(passage);
            }).on('mouseenter', (e) => {
                $(e.currentTarget).closest('.location-single').addClass('location-is-hovered');
            }).on('mouseleave', (e) => {
                $(e.currentTarget).closest('.location-single').removeClass('location-is-hovered');
            });
            delete locations[link];
        }

        for (let link in locations) {
            if (link === this.locationID)
                continue; // current location is in this list, no need to pollute it with the class

            $(this.renderedLocations[link]).addClass('link-inactive');
        }
    }

    renderMap() {
        const container = $('<div>').attr('id', 'map-display-container').addClass('map-id-' + this.map.mapID);
        $(container).append(
            $('<img>').attr('id', 'background-layer').attr('src', get(this.map.map.background))
        );

        for (let l of this.map.map.locations)
            this.renderLocation(l, container);

        this.attachLinks();

        return container;
    }

    run() {
        jQuery(this.macro.output).append(this.renderMap());
    }
}

export default GameMap;


code:
const disableNumberifyInVisibleElements = [
    '#passage-hairdressers-seat',
    '#passage-start',
    '#passage-wardrobe',
    '#passage-cheats',
    '#passage-testing-room'
];

let currentLinks = [];

function getPrettyKeyNumber(counter) {
    var str = "";

    if (counter > 30)
        str = "Ctrl + ";
    else if (counter > 20)
        str = "Alt + ";
    else if (counter > 10)
        str = "Shift + ";

    if (counter % 10 === 0)
        str += "0";
    else if (counter < 10)
        str += counter;
    else {
        let c = Math.floor(counter / 10);
        str += (counter - (10 * c)).toString();
    }

    return str;
}

$(document).on(':passagerender', (ev) => {
    currentLinks = [];

    for (var i = 0; i < disableNumberifyInVisibleElements.length; i++) {
        if ($(ev.content).find(disableNumberifyInVisibleElements[i]).length || $(ev.content).is(disableNumberifyInVisibleElements[i]))
            return; // simply skip this render
    }

    currentLinks = $(ev.content).find(".link-internal"); // wanted to use .macro-link, but wardrobe and something else doesn't get selected, lmao

    $(currentLinks).each(function(i, el) {
        $(el).html("(" + getPrettyKeyNumber(i + 1) + ") " + $(el).html());
    });
});

$(document).on('keyup', (ev) => {
    if ((ev.keyCode >= 48 && ev.keyCode <= 57) || (ev.keyCode >= 96 && ev.keyCode <= 105)) {
        const fixedKeyIndex = (ev.keyCode < 60 ? ev.keyCode - 48 : ev.keyCode - 96);

        let requestedLinkIndex = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8][fixedKeyIndex];

        if (ev.ctrlKey)
            requestedLinkIndex += 30;
        else if (ev.altKey)
            requestedLinkIndex += 20;
        else if (ev.shiftKey)
            requestedLinkIndex += 10;

        if ($(currentLinks).length >= requestedLinkIndex + 1)
            $(currentLinks[requestedLinkIndex]).click();
    }
});


code:
export function checkForDaylightExposure() {
    const exposedState = parseInt(State.variables.exposed);
    const dayState = State.variables.daystate;

    return exposedState >= 1 && dayState !== "night";
}


code:
export function macroSetup(tag, target, output = false, asFunction = false, functionTarget = 'run') {
    Macro.add(tag, {
        isWidget: false,
        handler: function() {
            const self = this;
            let returnValue = (asFunction ? target(self) : new target(self)[functionTarget]());

            if (output)
                $(self.output).wiki(returnValue);
        }
    });
}

export function get(target) {
    if (typeof target === "function")
        return target();

    return target;
}


code:
export function underIntegrity() {
    let output = '';
    const underIntegrity = State.variables.underintegrity;
    const underIntegrityMax = State.variables.underintegritymax;

    if (underIntegrityMax !== 0) {
        if (underIntegrity <= (underIntegrityMax / 10) * 2)
            output = 'tattered \t';
        else if (underIntegrity <= (underIntegrityMax / 10) * 5)
            output = 'torn \t';
        else if (underIntegrity <= (underIntegrityMax / 10) * 9)
            output = 'frayed \t';
    }

    return output;
}

export function lowerIntegrity() {
    let output = '';
    const lowerIntegrity = State.variables.lowerintegrity;
    const lowerIntegrityMax = State.variables.lowerintegritymax;

    if (lowerIntegrityMax !== 0) {
        if (lowerIntegrity <= (lowerIntegrityMax / 10) * 2)
            output = 'tattered \t';
        else if (lowerIntegrity <= (lowerIntegrityMax / 10) * 5)
            output = 'torn \t';
        else if (lowerIntegrity <= (lowerIntegrityMax / 10) * 9)
            output = 'frayed \t';
    }

    return output;
}

export function upperIntegrity() {
    let output = '';
    const upperIntegrity = State.variables.upperintegrity;
    const upperIntegrityMax = State.variables.upperintegritymax;

    if (upperIntegrityMax !== 0) {
        if (upperIntegrity <= (upperIntegrityMax / 10) * 2)
            output = 'tattered \t';
        else if (upperIntegrity <= (upperIntegrityMax / 10) * 5)
            output = 'torn \t';
        else if (upperIntegrity <= (upperIntegrityMax / 10) * 9)
            output = 'frayed \t';
    }

    return output;
}


code:
import {GameState, GameVersion, NpcShared, NpcHuman, NpcBeast, NpcSwarm, NpcTentacles, NpcVore} from "../Update/Versions";
import * as deepmerge from "deepmerge";

const NpcMap = {
    'man': NpcHuman,
    'beast': NpcHuman,
    'vore': NpcVore,
    'tentacles': NpcTentacles,
    'swarm': NpcSwarm
};

const SugarCubeVariables = [
    'passage',
    'tags'
];

export function ImportDefaultGameState() {
    for (let key in State.variables)
        if (SugarCubeVariables.indexOf(key) === -1)
            delete State.variables[key];

    for (let key in GameState)
        State.variables[key] = GameState[key];

    State.variables._gameVersion = GameVersion;

    console.log("Default game state set.");
}

export function GetNpc(npcType = null) {
    if (!NpcMap.hasOwnProperty(npcType))
        throw new Error(`No valid npc type specified (${npcType}`);

    return deepmerge.merge(NpcShared, NpcMap[npcType]);
}


code:
Macro.add('time', {
    handler: () => {
        const time = State.variables.time;
        const hour = Math.floor(time/60);

        let dayState = '';

        if (hour < 6)
            dayState = 'night';
        else if (hour < 9)
            dayState = 'dawn';
        else if (hour < 18)
            dayState = 'day';
        else if (hour < 21)
            dayState = 'dusk';
        else
            dayState = 'night';

        State.variables.hour = hour;
        State.variables.daystate = dayState;
    }
});

export function colourContainerClasses() {
    const V = State.variables;
    return 'hair-'  + (V.haircolour||'').replace(/ /g,'-') +
        ' ' + 'eye-'   + (V.eyecolour||'').replace(/ /g,'-') +
        ' ' + 'upper-' + (V.uppercolour||'').replace(/ /g,'-') +
        ' ' + 'lower-' + (V.lowercolour||'').replace(/ /g,'-') +
        ' ' + 'under-' + (V.undercolour||'').replace(/ /g,'-')
}


code:
Macro.add("numberbox", {
    handler: function() {
        function validateAndApply(el, addend) {
            var curValue = Math.trunc(Wikifier.getValue(varName)),
                newValue = Math.trunc(el.value),
                newPoolValue = null;
            if (Number.isNaN(newValue) || !Number.isFinite(newValue)) return el.value = curValue, !1;
            if (null != addend && (newValue += addend), newValue < minValue ? newValue = minValue : newValue > maxValue && (newValue = maxValue), null !== pool) {
                var poolValue = pool.get(),
                    delta = (newValue - curValue) * poolCost;
                delta < 0 ? newPoolValue = poolValue - delta : delta > 0 && poolValue >= poolCost ? (poolValue < delta && (newValue = curValue + Math.trunc(poolValue / poolCost), delta = poolValue - poolValue % poolCost), newPoolValue = poolValue - delta) : newValue = curValue
            }
            return Wikifier.setValue(varName, newValue), el.value = newValue, null !== newPoolValue && pool.set(newPoolValue), !0
        }
        var _this = this;
        if (this.args.length < 4) {
            var errors = [];
            return this.args.length < 1 && errors.push("variable name"), this.args.length < 2 && errors.push("default value"), this.args.length < 3 && errors.push("min value"), this.args.length < 4 && errors.push("max value"), this.error("no " + errors.join(" or ") + " specified")
        }
        if ("string" != typeof this.args[0]) return this.error("variable name argument is not a string");
        var varName = this.args[0].trim();
        if ("$" !== varName[0] && "_" !== varName[0]) return this.error('variable name "' + this.args[0] + '" is missing its sigil ($ or _)');
        var varId = Util.slugify(varName),
            defValue = Number(this.args[1]),
            minValue = Number(this.args[2]),
            maxValue = Number(this.args[3]),
            poolCost = 1,
            autofocus = !1;
        if (this.args.length > 5 ? (poolCost = Number(this.args[4]), autofocus = "autofocus" === this.args[5]) : this.args.length > 4 && ("autofocus" === this.args[4] ? autofocus = !0 : poolCost = Number(this.args[4])), Number.isNaN(defValue) || !Number.isFinite(defValue) || Math.trunc(defValue) !== defValue) return this.error("default value (" + this.args[1] + ") is not a whole number");
        if (Number.isNaN(minValue) || !Number.isFinite(minValue) || Math.trunc(minValue) !== minValue) return this.error("min value (" + this.args[2] + ") is not a whole number");
        if (Number.isNaN(maxValue) || !Number.isFinite(maxValue) || Math.trunc(maxValue) !== maxValue) return this.error("max value (" + this.args[3] + ") is not a whole number");
        if (Number.isNaN(poolCost) || !Number.isFinite(poolCost) || Math.trunc(poolCost) !== poolCost || poolCost <= 0) return this.error("pool cost (" + this.args[4] + ") is not a whole number greater than zero");
        if (defValue < minValue) return this.error("default value (" + this.args[1] + ") is less than min value (" + this.args[2] + ")");
        if (defValue > maxValue) return this.error("default value (" + this.args[1] + ") is greater than max value (" + this.args[3] + ")");
        var pool = function() {
            var parent = _this.contextSelect(function(ctx) {
                return "numberpool" === ctx.name
            });
            return null !== parent && parent.hasOwnProperty("pool") ? parent.pool : null
        }();
        Config.debug && this.debugView.modes({
            block: !0
        });
        var $elControl = jQuery(document.createElement("div")),
            $elInput = jQuery(document.createElement("input"));
        $elControl.attr("id", this.name + "-body-" + varId).addClass("macro-" + this.name).appendTo(this.output), jQuery(document.createElement("button")).attr({
            id: this.name + "-minus-" + varId,
            tabindex: 0
        }).text("").on("click", function() {
            return validateAndApply($elInput[0], -1)
        }).appendTo($elControl), $elInput.attr({
            id: this.name + "-input-" + varId,
            name: this.name + "-input-" + varId,
            type: "text",
            pattern: "\\d+",
            tabindex: 0
        }).on("change", function() {
            validateAndApply(this)
        }).on("keypress", function(ev) {
            13 === ev.which && (ev.preventDefault(), $elInput.trigger("change"))
        }).appendTo($elControl), jQuery(document.createElement("button")).attr({
            id: this.name + "-plus-" + varId,
            tabindex: 0
        }).text("").on("click", function() {
            return validateAndApply($elInput[0], 1)
        }).appendTo($elControl), $elInput.val(defValue), validateAndApply($elInput[0]), autofocus && ($elInput.attr("autofocus", "autofocus"), postdisplay["#autofocus:" + $elInput.attr("id")] = function(task) {
            delete postdisplay[task], setTimeout(function() {
                return $elInput.focus()
            }, Engine.minDomActionDelay)
        })
    }
});

Macro.add("numberpool", {
    tags: ["onchange"],
    handler: function() {
        if (0 === this.args.length) return this.error("no variable name specified");
        if (this.payload.length > 2) return this.error("multiple <<onchange>> sections specified");
        if ("string" != typeof this.args[0]) return this.error("variable name argument is not a string");
        var varName = this.args[0].trim();
        if ("$" !== varName[0] && "_" !== varName[0]) return this.error('variable name "' + this.args[0] + '" is missing its sigil ($ or _)');
        var curValue = Wikifier.getValue(varName);
        if ("number" != typeof curValue || Number.isNaN(curValue) || !Number.isFinite(curValue)) return this.error("pool value is not a number");
        var varId = Util.slugify(varName);
        TempState.hasOwnProperty(this.name) || (TempState[this.name] = {}), TempState[this.name].hasOwnProperty(varId) || (TempState[this.name][varId] = 0), Object.defineProperty(this, "pool", {
            value: Object.defineProperties({}, {
                get: {
                    value: function() {
                        return Wikifier.getValue(varName)
                    }
                },
                set: {
                    value: function(content) {
                        return function(value) {
                            var curValue = Wikifier.getValue(varName);
                            value !== curValue && (Wikifier.setValue(varName, value), content && new Wikifier(null, content))
                        }
                    }(this.payload.length > 1 ? this.payload[1].contents.trim() : "")
                }
            })
        }), jQuery(document.createElement("div")).attr("id", this.name + "-" + varId + "-" + TempState[this.name][varId]++).addClass("macro-" + this.name).wiki(this.payload[0].contents.replace(/^\n/, "")).appendTo(this.output)
    }
});

Macro.add("numberslider", {
    handler: function() {
        function stepValidate(value) {
            if (fracDigits > 0) {
                var ma = Number(minValue + "e" + fracDigits),
                    sa = Number(stepValue + "e" + fracDigits),
                    _va = Number(value + "e" + fracDigits) - ma;
                return Number(_va - _va % sa + ma + "e-" + fracDigits)
            }
            var va = value - minValue;
            return va - va % stepValue + minValue
        }

        function validateAndApply(el) {
            var curValue = Wikifier.getValue(varName),
                newValue = Number(el.value),
                newPoolValue = null;
            if (Number.isNaN(newValue) || !Number.isFinite(newValue)) return el.value = curValue, !1;
            if (newValue = stepValidate(newValue), newValue < minValue ? newValue = minValue : newValue > maxValue && (newValue = maxValue), null !== pool)
                if (fracDigits > 0) {
                    var pa = Number(pool.get() + "e" + fracDigits),
                        ca = Number(curValue + "e" + fracDigits),
                        na = Number(newValue + "e" + fracDigits),
                        delta = na - ca;
                    pa < delta && (na -= delta - pa, delta = na - ca, newValue = Number(na + "e-" + fracDigits)), newPoolValue = Number(pa - delta + "e-" + fracDigits)
                } else {
                    var poolValue = pool.get(),
                        _delta = newValue - curValue;
                    poolValue < _delta && (newValue -= _delta - poolValue, _delta = newValue - curValue), newPoolValue = poolValue - _delta
                }
            return Wikifier.setValue(varName, newValue), el.value = newValue, null !== newPoolValue && pool.set(newPoolValue), !0
        }
        var _this2 = this;
        if (this.args.length < 5) {
            var errors = [];
            return this.args.length < 1 && errors.push("variable name"), this.args.length < 2 && errors.push("default value"), this.args.length < 3 && errors.push("min value"), this.args.length < 4 && errors.push("max value"), this.args.length < 5 && errors.push("step value"), this.error("no " + errors.join(" or ") + " specified")
        }
        if ("string" != typeof this.args[0]) return this.error("variable name argument is not a string");
        var varName = this.args[0].trim();
        if ("$" !== varName[0] && "_" !== varName[0]) return this.error('variable name "' + this.args[0] + '" is missing its sigil ($ or _)');
        var varId = Util.slugify(varName),
            defValue = Number(this.args[1]),
            minValue = Number(this.args[2]),
            maxValue = Number(this.args[3]),
            stepValue = Number(this.args[4]),
            autofocus = this.args.length > 5 && "autofocus" === this.args[5];
        if (Number.isNaN(defValue) || !Number.isFinite(defValue)) return this.error("default value (" + this.args[1] + ") is not a number");
        if (Number.isNaN(minValue) || !Number.isFinite(minValue)) return this.error("min value (" + this.args[2] + ") is not a number");
        if (Number.isNaN(maxValue) || !Number.isFinite(maxValue)) return this.error("max value (" + this.args[3] + ") is not a number");
        if (Number.isNaN(stepValue) || !Number.isFinite(stepValue) || stepValue <= 0) return this.error("step value (" + this.args[4] + ") is not a number greater than zero");
        if (defValue < minValue) return this.error("default value (" + this.args[1] + ") is less than min value (" + this.args[2] + ")");
        if (defValue > maxValue) return this.error("default value (" + this.args[1] + ") is greater than max value (" + this.args[3] + ")");
        var fracDigits = function() {
            var str = String(stepValue),
                pos = str.lastIndexOf(".");
            return pos === -1 ? 0 : str.length - pos - 1
        }();
        if (stepValidate(maxValue) !== maxValue) return this.error("max value (" + this.args[3] + ") is not a multiple of the step value (" + this.args[4] + ") plus the min value (" + this.args[2] + ")");
        var pool = function() {
            var parent = _this2.contextSelect(function(ctx) {
                return "numberpool" === ctx.name
            });
            return null !== parent && parent.hasOwnProperty("pool") ? parent.pool : null
        }();
        Config.debug && this.debugView.modes({
            block: !0
        });
        var $elControl = jQuery(document.createElement("div")),
            $elInput = jQuery(document.createElement("input")),
            $elValue = void 0,
            showValue = void 0;
        $elControl.attr("id", this.name + "-body-" + varId).addClass("macro-" + this.name).appendTo(this.output), $elInput.attr({
            id: this.name + "-input-" + varId,
            name: this.name + "-input-" + varId,
            type: "range",
            min: minValue,
            max: maxValue,
            step: stepValue,
            tabindex: 0
        }).on("change input." + Util.slugify(this.name), function() {
            validateAndApply(this), "function" == typeof showValue && showValue()
        }).on("keypress", function(ev) {
            13 === ev.which && (ev.preventDefault(), $elInput.trigger("change"))
        }).appendTo($elControl), !Browser.isIE || Browser.ieVersion > 9 ? ($elValue = jQuery(document.createElement("span")).attr("id", this.name + "-value-" + varId).appendTo($elControl), showValue = function() {
            $elValue.text(Number($elInput.val()).toFixed(fracDigits))
        }) : $elInput.off("input." + Util.slugify(this.name)), $elInput.val(defValue), validateAndApply($elInput[0]), "function" == typeof showValue && showValue(), autofocus && ($elInput.attr("autofocus", "autofocus"), postdisplay["#autofocus:" + $elInput.attr("id")] = function(task) {
            delete postdisplay[task], setTimeout(function() {
                return $elInput.focus()
            }, Engine.minDomActionDelay)
        })
    }
});


code:
import Overworld from "./Overworld";

export default [{
    map: Overworld,
    mapID: 'overworld',
    default: true
}];


code:
import {checkForDaylightExposure} from "../Game/PlayerStates";

export default {
    background: 'img/map_placeholders/map_placeholder.png',
    locations: [{
        id: 'domus_street',
        passage: 'Domus Street',
        timePass: 5,
        x: 35,
        y: 118,
        name: 'Domus',
        prefix: 'Street',
        suffix: '',
        sprite: 'img/map_placeholders/building_placeholder.png',
        light: 'img/map_placeholders/placeholder_light.png',
        canBeVisited: () => !checkForDaylightExposure(),
        links: [
            'barb_street',
            'danube_street',
            'residential_alleyways'
        ]
    }, {
        id: 'barb_street',
        passage: 'Barb Street',
        timePass: 5,
        x: 135,
        y: 30,
        name: 'Barb',
        prefix: 'Street',
        suffix: '',
        sprite: 'img/map_placeholders/building_placeholder.png',
        light: 'img/map_placeholders/placeholder_light.png',
        canBeVisited: () => !checkForDaylightExposure(),
        links: [
            'domus_street',
            'connudatus_street',
            'residential_alleyways'
        ],
        active: {
            x: 15,
            y: 15,
            width: 15,
            height: 15
        }
    }, {
        id: 'residential_alleyways',
        passage: 'Residential alleyways',
        timePass: 5,
        x: 115,
        y: 110,
        name: 'Residential Alleyways',
        prefix: '',
        sprite: 'img/map_placeholders/building_placeholder.png',
        light: 'img/map_placeholders/placeholder_light.png',
        links: [
            'domus_street',
            'barb_street',
            'danube_street',
            'connudatus_street'
        ]
    }, {
        id: 'danube_street',
        passage: 'Danube Street',
        timePass: 5,
        x: 137,
        y: 207,
        name: 'Danube',
        prefix: 'Street',
        suffix: '',
        sprite: 'img/map_placeholders/building_placeholder.png',
        light: 'img/map_placeholders/placeholder_light.png',
        canBeVisited: () => !checkForDaylightExposure(),
        links: [
            'domus_street',
            'connudatus_street',
            'residential_alleyways'
        ]
    }, {
        id: 'connudatus_street',
        passage: 'Connudatus Street',
        timePass: 5,
        x: 210,
        y: 100,
        name: 'Connudatus',
        prefix: 'Street',
        suffix: '',
        sprite: 'img/map_placeholders/building_placeholder.png',
        light: 'img/map_placeholders/placeholder_light.png',
        canBeVisited: () => !checkForDaylightExposure(),
        links: [
            'barb_street',
            'danube_street',
            'residential_alleyways'
        ]
    }] /* First cluster of 5 */
};


code:
export const GameVersion = '1.26.1';

// Save update function
export default (variables) => {

};

export const GameState = {
    settings: {
        text_only: false, // $textonly
        enable_images: true, // $images
        debug: true, // $debug
        numberify: true, // $numberify_enable
        devmode: false, // $dev
        gamemode: "normal", /// $gamemode

        intro: true, // $intro
        tutorial: false, // $tutorial

        content: {
            // content toggles here
        }
    },

    player: {
        skills: {
            seduction: 0,
            vaginal: 0,
            penile: 0,
            anal: 0,
            oral: 0,
            hand: 0,
            feet: 0,
            chest: 0,
            thigh: 0,
            bottom: 0,
        },
        stats: {
            control: {
                max: 1000, // $controlmax
                current: 1000 // $control
            },
            trauma: {
                max: 5000, //$traumamax
                current: 0 // $trauma
            },
            stress: {
                max: 10010, // $stressmax
                current: 0 // $stress
            },
            physique: {
                max: 20000, // $physiquemax
                current: 3500 // $physique
            },
            arousal: {
                max: 10000, // $arousalmax
                current: 0 // $arousal ?
            },
            pain: {
                max: 1000, // $painmax ?
                current: 0 // $pain
            },
            fatigue: {
                max: 2000, // $tirednessmax
                current: 0 // $tiredness
            },
            beauty: {
                max: 10000, // $beautymax
                current: 100 // $beauty
            }
        },
        special_stats: {
            purity: 1000, // $purity
            submissive: 1000, // $submissive
            assertive: 0, // $assertive
            assertive_action: "trauma", // $assertiveaction
            awareness: {
                current: 0, // $awareness
                level: 0 // $awarelevel
            },
            fame: 0, // $fame
            fame_exhibitionism: 0, // $fameexhibitionism
            fame_prostitution: 0, // $fameprostitution
            fame_bestiality: 0, // $famebestiality
            fame_sex: 0, // $famesex
            fame_rape: 0, // $famerape
            fame_good: 0, // $famegood
            fame_business: 0, // $famebusiness
            fame_park: 0, // $famepark

            transformation: {
                wolf: {
                    build: 0, // $wolfbuild
                    girl: false // $wolfgirl
                }
            },

            oxygen: {
                max: 1200, // $oxygenmax
                current: 1200 // $oxygen
            }
        },
        possesions: {
            money: 500, // $money
            items: {
                clothing: {

                }
            }
        },
        looks: {
            development_level: 0, // $devlevel
            gender_appearance: '', // $playergenderappearance

            hair: {
                length: 200, // $hairlength
                color: '', // $haircolor
            }
        },
        school: {
            deliquency: 10, /// $deliquency

            cool: 120, // $cool
            cool_max: 400, // $coolmax

            school: 800, // $school

            math: 200, // $maths
            english: 200, // $english
            science: 200, // $science
            history: 200 // $history
        },
        data: {
            birth_month: "september", // $birthmonth
            birth_day: 3, // $birthday
            name: "It's you.",
            gender: 'f', // $playergender
            background: 'waif', // $background
            breast_size: '', // $breastsize
        }
    },

    location: {
        time: 420, // $time
        day: 0, // $days
        week_day: 1, // $weekday
        month: "september", // $month
        month_day: 4, // $monthday
        year: 2018, // $year
        map_id: 'overworld'
    },

    state: {
        combat: false, // $combat
        skill_level_up: [], // contains the name of the skill that leveled up so things can display after combat

        water: false, // if in water - $water
        underwater_time: 0, // $underwatertime
        underwater: 0, // if underwater - $underwater

        player: {
            mind: {
                /*Disables you if 1.*/
                panic_paralysis: false,
                panic_violence: false,
                trance: false,
                /*Builds during consensual, provides benefit at end.*/
                assertive: false,
                /*Set to 1 if NPC is successfully asked not to go ham on the specified body part.*/
                no_vaginal: false,
                no_anal: false,
                no_penile: false,
                /*Trauma and stress at encounter's start.*/
                trauma_saved: 0,
                stress_saved: 0,
                /*Trauma and stress gained during encounter.*/
                trauma_gain: 0,
                stress_gain: 0,
            },

            body: {
                /*Set to a string indicating which body part it is occupied by, such as "penis" or "lefthand"*/
                vagina_use: 0,
                penis_use: 0,
                mouth_use: 0,
                anus_use: 0,
                thigh_use: 0,
                bottom_use: 0,
                feet_use: 0,
                left_arm: 0,
                right_arm: 0,
                chest_use: 0,
                head: 0,
                left_let: 0,
                right_leg: 0,
                breast_use: 0,
                left_nipple: 0,
                right_nipple: 0,
                /*Set to a string indicating both which body part it is occupied by, and the type of "hold" that body part has on in. Such as "anusentrance" or "anusimminent"*/
                vagina_state: 0,
                penis_state: 0,
                anus_state: 0,
                mouth_state: 0,
                left_arm_state: 0,
                right_arm_state: 0,
                feet_state: 0,
                /*More stuff I hope is self-explanatory*/
                penis_fucked: false,
                vagina_fucked: false,
                pull_away: false,
                orgasm_current: 0,
                water_wash: 0,
            }
        },

        encounter: {
            state: {
                /*Makes actions easier.*/
                enemy_trust: 0,
                /*Encounter ends if this reaches 0.*/
                enemy_health: 0,
                enemy_health_max: 0,
                /*Encounter ends if this reaches max.*/
                enemy_arousal: 0,
                enemy_arousal_max: 0,
                /*Impacts NPC behaviour during encounters.*/
                enemy_anger: 0,
                enemy_anger_max: 0,
            },

            audience: {
                /*This is the audience for regular encounters, and is separate to dance audiences.*/
                /*What NPCs are in the audience.*/
                audience_camera: [
                    // description of the person
                ],/*audience_camera_2 - 6 too*/
                /*Forces all the audience to be boys.*/
                audience_force_boy: false,
                /*Forces all the audience to be girls.*/
                audience_force_girl: false,
            }
        }
    },

    playthrough_stats: {
        rape: 0, // $rapestat
        beast_rape: 0, // $beastrapestat
        tentacle_rape: 0, // $tentaclerapestat
        molested: 0, // $moleststat

        vaginal: 0, // $vaginalstat
        vaginal_ejac: 0, // $vaginalejacstat
        vaginal_entrance_ejac: 0, // $vaginalentranceejacstat
        anal: 0, // $analstat
        anal_ejac: 0, // $analejacstat
        oral: 0, // $oralstat
        oral_ejac: 0, // $oralejacstat
        hand: 0, // $ohandstat
        hand_ejac: 0, // $handejacstat
        feet: 0, // $feetstat
        feet_ejac: 0, // $feetejacstat
        thigh: 0, // $thighstat
        thigh_ejac: 0, // $thighejacstat
        bottom: 0, // $bottomstat
        bottom_ejac: 0, // $bottomejacstat
        penile: 0, // $penilestat
        penile_ejac: 0, // $penileejacstat
        face_ejac: 0, // $faceejacstat
        chest: 0, // $cheststat
        chest_ejac: 0, // $chestejacstat
        hair_ejac: 0, // $hairyejacstat
        tummy_ejac: 0, // $tummyejacstat
        neck_ejac: 0, // $neckejacstat
        ejac: 0, // $ejacstat
        orgasm: 0, // $orgasmstat

        clothes_stripped: 0, // $clothesstripstat
        clothes_ruined: 0, // $clothesruinstat

        hit: 0, // $hitstat
        attack: 0, // $attackstat

        prostitution: 0, // $prostitutionstat
        forced_prostitution: 0, // $forcedprostitutionstat

        tables_served: 0, // $tablesservedstat
        parasite: 0, // $parasitestat
        pass_out: 0, // $passoutstat

        masturbation: 0, // $masturbationstat
        masturbation_orgasm: 0, // $masturbationorgasmstat
        masturbation_time: 0, // $masturbationtimestat

        dance: 0, // $dancestat
        pull_away: 0, // $pullaway

        no_vaginal: 0, // $novaginal
        no_anal: 0, // $noanal
        no_penile: 0, // $nopenile

        vaginal_chastity: 0, // $vaginalchastity
        anal_chastity: 0, // $analchastity
        penile_chastity: 0, // $penilechastity
        drinks_served: 0, // $drinksservedstat
    }
};

export const NpcShared = {
    state: {
        npc: null, // npc name
        type: null, // npc type "man", "beast", "swarm", "parasite", "tentacle"
        strength: 0, // unused for now
    },
    specific_speech: {
        /*Speech variables that disappear after the current NPC's turn, meaning other NPCs won't use them.*/
        /* I don't even WANNA touch this shit */
        speech_orgasm_weak_cum_count: 0,
        speech_orgasm_no_cum_count: 0,
        speech_orgasm_count: 0,
        speech_orgasm_repeat: 0,
        speech_crossdress_angry: 0,
        speech_crossdress_aroused: 0,
        speech_crosdress_shock: 0,
        speech_crossdress_disappointed: 0,
        speech_cum: 0,
        speech_penis_penetrated: 0,
        speech_vagina_penetrated: 0,
        speech_anus_penetrated: 0,
        speech_mouth_penetrated: 0,
        speech_other_anus_penetrated: 0,
        speech_vagina_imminent: 0,
        speech_penis_imminent: 0,
        speech_anus_imminent: 0,
        speech_mouth_imminent: 0,
        speech_other_anus_imminent: 0,
        speech_vagina_entrance: 0,
        speech_penis_entrance: 0,
        speech_anus_entrance: 0,
        speech_mouth_entrance: 0,
        speech_other_anus_entrance: 0,
        speech_vagina_withhold: 0,
        speech_anus_withhold: 0,
        speech_penis_withold: 0,
        speech_other_anus_withold: 0,
        speech_vagina: 0,
        speech_penis: 0,
        speech_anus: 0,
        speech_vagina_mouth: 0,
        speech_vagina_vagina: 0,
        speech_beat: 0,
        speech_hit: 0,
        speech_throat: 0,
        speech_vagina_foot: 0,
        speech_penis_foot: 0,
        speech_chastity: 0,
        speech_struggle: 0,
        speech_strip_struggle: 0,
        speech_spank: 0,
        speech_arms: 0,
        speech_clit: 0,
        speech_glans: 0,
        speech_bottom: 0,
        speech_hair: 0,
        speech_chest_rub: 0,
        speech_breast_rub: 0,
        speech_vagina_flaunt: 0,
        speech_vagina_virgin: 0,
        speech_anus_virgin: 0,
        speech_mouth_virgin: 0,
        speech_penis_virgin: 0,
        speech_penis_escape: 0,
        speech_vagina_escape: 0,
        speech_anus_escape: 0,
    }
};

export const NpcHuman = {
    state: {
        type: "man",
    },
    person: {
        /*These all have five additional versions, the same but with a number on the end, 2 - 6. One for each NPC "slot" in scenes and combat. Note that the first instance sometimes has the number 1 on the end, and sometimes does not.*/
        /*Controls correct grammar and capitalisation during encounters.*/
        intro: false,
        /*What their body parts are occupied by. Note this is different from, but will impact, the actions they take.*/
        lefthand: false,
        righthand: false,
        penis: false,
        vagina: false,
        mouth: false,
        /*NPC gender, pronouns and description, like "muscular" or "thin".*/
        gender: 'f',
        pronoun: 'thin',
        description: null,
    }
};

export const NpcBeast = {
    state: {
        type: "beast"
    },
    beast: {
        /*How many beasts are waiting their turn, or having their turn.*/
        beast_no: 0,
        /*How many beasts in the encounter total.*/
        beast_nomax: 0,
        /*Type of beast. Dog, wolf etc.*/
        beast_name: "none",
        /*If they have claws.*/
        claws: 0,
        /*Currently active beast's gender and pronouns.*/
        data: [/*{
            gender: 'f', // beastgender
            pronoun: 'x' // beastpronoun
        }*/]
    }
};

export const NpcSwarm = {
    state: {
        type: "swarm"
    },
    swarm: {
        swarm: [],/*Also swarm2, swarm3...up to swarm10*/
        swarm_pending: 0,
        /*Set at start of encounter*/
        swarm_steady: 0,
        swarm_Steady: 0,
        swarm_name: 0,
        swarm_move: 0,
        swarm_active: 0,
        swarm_creature: 0,
        swarm_count: 0,
        /*I tried something different with the swarms, having each action they could take associated with a variable, which would then be used to output text elsewhere*/
        swarm_chest_molest: 0,
        swarm_chest_grab: 0,
        swarm_chest_grab_intro: 0,
        swarm_chest_grab_clothes: 0,
        swarm_chest_clothed: 0,
        swarm_chest_covered: 0,
        swarm_activate: 0,
        swarm_spill: 0,
        swarm_back: 0,
        swarm_back_molest: 0,
        swarm_back_grab: 0,
        swarm_back_grab_intro: 0,
        swarm_back_grab_under_clothed: 0,
        swarm_back_under_clothed: 0,
        swarm_back_grab_lower_clothed: 0,
        swarm_back_lower_clothed: 0,
        swarm_back_cover: 0,
        swarm_back_inside: 0,
        swarm_back_inside_intro: 0,
        swarm_back_grab_lower_chastity: 0,
        swarm_front: 0,
        swarm_front_molest: 0,
        swarm_front_grab: 0,
        swarm_front_grab_intro: 0,
        swarm_front_grab_under_clothed: 0,
        swarm_front_under_clothed: 0,
        swarm_front_grab_lower_clothed: 0,
        swarm_front_lower_clothed: 0,
        swarm_front_cover: 0,
        swarm_front_inside: 0,
        swarm_front_inside_intro: 0,
        swarm_front_grab_lower_chastity: 0,
        front: 0,
        back: 0,
        chest: 0,
    }
};

export const NpcVore = {
    state: {
        type: "swarm"
    },
    vore: {
        /*Set at encounter's start.*/
        vore_strength: 0,
        /*How hard you struggle.*/
        vore_struggle: 0,
        /*If the encounter has tentacles.*/
        vore_tentacles: false,
        /*What part of the beast's digestive system you're in.*/
        vore_stage: 0,
        /*Name of creature.*/
        vore_creature: null,
        /*Set to 1 if swallowed, reset at end of encounter.*/
        swallowed: false,

    }
};

export const NpcTentacles = {
    state: {
        type: "tentacles"
    },
    tentacles: {
        /*Set at encounter's start*/
        /*Total number of tentacles.*/
        tentacle_no: 0,
        /*Number of active tentacles.*/
        active_tentacle_no: 0,
        /*How much health each tentacle has. Set at start.*/
        tentacle_health: 0,
        /*There alternative versions of the following, with the number 1 replaced with 2 - 16, covering each possible tentacle*/
        /*What each tentacle is doing.*/
        tentacle: [/*{
            description: null,
            shaft: false,
            head: false,
            health: 0
        }*/],
    }
};


code:
import Migration from "./Versions";
import Game from "../Game/Game";

Config.saves.onLoad = (save) => {
    let saveGameVersion = save.state.history[save.state.history.length-1].variables._gameVersion;
    if (typeof saveGameVersion === "undefined")
        saveGameVersion = '1.26.1'; // any saves before this need to be handled via the old game version

    if (saveGameVersion !== Game.getGameVersion()) {
        console.log(`Trying to update game save from ${saveGameVersion} to ${Game.getGameVersion()}`);

        while (true) {
            const major = saveGameVersion.split('.')[0];
            const minor = saveGameVersion.split('.')[1];
            const patch = saveGameVersion.split('.')[2];

            if (typeof Migration[major] === "undefined")
                break;

            if (typeof Migration[major][minor] === "undefined")
                break;

            if (typeof Migration[major][minor][patch] === "undefined")
                break;

            saveGameVersion = Migration[major][minor][patch](save.state.history[save.state.history.length-1].variables);
            if (saveGameVersion === Game.getGameVersion())
                break; // on current version
        }

        save.state.history[save.state.history.length-1].variables._gameVersion = saveGameVersion;

        if (saveGameVersion !== Game.getGameVersion())
            console.warn(`Reported game save version is ${saveGameVersion}, while the game version is ${Game.getGameVersion()}, some errors can occur.`);
    } // no need to update when version matches already boi
};


code:
import V1_26_1 from "./1/26/1"

export default {
    '1': {
        '26': {
            '1': V1_26_1
        }
    }
};

// current game data for new saves here
export {GameState, GameVersion, NpcShared, NpcHuman, NpcBeast, NpcSwarm, NpcTentacles, NpcVore} from "./1/26/1";


code:
import Game from "./Game/Game";

const instance = new Game();


description: App for degrees-of-lewdity

build (dev): webpack

build: webpack --env.mode=production

compile: cd .. && compile.bat

license: SEE LICENSE IN LICENSE.md


code:
const path = require('path');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

module.exports = (env) => {
    const webpackMode = (typeof env === "undefined" ? 'development' : (
        typeof env.mode === "string" ? env.mode : 'development'
    ));

    let options = {
        mode: webpackMode,
        entry: [
            'babel-polyfill',
            './index.js'
        ],
        output: {
            path: path.resolve(__dirname, '../game'),
            filename: 'base.js'
        },
        module: {
            rules: [{
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        plugins: [
                            '@babel/plugin-proposal-object-rest-spread',
                            '@babel/plugin-proposal-class-properties',
                            'lodash'
                        ]
                    }
                }
            }]
        },
        plugins: [
            new LodashModuleReplacementPlugin()
        ]
    };

    if (webpackMode === 'development')
        options['devtool'] = 'inline-source-map';

    return options;
};


code:
// Settings
$textonly $settings.text_only
$images $settings.enable_images
$debug $settings.debug
$numberify_enable $settings.numberify
$dev $settings.devmode
$gamemode $settings.gamemode

// Initial settings
$intro $settings.intro
$tutorial $settings.tutorial

// Location / time
$time $location.time
$days $location.day
$weekday $location.week_day
$month $location.month
$monthday $location.month_day
$year $location.year

// Player looks
$hairlength $player.looks.hair.length
$playergenderappearance $player.looks.gender_appearance
$haircolor $player.looks.hair.color

// Player information
$playergender $player.data.gender
$background $player.data.background
$birthmonth $player.data.birth_month
$birthday $player.data.birth_day
$breastsize $player.data.breast_size

// Stats
$controlmax $player.stats.control.max
$control $player.stats.control.current
$traumamax $player.stats.trauma.max
$trauma $player.stats.trauma.current
$stressmax $player.stats.stress.max
$stress $player.stats.stress.current
$physiquemax $player.stats.physique.max
$physique $player.stats.physique.current
$pain $player.stats.pain.current
$tirednessmax $player.stats.fatigue.max
$tiredness $player.stats.fatigue.current
$beautymax $player.stats.beauty.max
$beauty $player.stats.beauty.current

// Special stats
$purity $player.special_stats.purity
$submissive $player.special_stats.submissive
$assertive $player.special_stats.assertive
$assertiveaction $player.special_stats.assertive_action
$awareness $player.special_stats.awareness.current
$awarelevel $player.special_stats.awareness.level
$fame $player.special_stats.fame
$fameexhibitionism $player.special_stats.fame_exhibitionism
$fameprostitution $player.special_stats.fame_prostitution
$famebestiality $player.special_stats.fame_bestiality
$famesex $player.special_stats.fame_sex
$famerape $player.special_stats.fame_rape
$famegood $player.special_stats.fame_good
$famebusiness $player.special_stats.fame_business
$famepark $player.special_stats.fame_park

$oxygenmax $player.special_stats.oxygen.max
$oxygen $player.special_stats.oxygen.current

// Transformations
$wolfbuild $player.special_stats.transformation.wolf.build
$wolfgirl $player.special_stats.transformation.wolf.girl


code:
*.bat	text eol=crlf
*.tw	text eol=lf
*.sh	text eol=lf
*.py	text eol=lf
*.txt	text eol=lf
compile	text eol=lf
compile-git	text eol=lf
sanityCheck	text eol=lf

path: .gitattributes


code:
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.swp
*.swo

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# IPython Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# dotenv
.env

# virtualenv
venv/
ENV/

# Spyder project settings
.spyderproject

# Rope project settings
.ropeproject

# Start.tw
src/config/start.tw

path: .gitignore


code:
further development:
-specialized slave schools
-fortifications
-more levels for militia edict (further militarize society)
-conquering other arcologies?

Events:
-famous criminal escapes to the arcology, followed by another arcology police force

Bugs:
-sometimes troop counts breaks
-sometimes rebel numbers have fractionary parts


code:
# How to vector art

Please read the whole document before starting to work.
Some aspects are not obvious right away.

Note: This does not actually describe how to be an artist.

## TL;DR

    killall inkscape
    artTools/vector_layer_split.py artTools/vector_source.svg tw src/art/vector/layers/
    compile

## 1. Be an artist

Make changes to the vector_source.svg.
Inkscape was thoroughly tested.
Adobe Illustrator might work decently, too.

## 2. Respect the structure

While editing, keep the Layers in mind.

* In Inkscape, Layers are special "Inkscape groups".
* In Illustrator, Layers are groups with user-definded IDs.
* All Layers should have an ID that is globally unique
  (not just within their subtree).

* Please use anonymous groups only for the ease of editing. Remove all "helper" groups before finally saving the file.
* Anonymous groups can be used for continous scaling (of e.g. boobs).

* Every asset that should go into a separate file needs to be in a labelled group
  (even if that is a group with only one shape).
* There are some globally available styles defined as CSS classes (e.g. skin, hair).
  Use them if your asset should be changed upon display.
  Do not set the style directly in the object.

## 3. Normalise the document (before committing)

**THIS IS IMPORTANT**

The various editors out there (Inkscape, Illustrator) may use different variants of formatting the SVG XML.
Use

    python3 normalize_svg.py vector_source.svg

before committing to normalise the format so git will not freak out about the changed indentation.

If you use Inkscape, please use in Edit → Settings → Input/Output → SVG-Output → Path Data → Optimized. This is the default.
In case your Editor uses another path data style which cannot be changed, please contact the other artists and developers via the issue tracker to find a new common ground.

What it does:
* Formats the SVG XML according to Pythons lxml module, regardless of editor.
* Adobe Illustrator uses group IDs as layer labels.
  Inkscape however uses layer labels and a separate, auto-generated group ID.
  normalize_svg.py overwrites the group ID with the Inkscape layer label
  so they are synchronised with Inkscape layer labels.
* Inkscape copies the global style definition into the object *every time*
  the object is edited. If an object references a CSS class AND at the same time
  has a local style, the local style is removed
  so global dynamic styling is possible later on.

Note: Behaviour of Adobe Illustrator is untested.

## 4. Split the layers

For NoX original, deepurk extensions, execute

    python3 vector_layer_split.py vector_source_ndmain.svg tw ../src/art/vector/layers/

For faraen revamped art (based on NoX original)

	python3 vector_revamp_layer_split.py vector_revamp_source.svg tw ../src/art/vector_revamp/layers/

. This application reads all groups in `vector_source.svg`.
Each group is stored in a separate file in the target directory `/src/art/vector/layers/`.
The group ID sets the file name. Therefore, the group ID **must not** contain spaces or any other weird characters.

Also consider:
* A group with ID ending in _ is ignored. Use Layers ending in _ to keep overview.
* A group with ID starting with "g" (Inkscape) or "XMLID" (Illustrator) is also ignored.
* Existing files are overwritten **without enquiry**.
* The target directory is not emptied. If a file is no longer needed, you should remove it manually.
* This procedure removes global definitions. This means, SVG filters are currently not supported.

Available output formats are `svg` and `tw`.
`svg` output exists for debug reasons.
`tw` embeds the SVG data into Twine files, but removes the global style definitions so they can be set during display.

## 5. Edit the code

`/src/art/` contains Twine code which shows the assets in the story.
There are many helpful comments in `/src/art/artWidgets.tw`.
The code also generates the previously removed global style definitions on the fly and per display.

## 6. Compile the story

Use the provided compile script (Windows batch or shell) for compiling.

## 7. Advanced usage

You can define multiple CSS classes to one element, e.g. "skin torso". When procedurally generating CSS, you can then set a global "skin" style, and a different one for "torso".

You can put variable text into the image using single quotes, e.g. "'+_tattooText+'". The single quotes *break* the quoting in Twine so the content of the Twine variable `_tattooText` is shown upon display. You can even align text on paths.

An anonymous group can be used for dynamic transformation. However, finding the correct parameters for the affine transformation matrix can be cumbersome. Use any method you see fit. See `src/art/vector/Boob.tw` for one example of the result.


code:
Apron
AttractiveLingerie
AttractiveLingerieForAPregnantWoman
BallGown
Battledress
BodyOil
Bunny
Chains
ChattelHabit
Cheerleader
ClubslutNetting
ComfortableBodysuit
Conservative
Cutoffs
Cybersuit
FallenNunsHabit
FuckdollSuit
HalterTopDress
HaremGauze
Hijab
Huipil
Kimono
LatexCatsuit
Leotard
LongQipao
MaternityDress
MilitaryUniform
MiniDress
Monokini
NiceBusinessAttire
NiceMaid
NiceNurse
No
PenitentNunsHabit
RedArmyUniform
RestrictiveLatex
ScalemailBikini
SchoolgirlUniform
SchutzstaffelUniform
ShibariRopes
SlaveGown
Slutty
SluttyBusinessAttire
SluttyJewelry
SluttyMaid
SluttyNurse
SluttyQipao
SluttySchutzstaffelUniform
Spats
StretchPants
StringBikini
Succubus
Toga
UncomfortableStraps
Western


code:
#!/bin/bash

# This script reads all possible values of $slave.clothes as mentioned by the documentation.
# This script uses the actual implementation of the JS clothing2artSuffix function as defined in src/art/vector/Helper_Functions.tw
# This script outputs suffixes to be used in the SVG naming scheme
# This script is meant to be executed at the project root directory.
# This script depends on bash, grep, sed, paste and nodejs (so best executed on Linux, I guess)

# grep -Porh '(?<=\.clothes = )"[^"]+"' src/ | sort | uniq # searches sources for all clothes strings actually used in the game, even undocumented ones

(
  echo 'var window = {};'
  grep -v '^:' src/art/vector/Helper_Functions.tw
  echo -n 'Array('
  sed '/^clothes:/,/:/!d' "slave variables documentation - Pregmod.txt" | grep '"' | paste -sd,
  echo ').forEach(v => {console.log(window.clothing2artSuffix(v));});'
) | nodejs | sort


code:
#!/usr/bin/env python3

'''
Application for "normalizing" SVGs

These problems are addressed:

* Inkscape notoriously copies class styles into the object definitions.
https://bugs.launchpad.net/inkscape/+bug/167937

* Inkscape uses labels on layers. Layers are basically named groups.
  Inkscape does not sync the group id with the layer label.

Usage Example:
python3 inkscape_svg_fixup.py vector_source.svg
'''

import lxml.etree as etree
import sys

def fix(tree):
  # know namespaces
  ns = {
    'svg' : 'http://www.w3.org/2000/svg',
    'inkscape' : 'http://www.inkscape.org/namespaces/inkscape'
  }

  # find document global style definition
  # mangle it and interpret as python dictionary
  style_element = tree.find('./svg:style',namespaces=ns)
  style_definitions = style_element.text
  pythonic_style_definitions = '{'+style_definitions.\
  replace('\t.','"').replace('{','":"').replace('}','",').\
  replace('/*','#')+'}'
  styles = eval(pythonic_style_definitions)

  # go through all SVG elements
  for elem in tree.iter():
    if (elem.tag == etree.QName(ns['svg'], 'g')):
      # compare inkscape label with group element ID
      l = elem.get(etree.QName(ns['inkscape'], 'label'))
      if l:
        i = elem.get('id')
        if (i != l):
          print("Overwriting ID %s with Label %s..."%(i, l))
          elem.set('id', l)

    # clean styles (for easier manual merging)
    style_string = elem.get('style')
    if style_string:
      split_styles = style_string.strip('; ').split(';')
      styles_pairs = [s.strip('; ').split(':') for s in split_styles]
      filtered_pairs = [ (k,v) for k,v in styles_pairs if not (
        k.startswith('font-') or
        k.startswith('text-') or
        k.endswith('-spacing') or
        k in ["line-height", " direction", " writing", " baseline-shift", " white-space", " writing-mode"]
      )]
      split_styles = [':'.join(p) for p in filtered_pairs]
      style_string = ';'.join(sorted(split_styles))
      elem.attrib["style"] = style_string

    # remove all style attributes offending class styles
    s = elem.get('style')
    c = elem.get('class')
    if (c and s):
      s = s.lower()
      c = c.split(' ')[0] # regard main style only
      cs = ''
      if c in styles:
        cs = styles[c].strip('; ').lower()
      if (c not in styles):
        print("Object id %s references unknown style class %s."%(i,c))
      else:
        if (cs != s.strip('; ')):
          print("Style %s removed from object id %s differed from class %s style %s."%(s,i,c,cs))
        del elem.attrib["style"]

if __name__ == "__main__":
  input_file = sys.argv[1]
  tree = etree.parse(input_file)
  fix(tree)
  # store SVG into file (input file is overwritten)
  svg = etree.tostring(tree, pretty_print=True)
  with open(input_file, 'wb') as f:
    f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
    f.write(svg)


code:
#!/usr/bin/env python3

'''
Application for procedural content adaption
*THIS IS VERY EXPERIMENTAL*
Contains a very poor man's implementation of spline mesh warping.

This application parses SVG path data of an outfit sample aligned to a body part.
The outfit is then replicated for other shapes of the same body part.

Example data is geared towards generating a strap outfit for boobs and torso
for all sizes of boobs and all shapes of torsos based on a single outfit for
boobs of size 2 and a hourglass torso respectively.

Limitations:
* handles paths only
* only svg.path Line and CubicBezier are tested
* only the aforementioned examples are tested

Usage:
python3 vector_clothing_replicator.py infile clothing bodypart destinationfile

Usage Example:
python3 vector_clothing_replicator.py vector_source.svg Straps Boob vector_destination.svg
python3 vector_clothing_replicator.py vector_source.svg Straps Torso vector_destination.svg
'''

from svg.path import parse_path
import copy
import lxml.etree as etree
import sys

REFERENCE_PATH_SAMPLES = 200
EMBED_REPLICATIONS = True # wether to embed all replications into the input file or output separate files

input_file = sys.argv[1]
clothing = sys.argv[2]
bodypart = sys.argv[3]
output_file_embed = sys.argv[4]

# TODO: make these configurable
output_file_pattern = '%s_%s_%s.svg' #bodypart, target_id, clothing

if ('Torso' == bodypart):
  xpath_shape = './svg:g[@id="Torso_"]/svg:g[@id="Torso_%s"]/svg:path[@class="skin torso"]/@d' # TODO: formulate more general, independent of style
  xpath_outfit_container = '//svg:g[@id="Torso_Outfit_%s_"]'%(clothing)
  xpath_outfit = '//svg:g[@id="Torso_Outfit_%s_%s"]'%(clothing,'%s')
  target_ids = "Unnatural,Hourglass,Normal".split(",")
  reference_id = "Hourglass"
else:
  raise RuntimeError("Please specify a bodypart for clothing to replicate.")

tree = etree.parse(input_file)
ns = {'svg' : 'http://www.w3.org/2000/svg'}

canvas = copy.deepcopy(tree)
for e in canvas.xpath('./svg:g',namespaces=ns)+canvas.xpath('./svg:path',namespaces=ns):
  # TODO: this should be "remove all objects, preserve document properties"
  e.getparent().remove(e)

def get_points(xpath_shape):
  '''
  This funciton extracts reference paths by the given xpath selector.
  Each path is used to sample a fixed number of points.
  '''
  paths_data = tree.xpath(xpath_shape,namespaces=ns)
  points = []
  path_length = None
  for path_data in paths_data:
    p = parse_path(path_data)
    points += [
      p.point(1.0/float(REFERENCE_PATH_SAMPLES)*i)
      for i in range(REFERENCE_PATH_SAMPLES)
    ]
  if (not points):
    raise RuntimeError(
      'No paths for reference points found by selector "%s".'%(xpath_shape)
    )
  return points

def point_movement(point, reference_points, target_points):
  '''
  For a given point, finds the nearest point in the reference path.
  Gives distance vector from the nearest reference point to the
  respective target reference point.
  '''
  distances = [abs(point-reference_point) for reference_point in reference_points]
  min_ref_dist_idx = min(enumerate(distances), key=lambda x:x[1])[0]
  movement = target_points[min_ref_dist_idx] - reference_points[min_ref_dist_idx]
  return movement

reference_points = get_points(xpath_shape%(reference_id))
container = tree.xpath(xpath_outfit_container,namespaces=ns)
if (len(container) != 1):
  raise RuntimeError('Outfit container selector "%s" does not yield exactly one layer.'%(xpath_outfit_container))
container = container[0]
outfit_source = container.xpath(xpath_outfit%(reference_id),namespaces=ns)
if (len(outfit_source) != 1):
  raise RuntimeError('Outfit source selector "%s" does not yield exactly one outfit layer in container selected by "%s".'%(xpath_outfit%(reference_id), xpath_outfit_container))
outfit_source = outfit_source[0]

for target_id in target_ids:
  print(
    'Generating variant "%s" of clothing "%s" for bodypart "%s"...'%
    (target_id, clothing, bodypart)
  )
  outfit = copy.deepcopy(outfit_source)
  paths = outfit.xpath('./svg:path',namespaces=ns)
  if target_id == reference_id:
    print("This is the source variant. Skipping...")
  else:
    layerid = outfit.get('id').replace('_%s'%(reference_id),'_%s'%(target_id))
    outfit.set('id', layerid)
    outfit.set(etree.QName('http://www.inkscape.org/namespaces/inkscape', 'label'), layerid) # for the Inkscape-users
    target_points = get_points(xpath_shape%(target_id))
    if (len(reference_points) != len(target_points)):
      raise RuntimeError(
        ('Different amounts of sampled points in reference "%s" and target "%s" paths. '+
        'Selector "%s" probably matches different number of paths in the two layers.')%
        (reference_id, target_id, xpath_shape)
      )
    for path in paths:
      path_data = path.get("d")
      p = parse_path(path_data)
      for segment in p:
        original_distance = abs(segment.end-segment.start)
        start_movement = point_movement(segment.start, reference_points, target_points)
        segment.start += start_movement
        end_movement = point_movement(segment.end, reference_points, target_points)
        segment.end += end_movement
        distance = abs(segment.end-segment.start)
        try:
          # enhance position of CubicBezier control points
          # amplification is relative to the distance gained by movement
          segment.control1 += start_movement
          segment.control1 += (segment.control1-segment.start)*(distance/original_distance-1.0)
          segment.control2 += end_movement
          segment.control2 += (segment.control2-segment.end)*(distance/original_distance-1.0)
        except AttributeError as ae:
          # segment is not a CubicBezier
          pass
      path.set("d", p.d())
    if EMBED_REPLICATIONS:
      container.append(outfit)
  if not EMBED_REPLICATIONS:
    container = copy.deepcopy(canvas).xpath('.',namespaces=ns)[0]
    container.append(outfit)

  if not EMBED_REPLICATIONS:
    svg = etree.tostring(container, pretty_print=True)
    with open((output_file_pattern%(bodypart, target_id, clothing)).lower(), 'wb') as f:
      f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
      f.write(svg)

if EMBED_REPLICATIONS:
  svg = etree.tostring(tree, pretty_print=True)
  with open(output_file_embed, 'wb') as f:
    f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
    f.write(svg)


code:
#!/usr/bin/env python3

'''
Application for splitting groups from one SVG file into separate files

Usage:
python3 vector_layer_split.py infile format outdir

Usage Example:
python3 vector_layer_split.py vector_source.svg tw ../src/art/vector/layers/
'''

import lxml.etree as etree
import sys
import os
import copy
import re
import normalize_svg

input_file = sys.argv[1]
output_format = sys.argv[2]
output_directory = sys.argv[3]
if not os.path.exists(output_directory):
    os.makedirs(output_directory)

ns = {
  'svg' : 'http://www.w3.org/2000/svg',
  'inkscape' : 'http://www.inkscape.org/namespaces/inkscape',
  'sodipodi':"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
}

tree = etree.parse(input_file)
normalize_svg.fix(tree)

# prepare output template
template = copy.deepcopy(tree)
root = template.getroot()

# remove all svg root attributes except document size
for a in root.attrib:
  if (a != "viewBox"):
    del root.attrib[a]

# add placeholder for CSS class (needed for workaround for non HTML 5.1 compliant browser)
if output_format == 'tw':
  root.attrib["class"] = "'+_art_display_class+'"

# remove all content, including metadata
# for twine output, style definitions are removed, too
defs = None
for e in root:
  if (e.tag == etree.QName(ns['svg'], 'defs')):
    defs = e
  if (e.tag == etree.QName(ns['svg'], 'g') or
      e.tag == etree.QName(ns['svg'], 'metadata') or
      e.tag == etree.QName(ns['svg'], 'defs') or
      e.tag == etree.QName(ns['sodipodi'], 'namedview') or
      (output_format == 'tw' and e.tag == etree.QName(ns['svg'], 'style'))
      ):
    root.remove(e)
# template preparation finished

# prepare regex for later use
regex_xmlns = re.compile(' xmlns[^ ]+',)
regex_space = re.compile('[>][ ]+[<]',)

# find all groups
layers = tree.xpath('//svg:g',namespaces=ns)
for layer in layers:
  i = layer.get('id')
  if ( # disregard non-content groups
    i.endswith("_") or # manually suppressed with underscore
    i.startswith("XMLID") or # Illustrator generated group
    i.startswith("g") # Inkscape generated group
  ):
    continue
  # create new canvas
  output = copy.deepcopy(template)
  # copy all shapes into template
  canvas = output.getroot()
  for e in layer:
    canvas.append(e)
  # represent template as SVG (binary string)
  svg = etree.tostring(output, pretty_print=False)
  # poor man's conditional defs insertion
  # TODO: extract only referenced defs (filters, gradients, ...)
  # TODO: detect necessity by traversing the elements. do not stupidly search in the string representation
  if ("filter:" in svg.decode('utf-8')):
    # it seems there is a filter referenced in the generated SVG, re-insert defs from main document
    canvas.insert(0,defs)
    # re-generate output
    svg = etree.tostring(output, pretty_print=False)

  if (output_format == 'tw'):
    # remove unnecessary attributes
    # TODO: never generate unnecessary attributes in the first place
    svg = svg.decode('utf-8')
    svg = regex_xmlns.sub('',svg)
    svg = svg.replace(' inkscape:connector-curvature="0"','') # this just saves space
    svg = svg.replace('\n','').replace('\r','') # print cannot be multi-line
    svg = regex_space.sub('><',svg) # remove indentaion
    svg = svg.replace('svg:','') # svg namespace was removed
    svg = svg.replace('<g ','<g transform="\'+_art_transform+\'"') # internal groups are used for scaling
    svg = svg.encode('utf-8')

  # save SVG string to file
  i = layer.get('id')
  output_path = os.path.join(output_directory,i+'.'+output_format)
  with open(output_path, 'wb') as f:
    if (output_format == 'svg'):
      # Header for normal SVG (XML)
      f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
      f.write(svg)
    elif (output_format == 'tw'):
      # Header for SVG in Twine file (SugarCube print statement)
      f.write((':: Art_Vector_%s [nobr]\n\n'%(i)).encode("utf-8"))
      f.write("<<print '<html>".encode("utf-8"))
      f.write(svg)
      f.write("</html>' >>".encode("utf-8"))


code:
#!/usr/bin/env python3

'''
Application for splitting groups from one SVG file into separate files

Usage:
python3 vector_layer_split.py infile format outdir

Usage Example:
python3 vector_layer_split.py vector_source.svg tw ../src/art/vector/layers/
'''

import lxml.etree as etree
from lxml.etree import XMLParser, parse

import sys
import os
import copy
import re
import normalize_svg

input_file = sys.argv[1]
output_format = sys.argv[2]
output_directory = sys.argv[3]

if not os.path.exists(output_directory):
    os.makedirs(output_directory)

ns = {
    'svg': 'http://www.w3.org/2000/svg',
    'inkscape': 'http://www.inkscape.org/namespaces/inkscape',
    'sodipodi': "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
}

p = XMLParser(huge_tree=True)
tree = parse(input_file, parser=p)
#tree = etree.parse(input_file)
normalize_svg.fix(tree)

# prepare output template
template = copy.deepcopy(tree)
root = template.getroot()

# remove all svg root attributes except document size
for a in root.attrib:
    if (a != "viewBox"):
        del root.attrib[a]

# add placeholder for CSS class (needed for workaround for non HTML 5.1 compliant browser)
if output_format == 'tw':
    root.attrib["class"] = "'+_art_display_class+'"

# remove all content, including metadata
# for twine output, style definitions are removed, too
defs = None
for e in root:
    if (e.tag == etree.QName(ns['svg'], 'defs')):
        defs = e
    if (e.tag == etree.QName(ns['svg'], 'g') or
            e.tag == etree.QName(ns['svg'], 'metadata') or
            e.tag == etree.QName(ns['svg'], 'defs') or
            e.tag == etree.QName(ns['sodipodi'], 'namedview') or
            (output_format == 'tw' and e.tag == etree.QName(ns['svg'], 'style'))
    ):
        root.remove(e)
# template preparation finished

# prepare regex for later use
regex_xmlns = re.compile(' xmlns[^ ]+', )
regex_space = re.compile('[>][ ]+[<]', )
regex_transformValue = re.compile('(?<=transformVariableName=")[^"]*', )
regex_transformVar = re.compile('transformVariableName="[^"]*"', )
regex_transformAttr = re.compile('transform="[^"]*"', )
# find all groups
layers = tree.xpath('//svg:g', namespaces=ns)
for layer in layers:
    i = layer.get('id')
    if (  # disregard non-content groups
            i.endswith("_") or  # manually suppressed with underscore
            i.startswith("XMLID") or  # Illustrator generated group
            i.startswith("g")  # Inkscape generated group
    ):
        continue
    # create new canvas
    output = copy.deepcopy(template)
    # copy all shapes into template
    canvas = output.getroot()
    for e in layer:
        canvas.append(e)
    # represent template as SVG (binary string)
    svg = etree.tostring(output, pretty_print=False)
    # poor man's conditional defs insertion
    # TODO: extract only referenced defs (filters, gradients, ...)
    # TODO: detect necessity by traversing the elements. do not stupidly search in the string representation
    if ("filter:" in svg.decode('utf-8')):
        # it seems there is a filter referenced in the generated SVG, re-insert defs from main document
        canvas.insert(0, defs)
        # re-generate output
        svg = etree.tostring(output, pretty_print=False)
    elif ("clip-path=" in svg.decode('utf-8')):
        # it seems there is a clip path referenced in the generated SVG, re-insert defs from main document
        canvas.insert(0, defs)
        # re-generate output
        svg = etree.tostring(output, pretty_print=False)

    if (output_format == 'tw'):
        # remove unnecessary attributes
        # TODO: never generate unnecessary attributes in the first place
        svg = svg.decode('utf-8')
        svg = regex_xmlns.sub('', svg)
        svg = svg.replace(' inkscape:connector-curvature="0"', '')  # this just saves space
        svg = svg.replace('\n', '').replace('\r', '')  # print cannot be multi-line
        svg = regex_space.sub('><', svg)  # remove indentaion
        svg = svg.replace('svg:', '')  # svg namespace was removed

        transformGroups = regex_transformVar.findall(svg)

        if (len(transformGroups) > 0):
            svg = regex_transformAttr.sub('', svg)
            for transformGroup in transformGroups:
                transformValue = regex_transformValue.search(transformGroup)
                if (transformValue is not None):
                    svg = svg.replace(transformGroup, ' transform="\'+' + transformValue.group() + '+\'" ')  # internal groups are used for scaling

        svg = svg.encode('utf-8')

    # save SVG string to file
    i = layer.get('id')
    output_path = os.path.join(output_directory, i + '.' + output_format)
    with open(output_path, 'wb') as f:
        if (output_format == 'svg'):
            # Header for normal SVG (XML)
            f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'.encode("utf-8"))
            f.write(svg)
        elif (output_format == 'tw'):
            # Header for SVG in Twine file (SugarCube print statement)
            f.write((':: Art_Vector_Revamp_%s [nobr]\n\n' % (i)).encode("utf-8"))
            f.write("<<print '<html>".encode("utf-8"))
            f.write(svg)
            f.write("</html>' >>".encode("utf-8"))


code:
# Ignore everything in this directory
*
# Except the following:
!.gitignore


code:
#!/bin/bash

while [[ "$1" ]]
do
	case $1 in
	--insane)
		insane="true"
	;;
	*)
		echo "Unknown argument $1"
		exit 1
	esac
	shift
done

# Find and insert current commit
COMMIT=$(git rev-parse --short HEAD)
sed -Ei "s/build .releaseID/\0 commit $COMMIT/" src/gui/mainMenu/AlphaDisclaimer.tw

if [[ ! "$insane" ]]
then
	# Run sanity check.
	./sanityCheck
fi

ARCH="$(uname -m)"
if [ "$ARCH" = "x86_64" ]
then
	echo "x64 arch"
	./devTools/tweeGo/tweego_nix64 -o bin/FC_pregmod.html src/ || build_failed="true"
elif echo "$ARCH" | grep -Ee '86$' > /dev/null
then
	echo "x86 arch"
	./devTools/tweeGo/tweego_nix86 -o bin/FC_pregmod.html src/ || build_failed="true"
elif echo "$ARCH" | grep -Ee '^arm' > /dev/null
then
	echo "arm arch"
	# tweego doesn't provide arm binaries so you have to build it yourself
	export TWEEGO_PATH=devTools/tweeGo/storyFormats
	tweego -o bin/FC_pregmod.html src/ || build_failed="true"
else
	exit 2
fi

# Revert AlphaDisclaimer for next compilation
git checkout -- src/gui/mainMenu/AlphaDisclaimer.tw

if [ "$build_failed" = "true" ]
then
	exit 1
fi

#Make the output prettier, replacing \t with a tab and \n with a newline
sed -i -e '/^.*<div id="store-area".*$/s/\\t/\t/g' -e '/^.*<div id="store-area".*$/s/\\n/\n/g'  bin/FC_pregmod.html

path: compile


code:
#!/bin/bash

# Run sanity check.
./sanityCheck

HASH="$(git rev-list -n 1 --abbrev-commit HEAD)"
ARCH="$(uname -m)"
if [ "$ARCH" = "x86_64" ]
then
	echo "x64 arch"
	./devTools/tweeGo/tweego_nix64 -o "bin/FC_pregmod_$HASH.html" src/
elif echo "$ARCH" | grep -Ee '86$' > /dev/null
then
	echo "x86 arch"
	./devTools/tweeGo/tweego_nix86 -o "bin/FC_pregmod_$HASH.html" src/
elif echo "$ARCH" | grep -Ee '^arm' > /dev/null
then
	echo "arm arch"
	# tweego doesn't provide arm binaries so you have to build it yourself
	export TWEEGO_PATH=devTools/tweeGo/storyFormats
	tweego -o "bin/FC_pregmod_$HASH.html" src/
else
	exit 2
fi

#Make the output prettier, replacing \t with a tab and \n with a newline
sed -i -e '/^<div id="store-area".*$/s/\\t/\t/g' -e '/^<div id="store-area".*$/s/\\n/\n/g'  "bin/FC_pregmod_$HASH.html"

echo "FC_pregmod_$HASH.html" compilation finished.

path: compile-git


code:
@echo off
:: Free Cities Basic Compiler - Windows

:: Set working directory
pushd %~dp0

:: Run the appropriate compiler for the user's CPU architecture.
if %PROCESSOR_ARCHITECTURE% == AMD64 (
    CALL "%~dp0devTools\tweeGo\tweego_win64.exe" -o "%~dp0bin/FC_pregmod.html" "%~dp0src"
) else (
    CALL "%~dp0devTools\tweeGo\tweego_win86.exe" -o "%~dp0bin/FC_pregmod.html" "%~dp0src"
)

popd
ECHO Done


code:
@echo off
:: Free Cities Basic Compiler - Windows

:: Set working directory
pushd %~dp0

:: See if we can find a git installation
setlocal enabledelayedexpansion

for %%k in (HKCU HKLM) do (
    for %%w in (\ \Wow6432Node\) do (
        for /f "skip=2 delims=: tokens=1*" %%a in ('reg query "%%k\SOFTWARE%%wMicrosoft\Windows\CurrentVersion\Uninstall\Git_is1" /v InstallLocation 2^> nul') do (
            for /f "tokens=3" %%z in ("%%a") do (
                set GIT=%%z:%%b
				set GITFOUND=yes
                goto FOUND
            )
        )
    )
)
:FOUND
if %GITFOUND% == yes (
	set "PATH=%GIT%bin;%PATH%"
	bash --login -c ./sanityCheck
)

:: Compile the game
call "%~dp0compile.bat"

if %GITFOUND% == yes (
	:: Make the output prettier, replacing \t with a tab and \n with a newline
	bash -c "sed -i -e '/^.*<div id=\"store-area\".*$/s/\\\t/\t/g' -e '/^.*<div id=\"store-area\".*$/s/\\\n/\n/g' bin/FC_pregmod.html"

	:: Revert ./src/init/storyInit.tw for next compilation
	git checkout -- ./src/init/storyInit.tw
)

popd
PAUSE


code:
@echo off
:: Free Cities Basic Compiler - Windows

:: Set working directory
pushd %~dp0

:: Compile the game
call "%~dp0compile.bat"

popd
PAUSE