diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js index d09ebfef4727585df5c81c8cd1650f68a400d0ac..997230d4f39a2d3f9eddaf2b52be23091a2d33bc 100644 --- a/js/003-data/gameVariableData.js +++ b/js/003-data/gameVariableData.js @@ -1301,6 +1301,7 @@ App.Data.resetOnNGPlus = { /** @type {FC.Bool} */ cheatMode: 0, cheatModeM: 1, + slaveBotGeneration: 0, experimental: { nursery: 0, food: 0, diff --git a/src/005-passages/interactPassages.js b/src/005-passages/interactPassages.js index b5c6ad8c2119fda0995b96ddffc991bc5c48bbbd..98b628324b7b666efd1b9b3e9d3b7ad098750b93 100644 --- a/src/005-passages/interactPassages.js +++ b/src/005-passages/interactPassages.js @@ -109,6 +109,18 @@ new App.DomPassage( }, ["jump-from-safe"] ); +new App.DomPassage( + "Create Slave Bot", + () => { + V.nextButton = "Continue"; + V.nextLink = "Slave Interact"; + const el = new DocumentFragment(); + App.UI.DOM.appendNewElement("p", el, `Exporting slave...`); + App.UI.SlaveInteract.createSlaveBot(getSlave(V.AS)); + return el + }, ["jump-from-safe"] +); + new App.DomPassage( "Cheat Edit JS", () => { diff --git a/src/gui/options/options.js b/src/gui/options/options.js index da25face423b8fb7edeba5df6f0f7a74a40d76e5..6a697ab680915906a2bfb850b6f88769834d0aef 100644 --- a/src/gui/options/options.js +++ b/src/gui/options/options.js @@ -716,6 +716,10 @@ App.UI.optionsPassage = function() { .addComment("This will enable the experimental nursery, which allows players to interact with growing slave children. An alternative to the incubator."); } + options.addOption("Slave Bot generation is ", "slaveBotGeneration") + .addValue("Enabled", 1).on().addValue("Disabled", 0).off() + .addComment("This will enable an option to export slaves as a Slave Bot, compatible with SillyTavern for LLM-backed roleplay. Available in the Records tab."); + options.addOption("Allow sufficiently large clitori to evaluate true in canPenetrate()", "clitoralPenetration", V.experimental) .addValue("Enabled", 1).on().addValue("Disabled", 0).off() .addComment("canPenetrate() was originally built to assume dicks, so not all scenes may make sense without one. May also lead to awkward pregnancies."); diff --git a/src/interaction/siRecords.js b/src/interaction/siRecords.js index 286493fbc0c2d171f9f610d0d6121bc2009612d4..fcfb5abbfaf73b09ceead5764262d07b79766073 100644 --- a/src/interaction/siRecords.js +++ b/src/interaction/siRecords.js @@ -314,6 +314,14 @@ App.UI.SlaveInteract.records = function(slave, refresh) { "Export Slave", ) ); + if (V.slaveBotGeneration) { + linkArray.push( + App.UI.DOM.passageLink( + `Create slave bot`, + "Create Slave Bot", + ) + ); + } if (V.cheatMode) { linkArray.push( App.UI.DOM.passageLink( diff --git a/src/npc/generateSlaveBot.js b/src/npc/generateSlaveBot.js new file mode 100644 index 0000000000000000000000000000000000000000..7f8209f9afc89cb26a8f9fef20ff7d6ebdc1bea9 --- /dev/null +++ b/src/npc/generateSlaveBot.js @@ -0,0 +1,387 @@ +/** @param {App.Entity.SlaveState} slave */ +App.UI.SlaveInteract.createSlaveBot = function(slave) { + const el = new DocumentFragment(); + App.UI.DOM.appendNewElement("p", el, `Downloading slave bot...`, "note"); + Exporter.Json(createCharacterDataFromSlave(slave)); + return el; +}; + +// Adapted from https://github.com/ZoltanAI/character-editor +class Exporter { + + static downloadFile(file) { + const link = window.URL.createObjectURL(file); + + const a = document.createElement('a'); + a.setAttribute('download', file.name); + a.setAttribute('href', link); + a.click(); + } + + static Json(characterCard) { + const file = new File([JSON.stringify(characterCard, undefined, '\t')], (characterCard.data.name || 'character') + '.json', { type: 'application/json;charset=utf-8' }); + + Exporter.downloadFile(file); + } +} + +/** @param {App.Entity.SlaveState} slave */ +function createCharacterDataFromSlave(slave) { + // Construct a character card based on the Card v2 spec: https://github.com/malfoyslastname/character-card-spec-v2 + var characterCard = { + spec: 'chara_card_v2', + spec_version: '2.0', // May 8th addition + data: { + alternate_greetings: [], + avatar: "none", + character_version: "main", + creator: "FreeCities System Generated", + creator_notes: "FreeCities System Generated Slave Bot", + description: generateDescription(slave), + first_mes: `${properMaster()}, how may I serve you?`, + mes_example: "", + name: slave.slaveName, + personality: "", + scenario: `{{char}} and {{user}} exist in the slaveholding arcology of ${V.arcologies[0].name}. {{char}} is in {{user}}'s office, waiting for inspection.`, + system_prompt: "", + } + } + return characterCard +} + +function generateDescription(slave) { + let r = [] + // NAME + r.push(`Name: ${SlaveFullName(slave)}`); + + //RELATIONSHIP + r.push("\r\nRelationship: {{char}} is {{user}}'s "); + // Slave age (consider visible age) + r.push(`${slave.actualAge} year old `); + + // Devotion + if (slave.devotion < -95) { + r.push("hate-filled, "); + } else if (slave.devotion < -50) { + r.push("hateful, "); + } else if (slave.devotion < -20) { + r.push("reluctant, "); + } else if (slave.devotion <= 20) { + r.push("hesitant, "); + } else if (slave.devotion <= 50) { + r.push("accepting, "); + } else if (slave.devotion <= 95) { + r.push("devoted, "); + } else { + r.push("worshipful, "); + } + + // Trust + if (slave.trust < -95) { + r.push("abjectly terrified "); + } else if (slave.trust < -50) { + r.push("terrified "); + } else if (slave.trust < -20) { + r.push("frightened "); + } else if (slave.trust < 20) { + r.push("fearful "); + } else if (slave.trust <= 50) { + if (slave.devotion < -20) { + r.push("careful "); + } else { + r.push("careful "); + } + } else if (slave.trust < 95) { + if (slave.devotion < -20) { + r.push("bold "); + } else { + r.push("trusting "); + } + } else { + if (slave.devotion < -20) { + r.push("defiant "); + } else { + r.push("profoundly trusting "); + } + } + + // Slave's Title, ex:"pregnant big bottomed busty milky hourglass broodmother" + r.push(`${SlaveTitle(slave)}`); + + // DESCRIPTION + r.push("\r\nDescription: "); + + // Eyes + // eye color (orig vs. current?), Add check for no eyes (does it matter?) + if (!canSee(slave)) { + r.push(`${slave.eye.origColor} eyes, is blind, `); + } else { + r.push(`${slave.eye.origColor} eyes, `); + } + + // Skin + r.push(`${slave.skin} skin, `); + + // Slave intelligence: Ignore average, include mindbroken + if (slave.fetish === Fetish.MINDBROKEN) { + r.push("mindbroken, "); + } else if (slave.intelligence < -95) { + r.push("borderline retarded, "); + } else if (slave.intelligence < -50) { + r.push("very dumb, "); + } else if (slave.intelligence > 95) { + r.push("brilliant, "); + } else if (slave.intelligence > 50) { + r.push("very smart, "); + } + + // Beauty + if (slave.face < -40) { + r.push(`${slave.faceShape} ugly face, `); + } else if (slave.face > 50) { + r.push(`${slave.faceShape} gorgeous face, `); + } else if (slave.face > 10) { + r.push(`${slave.faceShape} very pretty face, `); + } else { + r.push(`${slave.faceShape} face, `); + } + + // Hairstyle + // Add "hair" to hairstyles that need it to make sense (e.g. "messy" becomes "messy hair" but "dreadlocks" stays as is) + if (["braided", "curled", "eary", "bun", "messy bun", "tails", "drills", "luxurious", "messy", "neat", "permed", "bangs", "hime", "strip", "up", "trimmed", "undercut", "double buns", "chignon"].includes(slave.hStyle)) { + r.push(`${slave.hStyle} hair, `) + } else { + r.push(`${slave.hStyle}, `) + } + + // Start conditional descriptions + let descParts = []; + // Eductation (bimbo/hindered, well educated) + if (slave.education < -10) { + descParts.push("vapid bimbo with no education"); + } else if (slave.intelligence > 25) { + descParts.push("very well educated"); + } + + // Height. Ignore Average + if (slave.height < 150) { + descParts.push(`short`); + } else if (slave.height > 180) { + descParts.push(`tall`); + } + + // Weight. Ignore average + if (slave.weight < -95) { + descParts.push(`emaciated`); + } else if (slave.weight < -30) { + descParts.push(`very skinny`); + } else if (slave.weight > 95) { + descParts.push(`very fat`); + } else if (slave.weight > 30) { + descParts.push(`plump`); + } + + // Boobs. Ignore Average. Add lactation? NG + if (slave.boobs < 300) { + descParts.push(`flat chested`); + } else if (slave.boobs < 500) { + descParts.push(`small breasts`); + } else if (slave.boobs > 1400) { + descParts.push(`massive breasts that impede movement`); + } else if (slave.boobs > 800) { + descParts.push(`large breasts`); + } + + // Butt. Ignore average + if (slave.butt <= 1) { + descParts.push(`flat butt`); + } else if (slave.butt > 7) { + descParts.push(`gigantic ass`); + } else if (slave.butt > 3) { + descParts.push(`big ass`); + } + + // Musculature + if (slave.muscles < -31) { + descParts.push(`very weak`); + } else if (slave.muscles > 50) { + descParts.push(`very muscular`); + } + + // Check amputee (add missing just arms/legs) + if (isAmputee(slave)) { + descParts.push(`missing both arms and both legs`); + } + + // Check pregnant + if (slave.preg > 30) { + descParts.push(`very pregnant`); + } else if (slave.preg > 20) { + descParts.push(`pregnant`); + } + + // Check teeth + if (slave.teeth === "straightening braces" || slave.teeth === "cosmetic braces") { + descParts.push(`braces on teeth`); + } else if (slave.teeth === "removable") { + descParts.push(`has dentures`); + } + + if (descParts.length > 0) { + r.push(`${descParts.join(', ')}`); + } + + + // BACKGROUND + if (slave.career === "a slave") { + r.push(`\r\nBackground: {{char}} has been enslaved for as long as they can remember`); + } else { + r.push(`\r\nBackground: Prior to enslavement, {{char}} was ${slave.career}`); + } + + + // OCCUPATION + r.push(`\r\nOccupation: {{char}} is assigned to ${slave.assignment}`); + + + // FETISH + // Paraphilias listed and prompted + if (slave.sexualFlaw === SexualFlaw.CUMADDICT ) { + r.push(`\r\nFetish: pathologically addicted to cum`); + } else if (slave.sexualFlaw === SexualFlaw.ANALADDICT ) { + r.push(`\r\nFetish: pathologically addicted to anal sex`); + } else if (slave.sexualFlaw === SexualFlaw.NEGLECT ) { + r.push(`\r\nTrait: only considers their partner's pleasure`); + } else if (slave.sexualFlaw === SexualFlaw.ATTENTION ) { + r.push(`\r\nTrait: pathologically narcissistic`); + } else if (slave.sexualFlaw === SexualFlaw.BREASTEXP ) { + r.push(`\r\nFetish: pathologically addicted to breast augmentation`); // is this right + } else if (slave.sexualFlaw === SexualFlaw.SELFHATING ) { + r.push(`\r\nTrait: pathologically masochistic`); + } else if (slave.sexualFlaw === SexualFlaw.ABUSIVE || slave.sexualFlaw === SexualFlaw.MALICIOUS) { + r.push(`\r\nTrait: sociopathic, delights in abusing others`); //Are above the same for purposes of LLM + } + + // Explain sex/entertainment skill level. Leave off average. Check virgin status. + + // ABILITIES + let abilParts = []; + if (slave.vagina === 0){ + abilParts.push("virgin"); + } else if (slave.skill.whoring <10){ + abilParts.push("sexually unskilled"); + } else if (slave.skill.whoring > 100){ + abilParts.push("renowned whore"); + } else if (slave.skill.whoring > 61){ + abilParts.push("expert whore"); + } + + if (slave.skill.entertainment > 100){ + abilParts.push(`renowned entertainer`); + } else if (slave.skill.entertainment > 61){ + abilParts.push(`expert entertainer`); + } + if (abilParts.length > 0) { + r.push(`\r\nAbilities: ${abilParts.join(', ')}`); + } + + // WEARING + r.push(`\r\n\Wearing: ${slave.clothes}`); + if (slave.collar !== "none"){ + r.push(`, ${slave.collar} collar`); + } + + // GENITALS + // Front + r.push("\r\nGenitals: "); + if (slave.dick === 0 && slave.vagina === -1) { // null slave + r.push("No genitals"); + } else if (slave.vagina < 0) { // has a dick + if (slave.dick < 1) { + r.push("tiny penis"); + } else if (slave.dick > 8) { + r.push("absurdly large penis"); + } else if (slave.dick > 5) { + r.push("large penis"); + } else { + r.push("penis"); + } + } else if (slave.vagina === 0) { // has a pussy + r.push("virgin pussy"); + } else if (slave.vagina < 2) { + r.push("tight pussy"); + } else if (slave.vagina > 6) { + r.push("cavernous pussy"); + } else if (slave.vagina > 3) { + r.push("loose pussy"); + } else { + r.push("pussy"); + } + // Back + if (slave.anus === 0) { + r.push(", virgin anus"); + } else if (slave.vagina > 3) { + r.push(", loose anus"); + } + // PHair + if (slave.pubicHStyle === "hairless" || slave.pubicHStyle === "bald") { + r.push(", no pubic hair"); + } else { + r.push(`, pubic hair ${slave.pubicHStyle}`); + } + + // RULES + let rulesParts = []; + // Speech (also check for mute) + if (slave.voice === 0) { + rulesParts.push(`Mute and cannot speak`); + } else if (slave.rules.speech === "restrictive"){ + rulesParts.push(`Not allowed to speak`); + } + // Masturbation rules, also check for ability to molest other slaves (release cases?) + if (slave.rules.release.masturbation === 0){ + rulesParts.push(`Not allowed to masturbate`); + } + // How they address the user + rulesParts.push(`Addresses {{user}} as ${properMaster()}`) + if (rulesParts.length > 0) { + r.push(`\r\nRules: ${rulesParts.join(', ')}`); + } + + // TATTOOS - Too much context for impact? + let tattooParts = []; + if (slave.armsTat) { + tattooParts.push(`${slave.armsTat} arm tattoo`); + } + if (slave.legsTat) { + tattooParts.push(`${slave.legsTat} leg tattoo`); + } + if (slave.bellyTat) { + tattooParts.push(`${slave.bellyTat} belly tattoo`); + } + if (slave.boobsTat) { + tattooParts.push(`${slave.boobsTat} breast tattoo`); + } + + if (tattooParts.length > 0) { + r.push(`\r\nTattoos: ${tattooParts.join(', ')}`); + } + + // CHASTITY + let chasParts = []; + if (slave.chastityVagina === 1) { + chasParts.push(`vagina`); + } + if (slave.chastityPenis === 1) { + chasParts.push(`penis`); + } + if (slave.chastityAnus === 1) { + chasParts.push(`anus`); + } + + if (chasParts.length > 0) { + r.push(`\r\nChastity device covers: ${chasParts.join(', ')}`); + } + + return r.join(""); +}